diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 08899754..0d7087e3 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -13,13 +13,15 @@ jobs: - name: Install dependencies (linux) run: sudo apt install ninja-build - name: Install Qt - uses: jurplel/install-qt-action@v3 + uses: jurplel/install-qt-action@v4 with: version: '5.15.2' + # version: '6.2.*' host: 'linux' target: 'desktop' arch: 'gcc_64' modules: 'qtwebglplugin' + # modules: 'qt5compat qtwebsockets qthttpserver' - name: Checkout repository uses: actions/checkout@v4 with: @@ -27,21 +29,21 @@ jobs: - name: Setup run: | qmake --version - cp app/qtquick/encryption_seed_template.h app/qtquick/encryption_seed.h + cp lib/tools/encryption_seed_template.h lib/tools/encryption_seed.h python -m pip install --upgrade pip jinja2 - name: Check style uses: pre-commit/action@v3.0.1 with: extra_args: --all-files - - name: Build - run: scripts/build.sh linux ${Qt5_DIR}/bin - name: Build httpserver run: | mkdir 3rdparty/build-qthttpserver cd 3rdparty/build-qthttpserver qmake CONFIG+=debug_and_release ../qthttpserver/qthttpserver.pro make -j4 && make install + - name: Build + run: scripts/build.sh linux ${QT_ROOT_DIR}/bin - name: Unit test env: TZ: "Asia/Tokyo" - run: scripts/unittest.sh linux ${Qt5_DIR}/bin + run: scripts/unittest.sh linux ${QT_ROOT_DIR}/bin diff --git a/README.md b/README.md index ebd55536..0e0074f8 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ REM checkout repo >cd Hagoromo >git submodule update -i REM copy and edit encryption seed ->copy app\qtquick\encryption_seed_template.h app\qtquick\encryption_seed.h +>copy lib\tools\encryption_seed_template.h lib\tools\encryption_seed.h REM build Hagoromo >.\scripts\build.bat path\to\Qt\5.15.2\msvc2019_64\bin REM Execute @@ -73,8 +73,8 @@ $ git submodule update -i # setup dependent modules $ sudo apt-get install zlib1g-dev # copy and edit encryption seed -$ cp app/qtquick/encryption_seed_template.h app/qtquick/encryption_seed.h -$ vi app/qtquick/encryption_seed.h +$ cp lib/tools/encryption_seed_template.h lib/tools/encryption_seed.h +$ vi lib/tools/encryption_seed.h # build Hagoromo $ ./scripts/build.sh linux path/to/Qt/5.15.2/gcc_64/bin # exec hagoromo @@ -91,8 +91,8 @@ $ git clone git@github.com:ioriayane/Hagoromo.git $ cd Hagoromo $ git submodule update -i # copy and edit encryption seed -$ cp app/qtquick/encryption_seed_template.h app/qtquick/encryption_seed.h -$ vi app/qtquick/encryption_seed.h +$ cp lib/tools/encryption_seed_template.h lib/tools/encryption_seed.h +$ vi lib/tools/encryption_seed.h # build Hagoromo $ ./scripts/build.sh mac path/to/Qt/5.15.2/gcc_64/bin # exec hagoromo @@ -208,7 +208,7 @@ REM checkout repo >cd Hangoromo >git submodule update -i REM copy and edit encryption seed ->copy app\qtquick\encryption_seed_template.h app\qtquick\encryption_seed.h +>copy lib\tools\encryption_seed_template.h lib\tools\encryption_seed.h REM build Hagoromo >.\scripts\build.bat path\to\Qt\5.15.2\msvc2019_64\bin REM Execute @@ -227,8 +227,8 @@ $ git submodule update -i # setup dependent modules $ sudo apt-get install zlib1g-dev # copy and edit encryption seed -$ cp app/qtquick/encryption_seed_template.h app/qtquick/encryption_seed.h -$ vi app/qtquick/encryption_seed.h +$ cp lib/tools/encryption_seed_template.h lib/tools/encryption_seed.h +$ vi lib/tools/encryption_seed.h # build Hagoromo $ ./scripts/build.sh linux path/to/Qt/5.15.2/gcc_64/bin # exec hagoromo @@ -245,8 +245,8 @@ $ git clone git@github.com:ioriayane/Hagoromo.git $ cd Hagoromo $ git submodule update -i # copy and edit encryption seed -$ cp app/qtquick/encryption_seed_template.h app/qtquick/encryption_seed.h -$ vi app/qtquick/encryption_seed.h +$ cp lib/tools/encryption_seed_template.h lib/tools/encryption_seed.h +$ vi lib/tools/encryption_seed.h # build Hagoromo $ ./scripts/build.sh mac path/to/Qt/5.15.2/gcc_64/bin # exec hagoromo diff --git a/app/app.pro b/app/app.pro index 41319d55..cfbf638a 100644 --- a/app/app.pro +++ b/app/app.pro @@ -21,6 +21,7 @@ QML_FILES = \ qml/controls/CalendarPicker.qml \ qml/controls/ClickableFrame.qml \ qml/controls/ComboBoxEx.qml \ + qml/controls/DragAndDropArea.qml \ qml/controls/IconButton.qml \ qml/controls/IconLabelFrame.qml \ qml/controls/ImageWithIndicator.qml \ diff --git a/app/deps.pri b/app/deps.pri index 39ae113e..aa3184c5 100644 --- a/app/deps.pri +++ b/app/deps.pri @@ -6,6 +6,7 @@ else:unix: LIBS += -L$$OUT_PWD/../lib/ -llib INCLUDEPATH += $$PWD/../lib DEPENDPATH += $$PWD/../lib +RESOURCES += $$PWD/../lib/lib.qrc win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../lib/release/liblib.a else:win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../lib/debug/liblib.a diff --git a/app/i18n/app_ja.qm b/app/i18n/app_ja.qm index 0ea89b69..01e66a6e 100644 Binary files a/app/i18n/app_ja.qm and b/app/i18n/app_ja.qm differ diff --git a/app/i18n/app_ja.ts b/app/i18n/app_ja.ts index 86e44293..7849f9be 100644 --- a/app/i18n/app_ja.ts +++ b/app/i18n/app_ja.ts @@ -98,12 +98,12 @@ カラムの追加 - + Account アカウント - + Column type カラムタイプ @@ -112,17 +112,17 @@ カスタムフィードの検索 - + Cancel キャンセル - + Logs ログ - + Add 追加 @@ -140,37 +140,37 @@ リストの編集 - + Avatar アイコン - + Name 名称 - + Description 説明 - + Cancel キャンセル - + Add 追加 - + Update 更新 - + Select contents コンテンツの選択 @@ -226,12 +226,12 @@ リストへ追加/削除 - + Add list リストの追加 - + Close 閉じる @@ -273,14 +273,14 @@ AtpAbstractListModel - - + + Blocked ブロック中 - - + + Detached by author 投稿者によって切り離し済み @@ -293,7 +293,7 @@ ブロック中のアカウント - + Close 閉じる @@ -306,7 +306,7 @@ ブロック中のリスト - + Close 閉じる @@ -324,27 +324,27 @@ Please recreate AppPassword in the official application. ChatListView - + Unmute conversation チャットのミュートを解除 - + Mute conversation チャットをミュート - + Leave conversation チャットを離脱 - + Start a new chat 新しいチャットを開始 - + Search 検索 @@ -352,37 +352,37 @@ Please recreate AppPassword in the official application. ChatMessageListView - + Quoted content warning 閲覧注意な引用 - + Blocked ブロック中 - + Copy message メッセージをコピー - + Delete for me 自分宛を削除 - + Report message メッセージを通報 - + Post url or at-uri ポストのURLまたはat-uri - + Write a message メッセージを書く @@ -593,94 +593,94 @@ Please recreate AppPassword in the official application. 引用ポスト - + Home ホーム - + Notifications 通知 - + Search posts 検索(ポスト) - + Search users 検索(ユーザー) - - + + Feed フィード - + User ユーザー - + List リスト - - + + Chat チャット - + Realtime リアルタイム - + Unknown 不明 - + Move to left 左へ移動 - + Move to right 右へ移動 - + Delete column 削除 - + Copy url URLをコピー - + Open in Official 公式で開く - + Drop 解除 - + Save 保存 - + Settings 設定 @@ -748,21 +748,29 @@ Please recreate AppPassword in the official application. カスタムフィードの検索 - + Search 検索 - + Cancel キャンセル - + Add 追加 + + DragAndDropArea + + + Detecting... + ドロップする + + EditProfileDialog @@ -771,27 +779,27 @@ Please recreate AppPassword in the official application. プロフィールの編集 - + Display Name 表示名 - + Description 説明 - + Cancel キャンセル - + Update 更新 - + Select contents コンテンツの選択 @@ -1858,67 +1866,67 @@ Please recreate AppPassword in the official application. ListDetailView - + List リスト - + Edit 編集 - + Muted ミュート中 - + Blocked ブロック中 - + Copy Official Url 公式のURLをコピー - + Open in new col 新しいカラムで開く - + Open in Official 公式で開く - + Delete list リストを削除 - + Unmute list リストのミュート解除 - + Mute list リストをミュート - + Unblock list リストのブロック解除 - + Block list リストのブロック - + Users ユーザー @@ -1926,32 +1934,32 @@ Please recreate AppPassword in the official application. ListsListView - + Unmute ミュート解除 - + Mute ミュートする - + Unblock ブロック解除 - + Block ブロックする - + Muted ミュート中 - + Blocked ブロック中 @@ -1989,7 +1997,7 @@ Please recreate AppPassword in the official application. 月毎 - + Close 閉じる @@ -2048,7 +2056,7 @@ Please recreate AppPassword in the official application. ミュート中のアカウント - + Close 閉じる @@ -2061,7 +2069,7 @@ Please recreate AppPassword in the official application. ミュート中のリスト - + Close 閉じる @@ -2080,12 +2088,12 @@ Please recreate AppPassword in the official application. NotificationDelegate - + Post from an account you muted. ミュートしているアカウントのポスト - + signed up with your starter pack あなたのスターターパックで登録しました @@ -2217,7 +2225,7 @@ Please recreate AppPassword in the official application. PostDelegate - + Post from an account you muted. ミュートしているアカウントのポスト @@ -2253,7 +2261,7 @@ Please recreate AppPassword in the official application. リンクカード - + Link card URL リンクカードのURL @@ -2266,22 +2274,22 @@ Please recreate AppPassword in the official application. リンクカードかフィードカードかリストカードのURL - + Link card URL, Custom feed URL, List URL, Post URL リンクカード/フィードカード/リストカード/ポストのURL - + Cancel キャンセル - + Post ポスト - + Select contents コンテンツの選択 @@ -2321,12 +2329,12 @@ Please recreate AppPassword in the official application. ミュート中 - + Follows you あなたをフォロー中 - + Muted user ミュート中 @@ -2444,12 +2452,12 @@ Please recreate AppPassword in the official application. 通報 - + Account blocked ブロックしたアカウント - + Account muted ミュートしたアカウント @@ -2458,7 +2466,7 @@ Please recreate AppPassword in the official application. このアカウントに設定されたラベル : - + This account has blocked you あなたをブロックしているアカウント @@ -2490,134 +2498,134 @@ Please recreate AppPassword in the official application. RecordOperator - - + + Posting ... %1 - + Posting ... - + Repost ... - + Like ... - + Follow ... - + Mute ... - + Block ... - + Block list ... - + Create list ... (%1) - + Add to list ... - + Delete post ... - + Delete like ... - + Delete repost ... - + Unfollow ... - + Unmute ... - + Unblock ... - + Unblock block list ... - + Delete list ... - + Delete list item ... - + Update profile ... (%1) - + Update post pinning ... (%1) - + Update list ... (%1) - + Update who can reply ... - - + + Update quote status ... - + Uploading images ... (%1/%2) - + Delete list item ... (%1) @@ -2907,57 +2915,57 @@ Why should this message be reviewed? 投稿への反応の設定 - + Quote settings 引用の設定 - + Quote posts enabled 引用を可能にする - + Reply settings 返信の設定 - + Everybody 誰でも - + Nobody 自分のみ - + Combine these options 以下の組み合わせ - + Mentioned users メンションするユーザー - + Followed users フォローしているユーザー - + Users in "%1" "%1"のユーザー - + Cancel キャンセル - + Apply 適用 @@ -3349,42 +3357,42 @@ Why should this message be reviewed? SuggestionProfileListView - + @%s can't be messaged. @%sにメッセージを送れません。 - + Chat:All チャット:全員 - + Chat:Following チャット:フォローしているユーザー - + Chat:None チャット:誰からも受け取らない - + Chat:Not set チャット:未設定 - + Following フォロー中 - + Follows you あなたをフォロー中 - + Muted user ミュート中 @@ -3400,7 +3408,7 @@ Why should this message be reviewed? TimelineView - + Quoted content warning 閲覧注意な引用 @@ -3413,7 +3421,7 @@ Why should this message be reviewed? - + Search posts 検索(ポスト) @@ -3437,23 +3445,23 @@ Why should this message be reviewed? いくつかのアカウントでログインが必要です。 - - + + Updating 'Edit interaction settings' ... 投稿への反応の設定を更新中 ... - + Loading lists リストの読み込み中 - + Chat チャット - + Loading account(s) ... アカウント情報の読み込み中 ... diff --git a/app/main.cpp b/app/main.cpp index 070d4e58..6a79d606 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -38,7 +38,6 @@ #include "qtquick/list/listblockslistmodel.h" #include "qtquick/list/listmuteslistmodel.h" #include "qtquick/thumbnailprovider.h" -#include "qtquick/encryption.h" #include "qtquick/profile/userprofile.h" #include "qtquick/timeline/userpost.h" #include "qtquick/systemtool.h" @@ -60,6 +59,7 @@ #include "qtquick/controls/calendartablemodel.h" #include "qtquick/realtime/realtimefeedlistmodel.h" +#include "tools/encryption.h" #include "tools/translatorchanger.h" void setAppFont(QGuiApplication &app) @@ -91,7 +91,7 @@ int main(int argc, char *argv[]) app.setOrganizationName(QStringLiteral("relog")); app.setOrganizationDomain(QStringLiteral("hagoromo.relog.tech")); app.setApplicationName(QStringLiteral("Hagoromo")); - app.setApplicationVersion(QStringLiteral("0.39.0")); + app.setApplicationVersion(QStringLiteral("0.40.0")); #ifndef HAGOROMO_RELEASE_BUILD app.setApplicationVersion(app.applicationVersion() + "d"); #endif diff --git a/app/qml/controls/ClickableFrame.qml b/app/qml/controls/ClickableFrame.qml index e25a9393..7130c40e 100644 --- a/app/qml/controls/ClickableFrame.qml +++ b/app/qml/controls/ClickableFrame.qml @@ -12,7 +12,11 @@ Frame { property string style: "Normal" property color borderColor: Material.color(Material.Grey, Material.Shade600) - background: MouseArea { + contentItem: MouseArea { + onClicked: (mouse) => clickableFrame.clicked(mouse) + } + + background: Item { Rectangle { id: backgroundRect states: [ @@ -65,7 +69,5 @@ Frame { color: "transparent" radius: 2 } - - onClicked: (mouse) => clickableFrame.clicked(mouse) } } diff --git a/app/qml/controls/DragAndDropArea.qml b/app/qml/controls/DragAndDropArea.qml new file mode 100644 index 00000000..6df22293 --- /dev/null +++ b/app/qml/controls/DragAndDropArea.qml @@ -0,0 +1,45 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls.Material 2.15 + +import tech.relog.hagoromo.singleton 1.0 + +//ドラッグ状態に反応してことを表す四角 +Rectangle { + id: dropRect + radius: 4 + color: Material.color(Material.Grey) + opacity: 0 + states: State { + //ドラッグ状態で領域内にいたら背景色と文字色を変更 + when: imageDropArea.containsDrag + PropertyChanges { target: dropRect; opacity: 0.5 } + PropertyChanges { target: dropRectMessage; opacity: 1 } + } + signal dropped(var urls) + + //ドラッグ状態で領域内にいるときの説明 + Text { + id: dropRectMessage + anchors.centerIn: parent + color: "black" + text: qsTr("Detecting...") + font.pointSize: AdjustedValues.f14 + } + //ドロップの受付 + DropArea { + id: imageDropArea + anchors.fill: parent + keys: ["text/uri-list"] + onDropped: (drop) => { + if(drop.hasUrls){ + var new_urls = [] + for(var i=0; i 0){ avatarImage.source = addListDialog.avatar diff --git a/app/qml/dialogs/AddToListDialog.qml b/app/qml/dialogs/AddToListDialog.qml index d9e450e5..ee9901e2 100644 --- a/app/qml/dialogs/AddToListDialog.qml +++ b/app/qml/dialogs/AddToListDialog.qml @@ -27,7 +27,7 @@ Dialog { signal requestAddList() onOpened: { - if(account.service.length === 0){ + if(account.uuid.length === 0){ return } addListDialog.account.uuid = account.uuid @@ -39,8 +39,7 @@ Dialog { addListDialog.account.avatar = account.avatar listsListModel.clear() - listsListModel.setAccount(account.service, account.did, account.handle, - account.email, account.accessJwt, account.refreshJwt) + listsListModel.setAccount(account.uuid) listsListModel.getLatest() } onClosed: { diff --git a/app/qml/dialogs/BlockedAccountsDialog.qml b/app/qml/dialogs/BlockedAccountsDialog.qml index ca0a3cfd..2f3e008a 100644 --- a/app/qml/dialogs/BlockedAccountsDialog.qml +++ b/app/qml/dialogs/BlockedAccountsDialog.qml @@ -26,12 +26,11 @@ Dialog { signal errorOccured(string account_uuid, string code, string message) onOpened: { - if(account.service.length === 0){ + if(account.uuid.length === 0){ return } - blocksListModel.setAccount(account.service, account.did, account.handle, - account.email, account.accessJwt, account.refreshJwt) + blocksListModel.setAccount(account.uuid) blocksListModel.getLatest() profileListScrollView.currentIndex = -1 diff --git a/app/qml/dialogs/BlockedListsDialog.qml b/app/qml/dialogs/BlockedListsDialog.qml index 4b52661a..9cfe4a43 100644 --- a/app/qml/dialogs/BlockedListsDialog.qml +++ b/app/qml/dialogs/BlockedListsDialog.qml @@ -26,11 +26,10 @@ Dialog { signal errorOccured(string account_uuid, string code, string message) onOpened: { - if(account.service.length === 0){ + if(account.uuid.length === 0){ return } - listBlocksListModel.setAccount(account.service, account.did, account.handle, - account.email, account.accessJwt, account.refreshJwt) + listBlocksListModel.setAccount(account.uuid) listBlocksListModel.getLatest() listScrollView.currentIndex = -1 diff --git a/app/qml/dialogs/DiscoverFeedsDialog.qml b/app/qml/dialogs/DiscoverFeedsDialog.qml index 55394930..7a5a8e1c 100644 --- a/app/qml/dialogs/DiscoverFeedsDialog.qml +++ b/app/qml/dialogs/DiscoverFeedsDialog.qml @@ -27,12 +27,11 @@ Dialog { signal errorOccured(string account_uuid, string code, string message) onOpened: { - if(account.service.length === 0){ + if(account.uuid.length === 0){ return } - feedGeneratorListModel.setAccount(account.service, account.did, account.handle, - account.email, account.accessJwt, account.refreshJwt) + feedGeneratorListModel.setAccount(account.uuid) feedGeneratorListModel.getLatest() generatorScrollView.currentIndex = -1 diff --git a/app/qml/dialogs/EditProfileDialog.qml b/app/qml/dialogs/EditProfileDialog.qml index 1de48657..9fb38d3c 100644 --- a/app/qml/dialogs/EditProfileDialog.qml +++ b/app/qml/dialogs/EditProfileDialog.qml @@ -33,11 +33,10 @@ Dialog { signal errorOccured(string account_uuid, string code, string message) onOpened: { - if(account.service.length === 0){ + if(account.uuid.length === 0){ return } - recordOperator.setAccount(account.service, account.did, account.handle, - account.email, account.accessJwt, account.refreshJwt) + recordOperator.setAccount(account.uuid) avatarImage.source = editProfileDialog.avatar bannerImage.source = editProfileDialog.banner displayNameText.text = editProfileDialog.displayName diff --git a/app/qml/dialogs/LogViewDialog.qml b/app/qml/dialogs/LogViewDialog.qml index e04ffa1e..2a4b34df 100644 --- a/app/qml/dialogs/LogViewDialog.qml +++ b/app/qml/dialogs/LogViewDialog.qml @@ -194,9 +194,7 @@ Dialog { text: qsTr("Search") onClicked: { console.log("search:" + searchText.text) - logSearchFeedListModel.setAccount(account.service, account.did, - account.handle, account.email, - account.accessJwt, account.refreshJwt) + logSearchFeedListModel.setAccount(account.uuid) logSearchFeedListModel.selectCondition = searchText.text logSearchFeedListModel.clear() logSearchFeedListModel.getLatest() @@ -252,9 +250,7 @@ Dialog { } onClickedItem: (name) => { console.log("select:" + name) - logDailyFeedListModel.setAccount(account.service, account.did, - account.handle, account.email, - account.accessJwt, account.refreshJwt) + logDailyFeedListModel.setAccount(account.uuid) logDailyFeedListModel.selectCondition = name logDailyFeedListModel.clear() logDailyFeedListModel.getLatest() @@ -304,9 +300,7 @@ Dialog { } onClickedItem: (name) => { console.log("select:" + name) - logMonthlyFeedListModel.setAccount(account.service, account.did, - account.handle, account.email, - account.accessJwt, account.refreshJwt) + logMonthlyFeedListModel.setAccount(account.uuid) logMonthlyFeedListModel.selectCondition = name logMonthlyFeedListModel.clear() logMonthlyFeedListModel.getLatest() diff --git a/app/qml/dialogs/MutedAccountsDialog.qml b/app/qml/dialogs/MutedAccountsDialog.qml index 9d482b15..9ee740f9 100644 --- a/app/qml/dialogs/MutedAccountsDialog.qml +++ b/app/qml/dialogs/MutedAccountsDialog.qml @@ -26,11 +26,10 @@ Dialog { signal errorOccured(string account_uuid, string code, string message) onOpened: { - if(account.service.length === 0){ + if(account.uuid.length === 0){ return } - mutesListModel.setAccount(account.service, account.did, account.handle, - account.email, account.accessJwt, account.refreshJwt) + mutesListModel.setAccount(account.uuid) mutesListModel.getLatest() profileListScrollView.currentIndex = -1 diff --git a/app/qml/dialogs/MutedListsDialog.qml b/app/qml/dialogs/MutedListsDialog.qml index 6fdabbd3..6782402d 100644 --- a/app/qml/dialogs/MutedListsDialog.qml +++ b/app/qml/dialogs/MutedListsDialog.qml @@ -26,11 +26,10 @@ Dialog { signal errorOccured(string account_uuid, string code, string message) onOpened: { - if(account.service.length === 0){ + if(account.uuid.length === 0){ return } - listMutesListModel.setAccount(account.service, account.did, account.handle, - account.email, account.accessJwt, account.refreshJwt) + listMutesListModel.setAccount(account.uuid) listMutesListModel.getLatest() listScrollView.currentIndex = -1 diff --git a/app/qml/dialogs/PostDialog.qml b/app/qml/dialogs/PostDialog.qml index 47ddaf88..91920bbc 100644 --- a/app/qml/dialogs/PostDialog.qml +++ b/app/qml/dialogs/PostDialog.qml @@ -59,6 +59,12 @@ Dialog { signal errorOccured(string account_uuid, string code, string message) + function openWithFiles(urls){ + if(embedImageListModel.append(urls)){ + postDialog.open() + } + } + onOpened: { var i = accountModel.indexAt(defaultAccountUuid) accountCombo.currentIndex = -1 @@ -162,562 +168,548 @@ Dialog { implicitHeight: (mainLayout.height > basisHeight) ? basisHeight : mainLayout.height ScrollBar.vertical.policy: (mainLayout.height > basisHeight) ? ScrollBar.AlwaysOn :ScrollBar.AlwaysOff clip: true - ColumnLayout { - id: mainLayout - Frame { - id: replyFrame - Layout.preferredWidth: postText.width - Layout.maximumHeight: 200 * AdjustedValues.ratio - visible: postType === "reply" - clip: true - ColumnLayout { - anchors.fill: parent - RowLayout { - AvatarImage { - id: replyAvatarImage - Layout.preferredWidth: AdjustedValues.i16 - Layout.preferredHeight: AdjustedValues.i16 - source: replyAvatar + Item { + implicitWidth: mainLayout.width + implicitHeight: mainLayout.height + ColumnLayout { + id: mainLayout + Frame { + id: replyFrame + Layout.preferredWidth: postText.width + Layout.maximumHeight: 200 * AdjustedValues.ratio + visible: postType === "reply" + clip: true + ColumnLayout { + anchors.fill: parent + RowLayout { + AvatarImage { + id: replyAvatarImage + Layout.preferredWidth: AdjustedValues.i16 + Layout.preferredHeight: AdjustedValues.i16 + source: replyAvatar + } + Author { + layoutWidth: replyFrame.width - replyFrame.padding * 2 - replyAvatarImage.width - parent.spacing + displayName: replyDisplayName + handle: replyHandle + indexedAt: replyIndexedAt + } } - Author { - layoutWidth: replyFrame.width - replyFrame.padding * 2 - replyAvatarImage.width - parent.spacing - displayName: replyDisplayName - handle: replyHandle - indexedAt: replyIndexedAt + Label { + Layout.preferredWidth: postText.width - replyFrame.padding * 2 + wrapMode: Text.WrapAnywhere + font.pointSize: AdjustedValues.f8 + text: replyText } } - Label { - Layout.preferredWidth: postText.width - replyFrame.padding * 2 - wrapMode: Text.WrapAnywhere - font.pointSize: AdjustedValues.f8 - text: replyText - } } - } - RowLayout { - ComboBox { - id: accountCombo - Layout.preferredWidth: 200 * AdjustedValues.ratio + AdjustedValues.i24 - Layout.preferredHeight: implicitHeight * AdjustedValues.ratio - enabled: !createRecord.running - font.pointSize: AdjustedValues.f10 - textRole: "handle" - valueRole: "did" - delegate: ItemDelegate { - width: parent.width - height: implicitHeight * AdjustedValues.ratio + RowLayout { + ComboBox { + id: accountCombo + Layout.preferredWidth: 200 * AdjustedValues.ratio + AdjustedValues.i24 + Layout.preferredHeight: implicitHeight * AdjustedValues.ratio + enabled: !createRecord.running font.pointSize: AdjustedValues.f10 - onClicked: accountCombo.currentIndex = model.index - AccountLayout { - anchors.fill: parent - anchors.margins: 10 - source: model.avatar - handle: model.handle + textRole: "handle" + valueRole: "did" + delegate: ItemDelegate { + width: parent.width + height: implicitHeight * AdjustedValues.ratio + font.pointSize: AdjustedValues.f10 + onClicked: accountCombo.currentIndex = model.index + AccountLayout { + anchors.fill: parent + anchors.margins: 10 + source: model.avatar + handle: model.handle + } + } + contentItem: AccountLayout { + id: accountAvatarLayout + width: parent.width + height: parent.height + leftMargin: 10 + handle: accountCombo.displayText } - } - contentItem: AccountLayout { - id: accountAvatarLayout - width: parent.width - height: parent.height - leftMargin: 10 - handle: accountCombo.displayText - } - onCurrentIndexChanged: { - var row = accountCombo.currentIndex - if(row >= 0){ - accountAvatarLayout.source = - postDialog.accountModel.item(row, AccountListModel.AvatarRole) - postLanguagesButton.setLanguageText( - postDialog.accountModel.item(row, AccountListModel.PostLanguagesRole) - ) - selectThreadGateDialog.initialQuoteEnabled = postDialog.accountModel.item(row, AccountListModel.PostGateQuoteEnabledRole) - selectThreadGateDialog.initialType = postDialog.accountModel.item(row, AccountListModel.ThreadGateTypeRole) - selectThreadGateDialog.initialOptions = postDialog.accountModel.item(row, AccountListModel.ThreadGateOptionsRole) - // リプライ制限のダイアログを開かずにポストするときのため選択済みにも設定する - selectThreadGateDialog.selectedQuoteEnabled = selectThreadGateDialog.initialQuoteEnabled - selectThreadGateDialog.selectedType = selectThreadGateDialog.initialType - selectThreadGateDialog.selectedOptions = selectThreadGateDialog.initialOptions - // 入力中にアカウントを切り替えるかもなので選んだ時に設定する - mentionSuggestionView.setAccount(postDialog.accountModel.item(row, AccountListModel.ServiceRole), - postDialog.accountModel.item(row, AccountListModel.DidRole), - postDialog.accountModel.item(row, AccountListModel.HandleRole), - postDialog.accountModel.item(row, AccountListModel.AccessJwtRole)) + onCurrentIndexChanged: { + var row = accountCombo.currentIndex + if(row >= 0){ + accountAvatarLayout.source = + postDialog.accountModel.item(row, AccountListModel.AvatarRole) + postLanguagesButton.setLanguageText( + postDialog.accountModel.item(row, AccountListModel.PostLanguagesRole) + ) + selectThreadGateDialog.initialQuoteEnabled = postDialog.accountModel.item(row, AccountListModel.PostGateQuoteEnabledRole) + selectThreadGateDialog.initialType = postDialog.accountModel.item(row, AccountListModel.ThreadGateTypeRole) + selectThreadGateDialog.initialOptions = postDialog.accountModel.item(row, AccountListModel.ThreadGateOptionsRole) + // リプライ制限のダイアログを開かずにポストするときのため選択済みにも設定する + selectThreadGateDialog.selectedQuoteEnabled = selectThreadGateDialog.initialQuoteEnabled + selectThreadGateDialog.selectedType = selectThreadGateDialog.initialType + selectThreadGateDialog.selectedOptions = selectThreadGateDialog.initialOptions + // 入力中にアカウントを切り替えるかもなので選んだ時に設定する + mentionSuggestionView.setAccount(postDialog.accountModel.item(row, AccountListModel.UuidRole)) + } } } - } - Item { - Layout.fillWidth: true - Layout.preferredHeight: 1 - } - IconButton { - id: threadGateButton - enabled: !createRecord.running && (postType !== "reply") - iconSource: "../images/thread.png" - iconSize: AdjustedValues.i18 - flat: true - foreground: (selectThreadGateDialog.selectedType !== "everybody" || !selectThreadGateDialog.selectedQuoteEnabled) - ? Material.accent : Material.foreground - onClicked: { - var row = accountCombo.currentIndex; - selectThreadGateDialog.account.service = postDialog.accountModel.item(row, AccountListModel.ServiceRole) - selectThreadGateDialog.account.did = postDialog.accountModel.item(row, AccountListModel.DidRole) - selectThreadGateDialog.account.handle = postDialog.accountModel.item(row, AccountListModel.HandleRole) - selectThreadGateDialog.account.email = postDialog.accountModel.item(row, AccountListModel.EmailRole) - selectThreadGateDialog.account.accessJwt = postDialog.accountModel.item(row, AccountListModel.AccessJwtRole) - selectThreadGateDialog.account.refreshJwt = postDialog.accountModel.item(row, AccountListModel.RefreshJwtRole) - selectThreadGateDialog.initialQuoteEnabled = selectThreadGateDialog.selectedQuoteEnabled - selectThreadGateDialog.initialType = selectThreadGateDialog.selectedType - selectThreadGateDialog.initialOptions = selectThreadGateDialog.selectedOptions - selectThreadGateDialog.open() + Item { + Layout.fillWidth: true + Layout.preferredHeight: 1 } - } - - IconButton { - id: postLanguagesButton - enabled: !createRecord.running - iconSource: "../images/language.png" - iconSize: AdjustedValues.i18 - flat: true - onClicked: { - languageSelectionDialog.setSelectedLanguages( - postDialog.accountModel.item(accountCombo.currentIndex, AccountListModel.PostLanguagesRole) - ) - languageSelectionDialog.open() + IconButton { + id: threadGateButton + enabled: !createRecord.running && (postType !== "reply") + iconSource: "../images/thread.png" + iconSize: AdjustedValues.i18 + flat: true + foreground: (selectThreadGateDialog.selectedType !== "everybody" || !selectThreadGateDialog.selectedQuoteEnabled) + ? Material.accent : Material.foreground + onClicked: { + var row = accountCombo.currentIndex; + selectThreadGateDialog.account.service = postDialog.accountModel.item(row, AccountListModel.ServiceRole) + selectThreadGateDialog.account.did = postDialog.accountModel.item(row, AccountListModel.DidRole) + selectThreadGateDialog.account.handle = postDialog.accountModel.item(row, AccountListModel.HandleRole) + selectThreadGateDialog.account.email = postDialog.accountModel.item(row, AccountListModel.EmailRole) + selectThreadGateDialog.account.accessJwt = postDialog.accountModel.item(row, AccountListModel.AccessJwtRole) + selectThreadGateDialog.account.refreshJwt = postDialog.accountModel.item(row, AccountListModel.RefreshJwtRole) + selectThreadGateDialog.initialQuoteEnabled = selectThreadGateDialog.selectedQuoteEnabled + selectThreadGateDialog.initialType = selectThreadGateDialog.selectedType + selectThreadGateDialog.initialOptions = selectThreadGateDialog.selectedOptions + selectThreadGateDialog.open() + } } - function setLanguageText(post_langs){ - var langs = languageListModel.convertLanguageNames(post_langs) - var lang_str = "" - for(var i=0;i 0){ - lang_str += ", " - } - lang_str += langs[i] - } - if(lang_str.length > 13){ - lang_str = lang_str.substring(0, 10) + "..." + IconButton { + id: postLanguagesButton + enabled: !createRecord.running + iconSource: "../images/language.png" + iconSize: AdjustedValues.i18 + flat: true + onClicked: { + languageSelectionDialog.setSelectedLanguages( + postDialog.accountModel.item(accountCombo.currentIndex, AccountListModel.PostLanguagesRole) + ) + languageSelectionDialog.open() } - iconText = lang_str - } - } - } - ScrollView { - z: 99 // MentionSuggetionViewを最前に表示するため - Layout.preferredWidth: 420 * AdjustedValues.ratio - Layout.preferredHeight: 120 * AdjustedValues.ratio - TextArea { - id: postText - verticalAlignment: TextInput.AlignTop - enabled: !createRecord.running - wrapMode: TextInput.WordWrap - selectByMouse: true - font.pointSize: AdjustedValues.f10 - property int realTextLength: systemTool.countText(text) - onTextChanged: mentionSuggestionView.reload(getText(0, cursorPosition)) - Keys.onPressed: (event) => { - if(mentionSuggestionView.visible){ - console.log("Key(v):" + event.key) - if(event.key === Qt.Key_Up){ - mentionSuggestionView.up() - event.accepted = true - }else if(event.key === Qt.Key_Down){ - mentionSuggestionView.down() - event.accepted = true - }else if(event.key === Qt.Key_Enter || - event.key === Qt.Key_Return){ - mentionSuggestionView.accept() - event.accepted = true - }else if(event.key === Qt.Key_Escape){ - mentionSuggestionView.clear() - } - }else{ - console.log("Key(n):" + event.key) - if(event.key === Qt.Key_Space && (event.modifiers & Qt.ControlModifier)){ - mentionSuggestionView.reload(getText(0, cursorPosition)) - event.accepted = true - } - } - } - MentionSuggestionView { - id: mentionSuggestionView - anchors.left: parent.left - anchors.right: parent.right - onVisibleChanged: { - var rect = postText.positionToRectangle(postText.cursorPosition) - y = rect.y + rect.height + 2 + function setLanguageText(post_langs){ + var langs = languageListModel.convertLanguageNames(post_langs) + var lang_str = "" + for(var i=0;i 0){ + lang_str += ", " + } + lang_str += langs[i] + } + if(lang_str.length > 13){ + lang_str = lang_str.substring(0, 10) + "..." + } + iconText = lang_str } - onSelected: (handle) => { - var after = replaceText(postText.text, postText.cursorPosition, handle) - if(after !== postText.text){ - postText.text = after - postText.cursorPosition = postText.text.length - } - } } } - } - RowLayout { - Layout.preferredWidth: postText.width - visible: embedImageListModel.count === 0 ScrollView { - Layout.fillWidth: true - clip: true + z: 99 // MentionSuggetionViewを最前に表示するため + Layout.preferredWidth: 420 * AdjustedValues.ratio + Layout.preferredHeight: 120 * AdjustedValues.ratio TextArea { - id: addingExternalLinkUrlText + id: postText + verticalAlignment: TextInput.AlignTop + enabled: !createRecord.running + wrapMode: TextInput.WordWrap selectByMouse: true font.pointSize: AdjustedValues.f10 - placeholderText: (quoteValid === false) ? - qsTr("Link card URL, Custom feed URL, List URL, Post URL") : - qsTr("Link card URL") + property int realTextLength: systemTool.countText(text) + onTextChanged: mentionSuggestionView.reload(getText(0, cursorPosition)) + Keys.onPressed: (event) => { + if(mentionSuggestionView.visible){ + console.log("Key(v):" + event.key) + if(event.key === Qt.Key_Up){ + mentionSuggestionView.up() + event.accepted = true + }else if(event.key === Qt.Key_Down){ + mentionSuggestionView.down() + event.accepted = true + }else if(event.key === Qt.Key_Enter || + event.key === Qt.Key_Return){ + mentionSuggestionView.accept() + event.accepted = true + }else if(event.key === Qt.Key_Escape){ + mentionSuggestionView.clear() + } + }else{ + console.log("Key(n):" + event.key) + if(event.key === Qt.Key_Space && (event.modifiers & Qt.ControlModifier)){ + mentionSuggestionView.reload(getText(0, cursorPosition)) + event.accepted = true + } + } + } + MentionSuggestionView { + id: mentionSuggestionView + anchors.left: parent.left + anchors.right: parent.right + onVisibleChanged: { + var rect = postText.positionToRectangle(postText.cursorPosition) + y = rect.y + rect.height + 2 + } + onSelected: (handle) => { + var after = replaceText(postText.text, postText.cursorPosition, handle) + if(after !== postText.text){ + postText.text = after + postText.cursorPosition = postText.text.length + } + } + } } } - IconButton { - id: externalLinkButton - iconSource: "../images/add.png" - enabled: addingExternalLinkUrlText.text.length > 0 && - !externalLink.running && - !feedGeneratorLink.running && - !listLink.running && - !createRecord.running - onClicked: { - var uri = addingExternalLinkUrlText.text - var row = accountCombo.currentIndex - if(feedGeneratorLink.checkUri(uri, "feed") && !quoteValid){ - feedGeneratorLink.setAccount(postDialog.accountModel.item(row, AccountListModel.ServiceRole), - postDialog.accountModel.item(row, AccountListModel.DidRole), - postDialog.accountModel.item(row, AccountListModel.HandleRole), - postDialog.accountModel.item(row, AccountListModel.EmailRole), - postDialog.accountModel.item(row, AccountListModel.AccessJwtRole), - postDialog.accountModel.item(row, AccountListModel.RefreshJwtRole)) - feedGeneratorLink.getFeedGenerator(uri) - }else if(listLink.checkUri(uri, "lists") && !quoteValid){ - listLink.setAccount(postDialog.accountModel.item(row, AccountListModel.ServiceRole), - postDialog.accountModel.item(row, AccountListModel.DidRole), - postDialog.accountModel.item(row, AccountListModel.HandleRole), - postDialog.accountModel.item(row, AccountListModel.EmailRole), - postDialog.accountModel.item(row, AccountListModel.AccessJwtRole), - postDialog.accountModel.item(row, AccountListModel.RefreshJwtRole)) - listLink.getList(uri) - }else if(postLink.checkUri(uri, "post") && !quoteValid && postType !== "quote"){ - postLink.setAccount(postDialog.accountModel.item(row, AccountListModel.ServiceRole), - postDialog.accountModel.item(row, AccountListModel.DidRole), - postDialog.accountModel.item(row, AccountListModel.HandleRole), - postDialog.accountModel.item(row, AccountListModel.EmailRole), - postDialog.accountModel.item(row, AccountListModel.AccessJwtRole), - postDialog.accountModel.item(row, AccountListModel.RefreshJwtRole)) - postLink.getPost(uri) - }else{ - externalLink.getExternalLink(uri) + RowLayout { + Layout.preferredWidth: postText.width + visible: embedImageListModel.count === 0 + ScrollView { + Layout.fillWidth: true + clip: true + TextArea { + id: addingExternalLinkUrlText + selectByMouse: true + font.pointSize: AdjustedValues.f10 + placeholderText: (quoteValid === false) ? + qsTr("Link card URL, Custom feed URL, List URL, Post URL") : + qsTr("Link card URL") } } - BusyIndicator { - anchors.fill: parent - anchors.margins: 3 - visible: externalLink.running || - feedGeneratorLink.running || - listLink.running - } - states: [ - State { - when: externalLink.valid || - feedGeneratorLink.valid || - listLink.valid || - (postLink.valid && postType !== "quote") - PropertyChanges { - target: externalLinkButton - iconSource: "../images/delete.png" - onClicked: { - externalLink.clear() - feedGeneratorLink.clear() - listLink.clear() - postLink.clear() - if(postType !== "quote"){ - quoteCid = "" - quoteUri = "" - quoteAvatar = "" - quoteDisplayName = "" - quoteHandle = "" - quoteIndexedAt = "" - quoteText = "" - } - } + IconButton { + id: externalLinkButton + iconSource: "../images/add.png" + enabled: addingExternalLinkUrlText.text.length > 0 && + !externalLink.running && + !feedGeneratorLink.running && + !listLink.running && + !createRecord.running + onClicked: { + var uri = addingExternalLinkUrlText.text + var row = accountCombo.currentIndex + if(feedGeneratorLink.checkUri(uri, "feed") && !quoteValid){ + feedGeneratorLink.setAccount(postDialog.accountModel.item(row, AccountListModel.UuidRole)) + feedGeneratorLink.getFeedGenerator(uri) + }else if(listLink.checkUri(uri, "lists") && !quoteValid){ + listLink.setAccount(postDialog.accountModel.item(row, AccountListModel.UuidRole)) + listLink.getList(uri) + }else if(postLink.checkUri(uri, "post") && !quoteValid && postType !== "quote"){ + postLink.setAccount(postDialog.accountModel.item(row, AccountListModel.UuidRole)) + postLink.getPost(uri) + }else{ + externalLink.getExternalLink(uri) + } } - ] - } - } - ExternalLinkCard { - Layout.preferredWidth: postText.width - Layout.maximumHeight: 280 * AdjustedValues.ratio - visible: externalLink.valid - - thumbImage.source: externalLink.thumbLocal - uriLabel.text: externalLink.uri - titleLabel.text: externalLink.title - descriptionLabel.text: externalLink.description - } - FeedGeneratorLinkCard { - Layout.preferredWidth: postText.width - visible: feedGeneratorLink.valid - - avatarImage.source: feedGeneratorLink.avatar - displayNameLabel.text: feedGeneratorLink.displayName - creatorHandleLabel.text: feedGeneratorLink.creatorHandle - likeCountLabel.text: feedGeneratorLink.likeCount - } - ListLinkCard { - Layout.preferredWidth: postText.width - visible: listLink.valid - avatarImage.source: listLink.avatar - displayNameLabel.text: listLink.displayName - creatorHandleLabel.text: listLink.creatorHandle - descriptionLabel.text: listLink.description - } - - ScrollView { - Layout.preferredWidth: postText.width - Layout.preferredHeight: 102 * AdjustedValues.ratio + ScrollBar.horizontal.height + 1 - ScrollBar.horizontal.policy: ScrollBar.AlwaysOn - visible: embedImageListModel.count > 0 - enabled: !createRecord.running - clip: true - RowLayout { - spacing: 4 * AdjustedValues.ratio - Repeater { - model: EmbedImageListModel { - id: embedImageListModel - property int adjustPostLength: count > 4 ? 5 + (Math.ceil(count / 4) + "").length : 0 + BusyIndicator { + anchors.fill: parent + anchors.margins: 3 + visible: externalLink.running || + feedGeneratorLink.running || + listLink.running } - delegate: ImageWithIndicator { - Layout.preferredWidth: 102 * AdjustedValues.ratio - Layout.preferredHeight: 102 * AdjustedValues.ratio - fillMode: Image.PreserveAspectCrop - source: model.uri - TagLabel { - anchors.left: parent.left - anchors.bottom: parent.bottom - anchors.margins: 3 - visible: model.alt.length > 0 - source: "" - fontPointSize: AdjustedValues.f8 - text: "Alt" - } - TagLabel { - anchors.right: parent.right - anchors.bottom: parent.bottom - anchors.margins: 3 - visible: model.number.length > 0 - source: "" - fontPointSize: AdjustedValues.f8 - text: model.number - } - MouseArea { - anchors.fill: parent - onClicked: { - altEditDialog.editingIndex = model.index - altEditDialog.embedImage = model.uri - altEditDialog.embedAlt = model.alt - altEditDialog.open() + states: [ + State { + when: externalLink.valid || + feedGeneratorLink.valid || + listLink.valid || + (postLink.valid && postType !== "quote") + PropertyChanges { + target: externalLinkButton + iconSource: "../images/delete.png" + onClicked: { + externalLink.clear() + feedGeneratorLink.clear() + listLink.clear() + postLink.clear() + if(postType !== "quote"){ + quoteCid = "" + quoteUri = "" + quoteAvatar = "" + quoteDisplayName = "" + quoteHandle = "" + quoteIndexedAt = "" + quoteText = "" + } + } } } - IconButton { - enabled: !createRecord.running - width: AdjustedValues.b24 - height: AdjustedValues.b24 - anchors.top: parent.top - anchors.right: parent.right - anchors.margins: 5 - iconSource: "../images/delete.png" - onClicked: embedImageListModel.remove(model.index) - } - } + ] } } - } - - Frame { - id: quoteFrame - Layout.preferredWidth: postText.width - Layout.maximumHeight: 200 * AdjustedValues.ratio - visible: quoteValid - clip: true - ColumnLayout { + ExternalLinkCard { Layout.preferredWidth: postText.width - RowLayout { - AvatarImage { - id: quoteAvatarImage - Layout.preferredWidth: AdjustedValues.i16 - Layout.preferredHeight: AdjustedValues.i16 - source: quoteAvatar - } - Author { - layoutWidth: postText.width - quoteFrame.padding * 2 - quoteAvatarImage.width - parent.spacing - displayName: quoteDisplayName - handle: quoteHandle - indexedAt: quoteIndexedAt - } - } - Label { - Layout.preferredWidth: postText.width - quoteFrame.padding * 2 - wrapMode: Text.WrapAnywhere - font.pointSize: AdjustedValues.f8 - text: quoteText - } - } - } + Layout.maximumHeight: 280 * AdjustedValues.ratio + visible: externalLink.valid - Label { - Layout.preferredWidth: postText.width - Layout.leftMargin: 10 - Layout.topMargin: 2 - Layout.bottomMargin: 2 - font.pointSize: AdjustedValues.f8 - text: createRecord.progressMessage - visible: createRecord.running && createRecord.progressMessage.length > 0 - color: Material.theme === Material.Dark ? Material.foreground : "white" - Rectangle { - anchors.fill: parent - anchors.leftMargin: -10 - anchors.topMargin: -2 - anchors.bottomMargin: -2 - z: -1 - radius: height / 2 - color: Material.color(Material.Indigo) + thumbImage.source: externalLink.thumbLocal + uriLabel.text: externalLink.uri + titleLabel.text: externalLink.title + descriptionLabel.text: externalLink.description } - } + FeedGeneratorLinkCard { + Layout.preferredWidth: postText.width + visible: feedGeneratorLink.valid - RowLayout { - spacing: 0 - Button { - enabled: !createRecord.running - flat: true - font.pointSize: AdjustedValues.f10 - text: qsTr("Cancel") - onClicked: postDialog.close() + avatarImage.source: feedGeneratorLink.avatar + displayNameLabel.text: feedGeneratorLink.displayName + creatorHandleLabel.text: feedGeneratorLink.creatorHandle + likeCountLabel.text: feedGeneratorLink.likeCount } - Item { - Layout.fillWidth: true - Layout.preferredHeight: 1 + ListLinkCard { + Layout.preferredWidth: postText.width + visible: listLink.valid + avatarImage.source: listLink.avatar + displayNameLabel.text: listLink.displayName + creatorHandleLabel.text: listLink.creatorHandle + descriptionLabel.text: listLink.description } - IconButton { - id: selfLabelsButton + + ScrollView { + Layout.preferredWidth: postText.width + Layout.preferredHeight: 102 * AdjustedValues.ratio + ScrollBar.horizontal.height + 1 + ScrollBar.horizontal.policy: ScrollBar.AlwaysOn + visible: embedImageListModel.count > 0 enabled: !createRecord.running - iconSource: "../images/labeling.png" - iconSize: AdjustedValues.i18 - flat: true - foreground: value.length > 0 ? Material.accent : Material.foreground - onClicked: selfLabelPopup.popup() - property string value: "" - SelfLabelPopup { - id: selfLabelPopup - onTriggered: (value, text) => { - if(value.length > 0){ - selfLabelsButton.value = value - selfLabelsButton.iconText = text - }else{ - selfLabelsButton.value = "" - selfLabelsButton.iconText = "" - } - } - onClosed: postText.forceActiveFocus() + clip: true + RowLayout { + spacing: 4 * AdjustedValues.ratio + Repeater { + model: EmbedImageListModel { + id: embedImageListModel + property int adjustPostLength: count > 4 ? 5 + (Math.ceil(count / 4) + "").length : 0 + } + delegate: ImageWithIndicator { + Layout.preferredWidth: 102 * AdjustedValues.ratio + Layout.preferredHeight: 102 * AdjustedValues.ratio + fillMode: Image.PreserveAspectCrop + source: model.uri + TagLabel { + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.margins: 3 + visible: model.alt.length > 0 + source: "" + fontPointSize: AdjustedValues.f8 + text: "Alt" + } + TagLabel { + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: 3 + visible: model.number.length > 0 + source: "" + fontPointSize: AdjustedValues.f8 + text: model.number + } + MouseArea { + anchors.fill: parent + onClicked: { + altEditDialog.editingIndex = model.index + altEditDialog.embedImage = model.uri + altEditDialog.embedAlt = model.alt + altEditDialog.open() + } + } + IconButton { + enabled: !createRecord.running + width: AdjustedValues.b24 + height: AdjustedValues.b24 + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: 5 + iconSource: "../images/delete.png" + onClicked: embedImageListModel.remove(model.index) + } + } + } } } - IconButton { - enabled: !createRecord.running && - !externalLink.valid && - !feedGeneratorLink.valid && - !listLink.valid && - !embedImageListModel.running - iconSource: "../images/add_image.png" - iconSize: AdjustedValues.i18 - flat: true - onClicked: { - if(fileDialog.prevFolder.length > 0){ - fileDialog.folder = fileDialog.prevFolder + + Frame { + id: quoteFrame + Layout.preferredWidth: postText.width + Layout.maximumHeight: 200 * AdjustedValues.ratio + visible: quoteValid + clip: true + ColumnLayout { + Layout.preferredWidth: postText.width + RowLayout { + AvatarImage { + id: quoteAvatarImage + Layout.preferredWidth: AdjustedValues.i16 + Layout.preferredHeight: AdjustedValues.i16 + source: quoteAvatar + } + Author { + layoutWidth: postText.width - quoteFrame.padding * 2 - quoteAvatarImage.width - parent.spacing + displayName: quoteDisplayName + handle: quoteHandle + indexedAt: quoteIndexedAt + } + } + Label { + Layout.preferredWidth: postText.width - quoteFrame.padding * 2 + wrapMode: Text.WrapAnywhere + font.pointSize: AdjustedValues.f8 + text: quoteText } - fileDialog.open() } } Label { - Layout.leftMargin: 5 - Layout.alignment: Qt.AlignVCenter + Layout.preferredWidth: postText.width + Layout.leftMargin: 10 + Layout.topMargin: 2 + Layout.bottomMargin: 2 font.pointSize: AdjustedValues.f8 - text: 300 - embedImageListModel.adjustPostLength - postText.realTextLength - } - ProgressCircle { - Layout.leftMargin: 5 - Layout.preferredWidth: AdjustedValues.i24 - Layout.preferredHeight: AdjustedValues.i24 - Layout.alignment: Qt.AlignVCenter - from: 0 - to: 300 - embedImageListModel.adjustPostLength - value: postText.realTextLength + text: createRecord.progressMessage + visible: createRecord.running && createRecord.progressMessage.length > 0 + color: Material.theme === Material.Dark ? Material.foreground : "white" + Rectangle { + anchors.fill: parent + anchors.leftMargin: -10 + anchors.topMargin: -2 + anchors.bottomMargin: -2 + z: -1 + radius: height / 2 + color: Material.color(Material.Indigo) + } } - Button { - id: postButton - Layout.alignment: Qt.AlignRight - enabled: postText.text.length > 0 && - postText.realTextLength <= (300 - embedImageListModel.adjustPostLength) && - !createRecord.running && - !externalLink.running && - !feedGeneratorLink.running && - !listLink.running - font.pointSize: AdjustedValues.f10 - text: qsTr("Post") - onClicked: { - var row = accountCombo.currentIndex; - createRecord.setAccount(postDialog.accountModel.item(row, AccountListModel.ServiceRole), - postDialog.accountModel.item(row, AccountListModel.DidRole), - postDialog.accountModel.item(row, AccountListModel.HandleRole), - postDialog.accountModel.item(row, AccountListModel.EmailRole), - postDialog.accountModel.item(row, AccountListModel.AccessJwtRole), - postDialog.accountModel.item(row, AccountListModel.RefreshJwtRole)) - createRecord.clear() - createRecord.setText(postText.text) - createRecord.setPostLanguages(postDialog.accountModel.item(row, AccountListModel.PostLanguagesRole)) - if(postType !== "reply"){ - // replyのときは制限の設定はできない - createRecord.setThreadGate(selectThreadGateDialog.selectedType, selectThreadGateDialog.selectedOptions) - createRecord.setPostGate(selectThreadGateDialog.selectedQuoteEnabled, []) - } - if(postType === "reply"){ - createRecord.setReply(replyCid, replyUri, replyRootCid, replyRootUri) + + RowLayout { + spacing: 0 + Button { + enabled: !createRecord.running + flat: true + font.pointSize: AdjustedValues.f10 + text: qsTr("Cancel") + onClicked: postDialog.close() + } + Item { + Layout.fillWidth: true + Layout.preferredHeight: 1 + } + IconButton { + id: selfLabelsButton + enabled: !createRecord.running + iconSource: "../images/labeling.png" + iconSize: AdjustedValues.i18 + flat: true + foreground: value.length > 0 ? Material.accent : Material.foreground + onClicked: selfLabelPopup.popup() + property string value: "" + SelfLabelPopup { + id: selfLabelPopup + onTriggered: (value, text) => { + if(value.length > 0){ + selfLabelsButton.value = value + selfLabelsButton.iconText = text + }else{ + selfLabelsButton.value = "" + selfLabelsButton.iconText = "" + } + } + onClosed: postText.forceActiveFocus() } - if(quoteValid){ - createRecord.setQuote(quoteCid, quoteUri) + } + IconButton { + enabled: !createRecord.running && + !externalLink.valid && + !feedGeneratorLink.valid && + !listLink.valid && + !embedImageListModel.running + iconSource: "../images/add_image.png" + iconSize: AdjustedValues.i18 + flat: true + onClicked: { + if(fileDialog.prevFolder.length > 0){ + fileDialog.folder = fileDialog.prevFolder + } + fileDialog.open() } - if(selfLabelsButton.value.length > 0){ - createRecord.setSelfLabels([selfLabelsButton.value]) + } + + Label { + Layout.leftMargin: 5 + Layout.alignment: Qt.AlignVCenter + font.pointSize: AdjustedValues.f8 + text: 300 - embedImageListModel.adjustPostLength - postText.realTextLength + } + ProgressCircle { + Layout.leftMargin: 5 + Layout.preferredWidth: AdjustedValues.i24 + Layout.preferredHeight: AdjustedValues.i24 + Layout.alignment: Qt.AlignVCenter + from: 0 + to: 300 - embedImageListModel.adjustPostLength + value: postText.realTextLength + } + Button { + id: postButton + Layout.alignment: Qt.AlignRight + enabled: postText.text.length > 0 && + postText.realTextLength <= (300 - embedImageListModel.adjustPostLength) && + !createRecord.running && + !externalLink.running && + !feedGeneratorLink.running && + !listLink.running + font.pointSize: AdjustedValues.f10 + text: qsTr("Post") + onClicked: { + var row = accountCombo.currentIndex; + createRecord.setAccount(postDialog.accountModel.item(row, AccountListModel.UuidRole)) + createRecord.clear() + createRecord.setText(postText.text) + createRecord.setPostLanguages(postDialog.accountModel.item(row, AccountListModel.PostLanguagesRole)) + if(postType !== "reply"){ + // replyのときは制限の設定はできない + createRecord.setThreadGate(selectThreadGateDialog.selectedType, selectThreadGateDialog.selectedOptions) + createRecord.setPostGate(selectThreadGateDialog.selectedQuoteEnabled, []) + } + if(postType === "reply"){ + createRecord.setReply(replyCid, replyUri, replyRootCid, replyRootUri) + } + if(quoteValid){ + createRecord.setQuote(quoteCid, quoteUri) + } + if(selfLabelsButton.value.length > 0){ + createRecord.setSelfLabels([selfLabelsButton.value]) + } + if(externalLink.valid){ + createRecord.setExternalLink(externalLink.uri, externalLink.title, externalLink.description, externalLink.thumbLocal) + createRecord.postWithImages() + }else if(feedGeneratorLink.valid){ + createRecord.setFeedGeneratorLink(feedGeneratorLink.uri, feedGeneratorLink.cid) + createRecord.post() + }else if(listLink.valid){ + createRecord.setFeedGeneratorLink(listLink.uri, listLink.cid) + createRecord.post() + }else if(embedImageListModel.count > 0){ + createRecord.setImages(embedImageListModel.uris(), embedImageListModel.alts()) + createRecord.postWithImages() + }else{ + createRecord.post() + } } - if(externalLink.valid){ - createRecord.setExternalLink(externalLink.uri, externalLink.title, externalLink.description, externalLink.thumbLocal) - createRecord.postWithImages() - }else if(feedGeneratorLink.valid){ - createRecord.setFeedGeneratorLink(feedGeneratorLink.uri, feedGeneratorLink.cid) - createRecord.post() - }else if(listLink.valid){ - createRecord.setFeedGeneratorLink(listLink.uri, listLink.cid) - createRecord.post() - }else if(embedImageListModel.count > 0){ - createRecord.setImages(embedImageListModel.uris(), embedImageListModel.alts()) - createRecord.postWithImages() - }else{ - createRecord.post() + BusyIndicator { + anchors.fill: parent + anchors.margins: 3 + visible: createRecord.running } } - BusyIndicator { - anchors.fill: parent - anchors.margins: 3 - visible: createRecord.running - } } } + DragAndDropArea { + anchors.fill: parent + anchors.margins: -5 + onDropped: (urls) => embedImageListModel.append(urls) + } } } diff --git a/app/qml/dialogs/ReportAccountDialog.qml b/app/qml/dialogs/ReportAccountDialog.qml index 5598ed48..f1531dab 100644 --- a/app/qml/dialogs/ReportAccountDialog.qml +++ b/app/qml/dialogs/ReportAccountDialog.qml @@ -135,8 +135,7 @@ Dialog { font.pointSize: AdjustedValues.f10 text: qsTr("Send report") onClicked: { - reporter.setAccount(account.service, account.did, account.handle, - account.email, account.accessJwt, account.refreshJwt) + reporter.setAccount(account.uuid) reporter.reportAccount(targetDid, reportTextArea.text, [labelerDidComboBox.currentValue], reportTypeButtonGroup.checkedButton.reason) diff --git a/app/qml/dialogs/ReportMessageDialog.qml b/app/qml/dialogs/ReportMessageDialog.qml index d0b80332..15e3be4f 100644 --- a/app/qml/dialogs/ReportMessageDialog.qml +++ b/app/qml/dialogs/ReportMessageDialog.qml @@ -145,8 +145,7 @@ Dialog { font.pointSize: AdjustedValues.f10 text: qsTr("Send report") onClicked: { - reporter.setAccount(account.service, account.did, account.handle, - account.email, account.accessJwt, account.refreshJwt) + reporter.setAccount(account.uuid) reporter.reportMessage(targetAccountDid, targetConvoId, targetMessageId, reportTextArea.text, reportTypeButtonGroup.checkedButton.reason) } diff --git a/app/qml/dialogs/ReportPostDialog.qml b/app/qml/dialogs/ReportPostDialog.qml index 3ea96db6..2087c54b 100644 --- a/app/qml/dialogs/ReportPostDialog.qml +++ b/app/qml/dialogs/ReportPostDialog.qml @@ -165,8 +165,7 @@ Dialog { font.pointSize: AdjustedValues.f10 text: qsTr("Send report") onClicked: { - reporter.setAccount(account.service, account.did, account.handle, - account.email, account.accessJwt, account.refreshJwt) + reporter.setAccount(account.uuid) reporter.reportPost(targetUri, targetCid, reportTextArea.text, [labelerDidComboBox.currentValue], reportTypeButtonGroup.checkedButton.reason) diff --git a/app/qml/dialogs/SelectThreadGateDialog.qml b/app/qml/dialogs/SelectThreadGateDialog.qml index ae6e39f4..7f1543c9 100644 --- a/app/qml/dialogs/SelectThreadGateDialog.qml +++ b/app/qml/dialogs/SelectThreadGateDialog.qml @@ -55,8 +55,7 @@ Dialog { } } listsListModel.clear() - listsListModel.setAccount(account.service, account.did, account.handle, - account.email, account.accessJwt, account.refreshJwt) + listsListModel.setAccount(account.uuid) listsListModel.getLatest() } onClosed: { diff --git a/app/qml/main.qml b/app/qml/main.qml index 9daede1b..24876328 100644 --- a/app/qml/main.qml +++ b/app/qml/main.qml @@ -307,12 +307,7 @@ ApplicationWindow { property string updateSequence: "" // threadgate, postgate onOpened: { selectThreadGateDialog.ready = false - postDialog.recordOperator.setAccount(selectThreadGateDialog.account.service, - selectThreadGateDialog.account.did, - selectThreadGateDialog.account.handle, - selectThreadGateDialog.account.email, - selectThreadGateDialog.account.accessJwt, - selectThreadGateDialog.account.refreshJwt) + postDialog.recordOperator.setAccount(selectThreadGateDialog.account.uuid) postDialog.recordOperator.clear() postDialog.recordOperator.requestPostGate(postUri) } @@ -347,12 +342,7 @@ ApplicationWindow { updateSequence = "threadgate" } - postDialog.recordOperator.setAccount(selectThreadGateDialog.account.service, - selectThreadGateDialog.account.did, - selectThreadGateDialog.account.handle, - selectThreadGateDialog.account.email, - selectThreadGateDialog.account.accessJwt, - selectThreadGateDialog.account.refreshJwt) + postDialog.recordOperator.setAccount(selectThreadGateDialog.account.uuid) postDialog.recordOperator.clear() if(updateSequence === "threadgate"){ console.log("Update threadgate") @@ -467,17 +457,12 @@ ApplicationWindow { // アカウント管理で内容が変更されたときにカラムとインデックスの関係が崩れるのでuuidで確認する AccountListModel { id: accountListModel - onUpdatedSession: (row, uuid) => { - console.log("onUpdatedSession:" + row + ", " + uuid) - } - onUpdatedAccount: (row, uuid) => { - console.log("onUpdatedAccount:" + row + ", " + uuid) - // カラムを更新しにいく - repeater.updateAccount(uuid) - } onFinished: { console.log("onFinished:" + allAccountsReady + ", count=" + columnManageModel.rowCount()) - if(rowCount() === 0){ + globalProgressFrame.text = "" + if(accountDialog.visible === true){ + // ダイアログが開いているときはアカウント追加のたびに呼ばれるので何もしない + }else if(rowCount() === 0){ accountDialog.open() }else if(columnManageModel.rowCount() === 0){ if(allAccountsReady){ @@ -538,13 +523,7 @@ ApplicationWindow { currentAccountIndex -= 1 load(true) }else{ - setAccount(accountListModel.item(currentAccountIndex, AccountListModel.ServiceRole), - accountListModel.item(currentAccountIndex, AccountListModel.DidRole), - handle, - "email", - accessJwt, - accountListModel.item(currentAccountIndex, AccountListModel.RefreshJwtRole) - ) + setAccount(accountListModel.item(currentAccountIndex, AccountListModel.UuidRole)) actor = did searchTarget = "#cache" if(listsListModel.getLatest()){ @@ -894,18 +873,6 @@ ApplicationWindow { scrollView.contentWidth = max_w } - function updateAccount(account_uuid){ - for(var i=0; i 0 RowLayout { + id: contentRootLayout AvatarImage { id: memberAvatarsImage Layout.preferredWidth: AdjustedValues.i24 diff --git a/app/qml/view/ColumnView.qml b/app/qml/view/ColumnView.qml index a90dacce..f5b4d84e 100644 --- a/app/qml/view/ColumnView.qml +++ b/app/qml/view/ColumnView.qml @@ -674,24 +674,12 @@ ColumnLayout { } } - function reflect(){ - // StackViewに積まれているViewに反映 - for(var i=0; i { if(success){ listDetailView.back() @@ -50,10 +49,9 @@ ColumnLayout { function rowCount() { return listItemListModel.rowCount(); } - function setAccount(service, did, handle, email, accessJwt, refreshJwt) { - listItemListModel.setAccount(service, did, handle, email, accessJwt, refreshJwt) - recordOperator.setAccount(service, did, handle, email, accessJwt, refreshJwt) - recordOperator.accountHandle = handle + function setAccount(uuid) { + listItemListModel.setAccount(uuid) + recordOperator.setAccount(uuid) } function getLatest() { listItemListModel.getLatest() @@ -232,7 +230,7 @@ ColumnLayout { id: listItemListModel autoLoading: false uri: listDetailView.listUri - property bool mine: (creatorHandle === recordOperator.accountHandle) && recordOperator.accountHandle.length > 0 + property bool mine: (creatorHandle === recordOperator.handle) && recordOperator.handle.length > 0 onErrorOccured: (code, message) => listDetailView.errorOccured(code, message) } @@ -261,6 +259,8 @@ ColumnLayout { delegate: ClickableFrame { id: listItemLayout + contentWidth: contentRootLayout.implicitWidth + contentHeight: contentRootLayout.implicitHeight clip: true style: "Post" topPadding: 10 @@ -273,6 +273,7 @@ ColumnLayout { RowLayout{ + id: contentRootLayout AvatarImage { id: postAvatarImage Layout.preferredWidth: AdjustedValues.i36 diff --git a/app/qml/view/ListsListView.qml b/app/qml/view/ListsListView.qml index a6fd809d..eb2cedd0 100644 --- a/app/qml/view/ListsListView.qml +++ b/app/qml/view/ListsListView.qml @@ -64,6 +64,8 @@ ScrollView { delegate: ClickableFrame { id: listsLayout + contentWidth: contentRootLayout.implicitWidth + contentHeight: contentRootLayout.implicitHeight clip: true style: "Post" topPadding: 10 @@ -111,6 +113,7 @@ ScrollView { ] RowLayout{ + id: contentRootLayout spacing: 10 AvatarImage { id: postAvatarImage diff --git a/app/qml/view/NotificationListView.qml b/app/qml/view/NotificationListView.qml index 7fb86e92..2fcb1086 100644 --- a/app/qml/view/NotificationListView.qml +++ b/app/qml/view/NotificationListView.qml @@ -90,6 +90,14 @@ ScrollView { userFilterMatched: model.userFilterMatched userFilterMessage: model.userFilterMessage + contentFilterFrame.visible: model.contentFilterMatched + contentMediaFilterFrame.visible: model.contentMediaFilterMatched + postImagePreview.visible: contentMediaFilterFrame.showContent && model.embedImages.length > 0 + + externalLinkFrame.visible: model.hasExternalLink && contentMediaFilterFrame.showContent + feedGeneratorFrame.visible: model.hasFeedGenerator && contentMediaFilterFrame.showContent + listLinkCardFrame.visible: model.hasListLink && contentMediaFilterFrame.showContent + reason: model.reason postAvatarImage.source: model.avatar postAvatarImage.onClicked: requestViewProfile(model.did) @@ -109,11 +117,8 @@ ScrollView { } return text } - contentFilterFrame.visible: model.contentFilterMatched contentFilterFrame.labelText: model.contentFilterMessage - contentMediaFilterFrame.visible: model.contentMediaFilterMatched contentMediaFilterFrame.labelText: model.contentMediaFilterMessage - postImagePreview.visible: contentMediaFilterFrame.showContent && model.embedImages.length > 0 postImagePreview.layoutType: notificationListView.imageLayoutType postImagePreview.embedImages: model.embedImages postImagePreview.embedAlts: model.embedImagesAlt @@ -138,21 +143,18 @@ ScrollView { embedVideoFrame.onClicked: Qt.openUrlExternally(rootListView.model.getItemOfficialUrl(model.index)) embedVideoFrame.thumbImageSource: model.videoThumbUri - externalLinkFrame.visible: model.hasExternalLink && contentMediaFilterFrame.showContent externalLinkFrame.onClicked: Qt.openUrlExternally(model.externalLinkUri) externalLinkFrame.thumbImage.source: model.externalLinkThumb externalLinkFrame.titleLabel.text: model.externalLinkTitle externalLinkFrame.uriLabel.text: model.externalLinkUri externalLinkFrame.descriptionLabel.text: model.externalLinkDescription - feedGeneratorFrame.visible: model.hasFeedGenerator && contentMediaFilterFrame.showContent feedGeneratorFrame.onClicked: requestViewFeedGenerator(model.feedGeneratorDisplayName, model.feedGeneratorUri) feedGeneratorFrame.avatarImage.source: model.feedGeneratorAvatar feedGeneratorFrame.displayNameLabel.text: model.feedGeneratorDisplayName feedGeneratorFrame.creatorHandleLabel.text: model.feedGeneratorCreatorHandle feedGeneratorFrame.likeCountLabel.text: model.feedGeneratorLikeCount - listLinkCardFrame.visible: model.hasListLink && contentMediaFilterFrame.showContent listLinkCardFrame.onClicked: requestViewListFeed(model.listLinkUri, model.listLinkDisplayName) listLinkCardFrame.avatarImage.source: model.listLinkAvatar listLinkCardFrame.displayNameLabel.text: model.listLinkDisplayName diff --git a/app/qml/view/ProfileListView.qml b/app/qml/view/ProfileListView.qml index 5faaffd1..d4e497db 100644 --- a/app/qml/view/ProfileListView.qml +++ b/app/qml/view/ProfileListView.qml @@ -47,9 +47,7 @@ ScrollView { } } function reflectAccount() { - recordOperator.setAccount(rootListView.model.service, rootListView.model.did, - rootListView.model.handle, rootListView.model.email, - rootListView.model.accessJwt, rootListView.model.refreshJwt) + recordOperator.setAccount(rootListView.model.uuid) } } @@ -87,6 +85,8 @@ ScrollView { delegate: ClickableFrame { id: profileLayout + contentWidth: contentRootLayout.implicitWidth + contentHeight: contentRootLayout.implicitHeight clip: true style: "Post" topPadding: 10 @@ -169,6 +169,7 @@ ScrollView { ] RowLayout{ + id: contentRootLayout AvatarImage { id: postAvatarImage Layout.preferredWidth: AdjustedValues.i36 diff --git a/app/qml/view/ProfileView.qml b/app/qml/view/ProfileView.qml index 53515914..83171268 100644 --- a/app/qml/view/ProfileView.qml +++ b/app/qml/view/ProfileView.qml @@ -120,19 +120,19 @@ ColumnLayout { function rowCount() { return userProfile.handle.length; } - function setAccount(service, did, handle, email, accessJwt, refreshJwt) { - accountDid = did - recordOperator.setAccount(service, did, handle, email, accessJwt, refreshJwt) - userProfile.setAccount(service, did, handle, email, accessJwt, refreshJwt) - authorFeedListModel.setAccount(service, did, handle, email, accessJwt, refreshJwt) - authorBlogEntryListModel.setAccount(service, did, handle, email, accessJwt, refreshJwt) - repostFeedListModel.setAccount(service, did, handle, email, accessJwt, refreshJwt) - likesFeedListModel.setAccount(service, did, handle, email, accessJwt, refreshJwt) - authorMediaFeedListModel.setAccount(service, did, handle, email, accessJwt, refreshJwt) - actorFeedGeneratorListModel.setAccount(service, did, handle, email, accessJwt, refreshJwt) - listsListModel.setAccount(service, did, handle, email, accessJwt, refreshJwt) - followsListModel.setAccount(service, did, handle, email, accessJwt, refreshJwt) - followersListModel.setAccount(service, did, handle, email, accessJwt, refreshJwt) + function setAccount(uuid) { + recordOperator.setAccount(uuid) + userProfile.setAccount(uuid) + authorFeedListModel.setAccount(uuid) + authorBlogEntryListModel.setAccount(uuid) + repostFeedListModel.setAccount(uuid) + likesFeedListModel.setAccount(uuid) + authorMediaFeedListModel.setAccount(uuid) + actorFeedGeneratorListModel.setAccount(uuid) + listsListModel.setAccount(uuid) + followsListModel.setAccount(uuid) + followersListModel.setAccount(uuid) + accountDid = authorFeedListModel.did } function getLatest() { userProfile.getProfile(userDid) @@ -387,7 +387,7 @@ ColumnLayout { onHoveredLinkChanged: profileView.hoveredLink = hoveredLink onLinkActivated: (url) => Qt.openUrlExternally(url) - onContentHeightChanged: Layout.preferredHeight = contentHeight + onContentHeightChanged: Layout.preferredHeight = descriptionLabel.contentHeight Behavior on Layout.preferredHeight { NumberAnimation { duration: 500 } } @@ -488,6 +488,7 @@ ColumnLayout { } } } + IconLabelFrame { id: moderationFrame Layout.preferredWidth: profileView.width @@ -578,6 +579,7 @@ ColumnLayout { } blogModel: BlogEntryListModel { id: authorBlogEntryListModel + targetHandle: userProfile.handle targetDid: profileView.userDid targetServiceEndpoint: userProfile.serviceEndpoint onTargetServiceEndpointChanged: { diff --git a/app/qml/view/SuggestionProfileListView.qml b/app/qml/view/SuggestionProfileListView.qml index 45dccd04..d4a43eb6 100644 --- a/app/qml/view/SuggestionProfileListView.qml +++ b/app/qml/view/SuggestionProfileListView.qml @@ -17,6 +17,8 @@ ListView { delegate: ClickableFrame { id: profileLayout + contentWidth: contentRootLayout.implicitWidth + contentHeight: contentRootLayout.implicitHeight clip: true style: "Post" topPadding: 10 @@ -31,6 +33,7 @@ ListView { onClicked: suggestionProfileListView.selectedProfile(model.did) RowLayout{ + id: contentRootLayout AvatarImage { id: postAvatarImage Layout.preferredWidth: AdjustedValues.i36 diff --git a/app/qml/view/TimelineView.qml b/app/qml/view/TimelineView.qml index f7b21c41..ddd3ef89 100644 --- a/app/qml/view/TimelineView.qml +++ b/app/qml/view/TimelineView.qml @@ -101,6 +101,7 @@ ScrollView { delegate: PostDelegate { Layout.preferredWidth: rootListView.width + layoutWidth: rootListView.width logMode: timelineView.logMode @@ -110,17 +111,31 @@ ScrollView { onRequestAddMutedWord: (text) => timelineView.requestAddMutedWord(text) onRequestCopyTagToClipboard: (text) => systemTool.copyToClipboard(text) + moderationFrame.visible: model.muted userFilterMatched: model.userFilterMatched userFilterMessage: model.userFilterMessage repostReactionAuthor.visible: model.isRepostedBy + replyReactionAuthor.visible: model.hasReply + pinnedIndicatorLabel.visible: (model.pinned && model.index === 0) + + contentFilterFrame.visible: model.contentFilterMatched + contentMediaFilterFrame.visible: model.contentMediaFilterMatched + postImagePreview.visible: contentMediaFilterFrame.showContent && model.embedImages.length > 0 + + quoteFilterFrame.visible: model.quoteFilterMatched && !model.quoteRecordBlocked + blockedQuoteFrame.visible: model.quoteRecordBlocked + + externalLinkFrame.visible: model.hasExternalLink && contentMediaFilterFrame.showContent + feedGeneratorFrame.visible: model.hasFeedGenerator && contentMediaFilterFrame.showContent + listLinkCardFrame.visible: model.hasListLink && contentMediaFilterFrame.showContent + + repostReactionAuthor.displayName: model.repostedByDisplayName repostReactionAuthor.handle: model.repostedByHandle - replyReactionAuthor.visible: model.hasReply replyReactionAuthor.displayName: model.replyParentDisplayName replyReactionAuthor.handle: model.replyParentHandle - pinnedIndicatorLabel.visible: (model.pinned && model.index === 0) postAvatarImage.source: model.avatar postAvatarImage.onClicked: requestViewProfile(model.did) @@ -134,20 +149,15 @@ ScrollView { } return text } - contentFilterFrame.visible: model.contentFilterMatched contentFilterFrame.labelText: model.contentFilterMessage - contentMediaFilterFrame.visible: model.contentMediaFilterMatched contentMediaFilterFrame.labelText: model.contentMediaFilterMessage - postImagePreview.visible: contentMediaFilterFrame.showContent && model.embedImages.length > 0 postImagePreview.layoutType: timelineView.imageLayoutType postImagePreview.embedImages: model.embedImages postImagePreview.embedAlts: model.embedImagesAlt postImagePreview.embedImageRatios: model.embedImagesRatio postImagePreview.onRequestViewImages: (index) => requestViewImages(index, model.embedImagesFull, model.embedImagesAlt) - quoteFilterFrame.visible: model.quoteFilterMatched && !model.quoteRecordBlocked quoteFilterFrame.labelText: qsTr("Quoted content warning") - blockedQuoteFrame.visible: model.quoteRecordBlocked blockedQuoteFrameLabel.text: model.quoteRecordBlockedStatus hasQuote: model.hasQuoteRecord && !model.quoteRecordBlocked quoteRecordFrame.onClicked: (mouse) => { @@ -171,21 +181,18 @@ ScrollView { embedVideoFrame.onClicked: Qt.openUrlExternally(rootListView.model.getItemOfficialUrl(model.index)) embedVideoFrame.thumbImageSource: model.videoThumbUri - externalLinkFrame.visible: model.hasExternalLink && contentMediaFilterFrame.showContent externalLinkFrame.onClicked: Qt.openUrlExternally(model.externalLinkUri) externalLinkFrame.thumbImage.source: model.externalLinkThumb externalLinkFrame.titleLabel.text: model.externalLinkTitle externalLinkFrame.uriLabel.text: model.externalLinkUri externalLinkFrame.descriptionLabel.text: model.externalLinkDescription - feedGeneratorFrame.visible: model.hasFeedGenerator && contentMediaFilterFrame.showContent feedGeneratorFrame.onClicked: requestViewFeedGenerator(model.feedGeneratorDisplayName, model.feedGeneratorUri) feedGeneratorFrame.avatarImage.source: model.feedGeneratorAvatar feedGeneratorFrame.displayNameLabel.text: model.feedGeneratorDisplayName feedGeneratorFrame.creatorHandleLabel.text: model.feedGeneratorCreatorHandle feedGeneratorFrame.likeCountLabel.text: model.feedGeneratorLikeCount - listLinkCardFrame.visible: model.hasListLink && contentMediaFilterFrame.showContent listLinkCardFrame.onClicked: requestViewListFeed(model.listLinkUri, model.listLinkDisplayName) listLinkCardFrame.avatarImage.source: model.listLinkAvatar listLinkCardFrame.displayNameLabel.text: model.listLinkDisplayName diff --git a/app/qtquick/account/accountlistmodel.cpp b/app/qtquick/account/accountlistmodel.cpp index ce2d06cd..9bacb9dd 100644 --- a/app/qtquick/account/accountlistmodel.cpp +++ b/app/qtquick/account/accountlistmodel.cpp @@ -1,11 +1,8 @@ #include "accountlistmodel.h" -#include "common.h" #include "extension/com/atproto/server/comatprotoservercreatesessionex.h" #include "extension/com/atproto/server/comatprotoserverrefreshsessionex.h" #include "extension/com/atproto/repo/comatprotorepogetrecordex.h" #include "atprotocol/app/bsky/actor/appbskyactorgetprofile.h" -#include "atprotocol/lexicons_func_unknown.h" -#include "tools/pinnedpostcache.h" #include "extension/directory/plc/directoryplc.h" #include @@ -24,21 +21,72 @@ using AtProtocolInterface::ComAtprotoServerCreateSessionEx; using AtProtocolInterface::ComAtprotoServerRefreshSessionEx; using AtProtocolInterface::DirectoryPlc; -AccountListModel::AccountListModel(QObject *parent) - : QAbstractListModel { parent }, m_allAccountsReady(false) +AccountListModel::AccountListModel(QObject *parent) : QAbstractListModel { parent } { + m_roleTo[UuidRole] = AccountManager::AccountManagerRoles::UuidRole; + m_roleTo[IsMainRole] = AccountManager::AccountManagerRoles::IsMainRole; + m_roleTo[ServiceRole] = AccountManager::AccountManagerRoles::ServiceRole; + m_roleTo[ServiceEndpointRole] = AccountManager::AccountManagerRoles::ServiceEndpointRole; + m_roleTo[IdentifierRole] = AccountManager::AccountManagerRoles::IdentifierRole; + m_roleTo[PasswordRole] = AccountManager::AccountManagerRoles::PasswordRole; + m_roleTo[DidRole] = AccountManager::AccountManagerRoles::DidRole; + m_roleTo[HandleRole] = AccountManager::AccountManagerRoles::HandleRole; + m_roleTo[EmailRole] = AccountManager::AccountManagerRoles::EmailRole; + m_roleTo[AccessJwtRole] = AccountManager::AccountManagerRoles::AccessJwtRole; + m_roleTo[RefreshJwtRole] = AccountManager::AccountManagerRoles::RefreshJwtRole; + m_roleTo[DisplayNameRole] = AccountManager::AccountManagerRoles::DisplayNameRole; + m_roleTo[DescriptionRole] = AccountManager::AccountManagerRoles::DescriptionRole; + m_roleTo[AvatarRole] = AccountManager::AccountManagerRoles::AvatarRole; + m_roleTo[PostLanguagesRole] = AccountManager::AccountManagerRoles::PostLanguagesRole; + m_roleTo[ThreadGateTypeRole] = AccountManager::AccountManagerRoles::ThreadGateTypeRole; + m_roleTo[ThreadGateOptionsRole] = AccountManager::AccountManagerRoles::ThreadGateOptionsRole; + m_roleTo[PostGateQuoteEnabledRole] = + AccountManager::AccountManagerRoles::PostGateQuoteEnabledRole; + m_roleTo[StatusRole] = AccountManager::AccountManagerRoles::StatusRole; + m_roleTo[AuthorizedRole] = AccountManager::AccountManagerRoles::AuthorizedRole; + connect(&m_timer, &QTimer::timeout, [=]() { - for (int row = 0; row < m_accountList.count(); row++) { + AccountManager *manager = AccountManager::getInstance(); + for (int row = 0; row < manager->count(); row++) { refreshSession(row); } }); m_timer.start(60 * 60 * 1000); + + AccountManager *manager = AccountManager::getInstance(); + + connect(manager, &AccountManager::errorOccured, this, &AccountListModel::errorOccured); + connect(manager, &AccountManager::updatedAccount, this, &AccountListModel::updatedAccount); + connect(manager, &AccountManager::countChanged, this, &AccountListModel::countChanged); + connect(manager, &AccountManager::finished, this, &AccountListModel::finished); + connect(manager, &AccountManager::allAccountsReadyChanged, this, + &AccountListModel::allAccountsReadyChanged); + + connect(this, &AccountListModel::updatedAccount, this, [=](const QString &uuid) { + int row = manager->indexAt(uuid); + qDebug().noquote() << "AccountListModel::updatedAccount:" << uuid << row; + if (row >= 0 && row < count()) { + emit dataChanged(index(row), index(row)); + } + }); +} + +AccountListModel::~AccountListModel() +{ + AccountManager *manager = AccountManager::getInstance(); + + disconnect(manager, &AccountManager::errorOccured, this, &AccountListModel::errorOccured); + disconnect(manager, &AccountManager::updatedAccount, this, &AccountListModel::updatedAccount); + disconnect(manager, &AccountManager::countChanged, this, &AccountListModel::countChanged); + disconnect(manager, &AccountManager::finished, this, &AccountListModel::finished); + disconnect(manager, &AccountManager::allAccountsReadyChanged, this, + &AccountListModel::allAccountsReadyChanged); } int AccountListModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent) - return m_accountList.count(); + return count(); } QVariant AccountListModel::data(const QModelIndex &index, int role) const @@ -48,101 +96,66 @@ QVariant AccountListModel::data(const QModelIndex &index, int role) const QVariant AccountListModel::item(int row, AccountListModelRoles role) const { - if (row < 0 || row >= m_accountList.count()) + if (row < 0 || row >= count()) return QVariant(); + AccountManager *manager = AccountManager::getInstance(); + const AccountData account = manager->getAccount(manager->getUuid(row)); + if (role == UuidRole) - return m_accountList.at(row).uuid; + return account.uuid; else if (role == IsMainRole) - return m_accountList.at(row).is_main; + return account.is_main; else if (role == ServiceRole) - return m_accountList.at(row).service; + return account.service; else if (role == ServiceEndpointRole) - return m_accountList.at(row).service_endpoint; + return account.service_endpoint; else if (role == IdentifierRole) - return m_accountList.at(row).identifier; + return account.identifier; else if (role == PasswordRole) - return m_accountList.at(row).password; + return account.password; else if (role == DidRole) - return m_accountList.at(row).did; + return account.did; else if (role == HandleRole) - return m_accountList.at(row).handle; + return account.handle; else if (role == EmailRole) - return m_accountList.at(row).email; + return account.email; else if (role == AccessJwtRole) - return m_accountList.at(row).status == AccountStatus::Authorized - ? m_accountList.at(row).accessJwt - : QString(); + return account.status == AccountStatus::Authorized ? account.accessJwt : QString(); else if (role == RefreshJwtRole) - return m_accountList.at(row).refreshJwt; + return account.refreshJwt; else if (role == DisplayNameRole) - return m_accountList.at(row).displayName; + return account.displayName; else if (role == DescriptionRole) - return m_accountList.at(row).description; + return account.description; else if (role == AvatarRole) - return m_accountList.at(row).avatar; + return account.avatar; else if (role == PostLanguagesRole) - return m_accountList[row].post_languages; + return account.post_languages; else if (role == ThreadGateTypeRole) - return m_accountList[row].thread_gate_type; + return account.thread_gate_type; else if (role == ThreadGateOptionsRole) - return m_accountList[row].thread_gate_options; + return account.thread_gate_options; else if (role == PostGateQuoteEnabledRole) - return m_accountList[row].post_gate_quote_enabled; + return account.post_gate_quote_enabled; else if (role == StatusRole) - return static_cast(m_accountList.at(row).status); + return static_cast(account.status); else if (role == AuthorizedRole) - return m_accountList.at(row).status == AccountStatus::Authorized; + return account.status == AccountStatus::Authorized; return QVariant(); } void AccountListModel::update(int row, AccountListModelRoles role, const QVariant &value) { - if (row < 0 || row >= m_accountList.count()) + if (row < 0 || row >= count()) return; - if (role == UuidRole) - m_accountList[row].uuid = value.toString(); - else if (role == ServiceRole) - m_accountList[row].service = value.toString(); - else if (role == IdentifierRole) - m_accountList[row].identifier = value.toString(); - else if (role == PasswordRole) - m_accountList[row].password = value.toString(); - else if (role == DidRole) - m_accountList[row].did = value.toString(); - else if (role == HandleRole) - m_accountList[row].handle = value.toString(); - else if (role == EmailRole) - m_accountList[row].email = value.toString(); - else if (role == AccessJwtRole) - m_accountList[row].accessJwt = value.toString(); - else if (role == RefreshJwtRole) - m_accountList[row].refreshJwt = value.toString(); - - else if (role == DisplayNameRole) - m_accountList[row].displayName = value.toString(); - else if (role == DescriptionRole) - m_accountList[row].description = value.toString(); - else if (role == AvatarRole) - m_accountList[row].avatar = value.toString(); - - else if (role == PostLanguagesRole) { - m_accountList[row].post_languages = value.toStringList(); - save(); - } else if (role == ThreadGateTypeRole) { - m_accountList[row].thread_gate_type = value.toString(); - save(); - } else if (role == ThreadGateOptionsRole) { - m_accountList[row].thread_gate_options = value.toStringList(); - save(); - } else if (role == PostGateQuoteEnabledRole) { - m_accountList[row].post_gate_quote_enabled = value.toBool(); - } + AccountManager::getInstance()->update( + row, m_roleTo.value(role, AccountManager::AccountManagerRoles::UnknownRole), value); emit dataChanged(index(row), index(row)); } @@ -153,131 +166,76 @@ void AccountListModel::updateAccount(const QString &service, const QString &iden const QString &accessJwt, const QString &refreshJwt, const bool authorized) { + AccountManager *manager = AccountManager::getInstance(); + const QStringList uuids = manager->getUuids(); + bool updated = false; - for (int i = 0; i < m_accountList.count(); i++) { - if (m_accountList.at(i).service == service - && m_accountList.at(i).identifier == identifier) { + for (const auto &uuid : uuids) { + int i = manager->indexAt(uuid); + const AtProtocolInterface::AccountData account = manager->getAccount(uuid); + if (account.service == service && account.identifier == identifier) { // update - m_accountList[i].password = password; - m_accountList[i].did = did; - m_accountList[i].handle = handle; - m_accountList[i].email = email; - m_accountList[i].accessJwt = accessJwt; - m_accountList[i].refreshJwt = refreshJwt; - m_accountList[i].status = - authorized ? AccountStatus::Authorized : AccountStatus::Unauthorized; + manager->updateAccount(uuid, service, identifier, password, did, handle, email, + accessJwt, refreshJwt, authorized); updated = true; emit dataChanged(index(i), index(i)); } } if (!updated) { // append - AtProtocolInterface::AccountData item; - item.uuid = QUuid::createUuid().toString(QUuid::WithoutBraces); - item.service = service; - item.identifier = identifier; - item.password = password; - item.did = did; - item.handle = handle; - item.email = email; - item.accessJwt = accessJwt; - item.refreshJwt = refreshJwt; - item.thread_gate_type = "everybody"; - item.status = authorized ? AccountStatus::Authorized : AccountStatus::Unauthorized; - beginInsertRows(QModelIndex(), count(), count()); - m_accountList.append(item); + manager->updateAccount(QString(), service, identifier, password, did, handle, email, + accessJwt, refreshJwt, authorized); endInsertRows(); - - emit countChanged(); } - - checkAllAccountsReady(); - save(); } void AccountListModel::removeAccount(int row) { - if (row < 0 || row >= m_accountList.count()) + if (row < 0 || row >= count()) return; + AccountManager *manager = AccountManager::getInstance(); + beginRemoveRows(QModelIndex(), row, row); - m_accountList.removeAt(row); + manager->removeAccount(manager->getUuid(row)); endRemoveRows(); - emit countChanged(); - - checkAllAccountsReady(); - save(); } void AccountListModel::updateAccountProfile(const QString &service, const QString &identifier) { - for (int i = 0; i < m_accountList.count(); i++) { - if (m_accountList.at(i).service == service && m_accountList.at(i).identifier == identifier - && m_accountList.at(i).status == AccountStatus::Authorized) { - getProfile(i); + const QStringList uuids = AccountManager::getInstance()->getUuids(); + for (const auto &uuid : uuids) { + AccountData account = AccountManager::getInstance()->getAccount(uuid); + if (account.service == service && account.identifier == identifier) { + AccountManager::getInstance()->getProfile(indexAt(uuid)); } } } int AccountListModel::indexAt(const QString &uuid) { - if (uuid.isEmpty()) - return -1; - - for (int i = 0; i < m_accountList.count(); i++) { - if (m_accountList.at(i).uuid == uuid) { - return i; - } - } - return -1; + return AccountManager::getInstance()->indexAt(uuid); } int AccountListModel::getMainAccountIndex() const { - if (m_accountList.isEmpty()) - return -1; - - for (int i = 0; i < m_accountList.count(); i++) { - if (m_accountList.at(i).is_main) { - return i; - } - } - return 0; + return AccountManager::getInstance()->getMainAccountIndex(); } void AccountListModel::setMainAccount(int row) { - if (row < 0 || row >= m_accountList.count()) + if (row < 0 || row >= count()) return; - for (int i = 0; i < m_accountList.count(); i++) { - bool new_val = (row == i); - if (m_accountList.at(i).is_main != new_val) { - m_accountList[i].is_main = new_val; - emit dataChanged(index(i), index(i)); - } - } - - save(); -} - -bool AccountListModel::checkAllAccountsReady() -{ - int ready_count = 0; - for (const AccountData &item : qAsConst(m_accountList)) { - if (item.status == AccountStatus::Authorized) { - ready_count++; - } - } - setAllAccountsReady(!m_accountList.isEmpty() && m_accountList.count() == ready_count); - return allAccountsReady(); + AccountManager::getInstance()->setMainAccount(row); + emit dataChanged(index(0), index(AccountManager::getInstance()->count() - 1)); } void AccountListModel::refreshAccountSession(const QString &uuid) { int row = indexAt(uuid); - if (row < 0) + if (row < 0 || row >= count()) return; refreshSession(row); } @@ -285,132 +243,41 @@ void AccountListModel::refreshAccountSession(const QString &uuid) void AccountListModel::refreshAccountProfile(const QString &uuid) { int row = indexAt(uuid); - if (row < 0) + if (row < 0 || row >= count()) return; getProfile(row); } void AccountListModel::save() const { - QSettings settings; - - QJsonArray account_array; - for (const AccountData &item : m_accountList) { - QJsonObject account_item; - account_item["uuid"] = item.uuid; - account_item["is_main"] = item.is_main; - account_item["service"] = item.service; - account_item["identifier"] = item.identifier; - account_item["password"] = m_encryption.encrypt(item.password); - account_item["refresh_jwt"] = m_encryption.encrypt(item.refreshJwt); - - if (!item.post_languages.isEmpty()) { - QJsonArray post_langs; - for (const auto &lang : item.post_languages) { - post_langs.append(lang); - } - account_item["post_languages"] = post_langs; - } - - if (item.thread_gate_type.isEmpty()) { - account_item["thread_gate_type"] = "everybody"; - } else { - account_item["thread_gate_type"] = item.thread_gate_type; - } - if (!item.thread_gate_options.isEmpty()) { - QJsonArray thread_gate_options; - for (const auto &option : item.thread_gate_options) { - thread_gate_options.append(option); - } - account_item["thread_gate_options"] = thread_gate_options; - } - account_item["post_gate_quote_enabled"] = item.post_gate_quote_enabled; - - account_array.append(account_item); - } - - Common::saveJsonDocument(QJsonDocument(account_array), QStringLiteral("account.json")); + AccountManager::getInstance()->save(); } void AccountListModel::load() { - if (!m_accountList.isEmpty()) { - beginRemoveRows(QModelIndex(), 0, count() - 1); - m_accountList.clear(); - endRemoveRows(); - emit countChanged(); - } + AccountManager *manager = AccountManager::getInstance(); - QJsonDocument doc = Common::loadJsonDocument(QStringLiteral("account.json")); - - if (doc.isArray()) { - bool has_main = false; - for (int i = 0; i < doc.array().count(); i++) { - if (doc.array().at(i).isObject()) { - AccountData item; - QString temp_refresh = doc.array().at(i).toObject().value("refresh_jwt").toString(); - item.uuid = doc.array().at(i).toObject().value("uuid").toString(); - item.is_main = doc.array().at(i).toObject().value("is_main").toBool(); - item.service = doc.array().at(i).toObject().value("service").toString(); - item.service_endpoint = item.service; - item.identifier = doc.array().at(i).toObject().value("identifier").toString(); - item.password = m_encryption.decrypt( - doc.array().at(i).toObject().value("password").toString()); - item.refreshJwt = m_encryption.decrypt(temp_refresh); - item.handle = item.identifier; - for (const auto &value : - doc.array().at(i).toObject().value("post_languages").toArray()) { - item.post_languages.append(value.toString()); - } - - item.thread_gate_type = doc.array() - .at(i) - .toObject() - .value("thread_gate_type") - .toString("everybody"); - if (item.thread_gate_type.isEmpty()) { - item.thread_gate_type = "everybody"; - } - for (const auto &value : - doc.array().at(i).toObject().value("thread_gate_options").toArray()) { - item.thread_gate_options.append(value.toString()); - } - item.post_gate_quote_enabled = - doc.array().at(i).toObject().value("post_gate_quote_enabled").toBool(true); - - beginInsertRows(QModelIndex(), count(), count()); - m_accountList.append(item); - endInsertRows(); - emit countChanged(); - - if (temp_refresh.isEmpty()) { - createSession(m_accountList.count() - 1); - } else { - refreshSession(m_accountList.count() - 1, true); - } - - if (item.is_main) { - has_main = true; - } - } - } - if (!has_main && !m_accountList.isEmpty()) { - // mainになっているものがない - m_accountList[0].is_main = true; - } + if (manager->count() > 0) { + beginRemoveRows(QModelIndex(), 0, manager->count() - 1); + manager->clear(); + endRemoveRows(); } - checkAllAccountsReady(); - if (m_accountList.isEmpty()) { - emit finished(); + manager->load(); + if (manager->count() > 0) { + beginInsertRows(QModelIndex(), 0, manager->count() - 1); + endInsertRows(); } } QVariant AccountListModel::account(int row) const { - if (row < 0 || row >= m_accountList.count()) + QString uuid = AccountManager::getInstance()->getUuid(row); + if (uuid.isEmpty()) { return QVariant(); - return QVariant::fromValue(m_accountList.at(row)); + } else { + return QVariant::fromValue(AccountManager::getInstance()->getAccount(uuid)); + } } QHash AccountListModel::roleNames() const @@ -443,179 +310,25 @@ QHash AccountListModel::roleNames() const void AccountListModel::createSession(int row) { - if (row < 0 || row >= m_accountList.count()) - return; - - ComAtprotoServerCreateSessionEx *session = new ComAtprotoServerCreateSessionEx(this); - connect(session, &ComAtprotoServerCreateSessionEx::finished, [=](bool success) { - // qDebug() << session << session->service() << session->did() << - // session->handle() - // << session->email() << session->accessJwt() << session->refreshJwt(); - // qDebug() << service << identifier << password; - if (success) { - qDebug() << "Create session" << session->did() << session->handle(); - m_accountList[row].did = session->did(); - m_accountList[row].handle = session->handle(); - m_accountList[row].email = session->email(); - m_accountList[row].accessJwt = session->accessJwt(); - m_accountList[row].refreshJwt = session->refreshJwt(); - m_accountList[row].status = AccountStatus::Authorized; - - emit updatedSession(row, m_accountList[row].uuid); - - // 詳細を取得 - getProfile(row); - } else { - qDebug() << "Fail createSession."; - m_accountList[row].status = AccountStatus::Unauthorized; - emit errorOccured(session->errorCode(), session->errorMessage()); - } - emit dataChanged(index(row), index(row)); - checkAllAccountsReady(); - if (allAccountTried()) { - emit finished(); - } - session->deleteLater(); - }); - session->setAccount(m_accountList.at(row)); - session->createSession(m_accountList.at(row).identifier, m_accountList.at(row).password, - QString()); + AccountManager::getInstance()->createSession(row); } void AccountListModel::refreshSession(int row, bool initial) { - if (row < 0 || row >= m_accountList.count()) - return; - - ComAtprotoServerRefreshSessionEx *session = new ComAtprotoServerRefreshSessionEx(this); - connect(session, &ComAtprotoServerRefreshSessionEx::finished, [=](bool success) { - if (success) { - qDebug() << "Refresh session" << session->did() << session->handle() - << session->email(); - m_accountList[row].did = session->did(); - m_accountList[row].handle = session->handle(); - m_accountList[row].email = session->email(); - m_accountList[row].accessJwt = session->accessJwt(); - m_accountList[row].refreshJwt = session->refreshJwt(); - m_accountList[row].status = AccountStatus::Authorized; - - emit updatedSession(row, m_accountList[row].uuid); - - // 詳細を取得 - getProfile(row); - } else { - if (initial) { - // 初期化時のみ(つまりloadから呼ばれたときだけは失敗したらcreateSessionで再スタート) - qDebug() << "Initial refresh session fail."; - m_accountList[row].status = AccountStatus::Unknown; - createSession(row); - } else { - m_accountList[row].status = AccountStatus::Unauthorized; - emit errorOccured(session->errorCode(), session->errorMessage()); - } - } - emit dataChanged(index(row), index(row)); - checkAllAccountsReady(); - if (allAccountTried()) { - emit finished(); - } - session->deleteLater(); - }); - session->setAccount(m_accountList.at(row)); - session->refreshSession(); + AccountManager::getInstance()->refreshSession(row, initial); } void AccountListModel::getProfile(int row) { - if (row < 0 || row >= m_accountList.count()) - return; - - getServiceEndpoint(m_accountList.at(row).did, m_accountList.at(row).service, - [=](const QString &service_endpoint) { - m_accountList[row].service_endpoint = service_endpoint; - qDebug().noquote() - << "Update service endpoint" << m_accountList.at(row).service - << "->" << m_accountList.at(row).service_endpoint; - - AppBskyActorGetProfile *profile = new AppBskyActorGetProfile(this); - profile->setAccount(m_accountList.at(row)); - connect(profile, &AppBskyActorGetProfile::finished, [=](bool success) { - if (success) { - AtProtocolType::AppBskyActorDefs::ProfileViewDetailed detail = - profile->profileViewDetailed(); - qDebug() << "Update profile detailed" << detail.displayName - << detail.description; - m_accountList[row].displayName = detail.displayName; - m_accountList[row].description = detail.description; - m_accountList[row].avatar = detail.avatar; - m_accountList[row].banner = detail.banner; - - save(); - - emit updatedAccount(row, m_accountList[row].uuid); - emit dataChanged(index(row), index(row)); - - qDebug() << "Update pinned post" << detail.pinnedPost.uri; - PinnedPostCache::getInstance()->update(m_accountList.at(row).did, - detail.pinnedPost.uri); - } else { - emit errorOccured(profile->errorCode(), profile->errorMessage()); - } - profile->deleteLater(); - }); - profile->getProfile(m_accountList.at(row).did); - }); -} - -void AccountListModel::getServiceEndpoint(const QString &did, const QString &service, - std::function callback) -{ - if (did.isEmpty()) { - callback(service); - return; - } - if (!service.startsWith("https://bsky.social")) { - callback(service); - return; - } - - DirectoryPlc *plc = new DirectoryPlc(this); - connect(plc, &DirectoryPlc::finished, this, [=](bool success) { - if (success) { - callback(plc->serviceEndpoint()); - } else { - callback(service); - } - plc->deleteLater(); - }); - plc->directory(did); -} - -bool AccountListModel::allAccountTried() const -{ - int count = 0; - for (const AccountData &item : qAsConst(m_accountList)) { - if (item.status != AccountStatus::Unknown) { - count++; - } - } - return (m_accountList.count() == count); + AccountManager::getInstance()->getProfile(row); } int AccountListModel::count() const { - return m_accountList.count(); + return AccountManager::getInstance()->count(); } bool AccountListModel::allAccountsReady() const { - return m_allAccountsReady; -} - -void AccountListModel::setAllAccountsReady(bool newAllAccountsReady) -{ - if (m_allAccountsReady == newAllAccountsReady) - return; - m_allAccountsReady = newAllAccountsReady; - emit allAccountsReadyChanged(); + return AccountManager::getInstance()->allAccountsReady(); } diff --git a/app/qtquick/account/accountlistmodel.h b/app/qtquick/account/accountlistmodel.h index 37c650ca..9901498f 100644 --- a/app/qtquick/account/accountlistmodel.h +++ b/app/qtquick/account/accountlistmodel.h @@ -2,7 +2,8 @@ #define ACCOUNTLISTMODEL_H #include "atprotocol/accessatprotocol.h" -#include "encryption.h" +#include "tools/encryption.h" +#include "tools/accountmanager.h" #include #include @@ -13,10 +14,10 @@ class AccountListModel : public QAbstractListModel Q_OBJECT Q_PROPERTY(int count READ count NOTIFY countChanged CONSTANT) - Q_PROPERTY(bool allAccountsReady READ allAccountsReady WRITE setAllAccountsReady NOTIFY - allAccountsReadyChanged FINAL) + Q_PROPERTY(bool allAccountsReady READ allAccountsReady NOTIFY allAccountsReadyChanged FINAL) public: explicit AccountListModel(QObject *parent = nullptr); + ~AccountListModel(); // モデルで提供する項目のルールID的な(QML側へ公開するために大文字で始めること) enum AccountListModelRoles { @@ -61,7 +62,6 @@ class AccountListModel : public QAbstractListModel Q_INVOKABLE int indexAt(const QString &uuid); Q_INVOKABLE int getMainAccountIndex() const; Q_INVOKABLE void setMainAccount(int row); - Q_INVOKABLE bool checkAllAccountsReady(); Q_INVOKABLE void refreshAccountSession(const QString &uuid); Q_INVOKABLE void refreshAccountProfile(const QString &uuid); @@ -72,12 +72,10 @@ class AccountListModel : public QAbstractListModel int count() const; bool allAccountsReady() const; - void setAllAccountsReady(bool newAllAccountsReady); signals: void errorOccured(const QString &code, const QString &message); - void updatedSession(int row, const QString &uuid); - void updatedAccount(int row, const QString &uuid); + void updatedAccount(const QString &uuid); void countChanged(); void finished(); @@ -87,20 +85,16 @@ class AccountListModel : public QAbstractListModel QHash roleNames() const; private: - QList m_accountList; QVariant m_accountTemp; QTimer m_timer; Encryption m_encryption; - bool m_allAccountsReady; + QHash m_roleTo; QString appDataFolder() const; void createSession(int row); void refreshSession(int row, bool initial = false); void getProfile(int row); - void getServiceEndpoint(const QString &did, const QString &service, - std::function callback); - bool allAccountTried() const; }; #endif // ACCOUNTLISTMODEL_H diff --git a/app/qtquick/atpabstractlistmodel.cpp b/app/qtquick/atpabstractlistmodel.cpp index 7471f862..9000886b 100644 --- a/app/qtquick/atpabstractlistmodel.cpp +++ b/app/qtquick/atpabstractlistmodel.cpp @@ -4,6 +4,7 @@ #include "atprotocol/lexicons_func_unknown.h" #include "extension/com/atproto/repo/comatprotorepogetrecordex.h" #include "extension/com/atproto/repo/comatprotorepoputrecordex.h" +#include "tools/accountmanager.h" #include "tools/labelerprovider.h" #include "common.h" #include "operation/translator.h" @@ -64,19 +65,12 @@ void AtpAbstractListModel::clear() AtProtocolInterface::AccountData AtpAbstractListModel::account() const { - return m_account; + return AccountManager::getInstance()->getAccount(m_account.uuid); } -void AtpAbstractListModel::setAccount(const QString &service, const QString &did, - const QString &handle, const QString &email, - const QString &accessJwt, const QString &refreshJwt) +void AtpAbstractListModel::setAccount(const QString &uuid) { - m_account.service = service; - m_account.did = did; - m_account.handle = handle; - m_account.email = email; - m_account.accessJwt = accessJwt; - m_account.refreshJwt = refreshJwt; + m_account.uuid = uuid; } QString AtpAbstractListModel::getTranslation(const QString &cid) const @@ -420,7 +414,12 @@ QStringList AtpAbstractListModel::getLaunguages(const QVariant &record) const QString AtpAbstractListModel::getVia(const QVariant &record) const { - return LexiconsTypeUnknown::fromQVariant(record).via; + AppBskyFeedPost::Main post = LexiconsTypeUnknown::fromQVariant(record); + if (!post.space_aoisora_post_via.isEmpty()) { + return post.space_aoisora_post_via; + } else { + return post.via; + } } QVariant AtpAbstractListModel::getQuoteItem(const AtProtocolType::AppBskyFeedDefs::PostView &post, @@ -1242,6 +1241,11 @@ void AtpAbstractListModel::setLoadingInterval(int newLoadingInterval) emit loadingIntervalChanged(); } +QString AtpAbstractListModel::uuid() const +{ + return account().uuid; +} + QString AtpAbstractListModel::service() const { return account().service; diff --git a/app/qtquick/atpabstractlistmodel.h b/app/qtquick/atpabstractlistmodel.h index 865d496a..4156a558 100644 --- a/app/qtquick/atpabstractlistmodel.h +++ b/app/qtquick/atpabstractlistmodel.h @@ -52,6 +52,7 @@ class AtpAbstractListModel : public QAbstractListModel Q_PROPERTY(bool displayPinnedPost READ displayPinnedPost WRITE setDisplayPinnedPost NOTIFY displayPinnedPostChanged) + Q_PROPERTY(QString uuid READ uuid NOTIFY uuidChanged CONSTANT) Q_PROPERTY(QString service READ service CONSTANT) Q_PROPERTY(QString did READ did CONSTANT) Q_PROPERTY(QString handle READ handle CONSTANT) @@ -133,9 +134,7 @@ class AtpAbstractListModel : public QAbstractListModel Q_INVOKABLE void clear(); AtProtocolInterface::AccountData account() const; - Q_INVOKABLE void setAccount(const QString &service, const QString &did, const QString &handle, - const QString &email, const QString &accessJwt, - const QString &refreshJwt); + Q_INVOKABLE void setAccount(const QString &uuid); virtual Q_INVOKABLE int indexOf(const QString &cid) const = 0; virtual Q_INVOKABLE QString getRecordText(const QString &cid) = 0; virtual Q_INVOKABLE QString getOfficialUrl() const = 0; @@ -160,6 +159,7 @@ class AtpAbstractListModel : public QAbstractListModel bool displayPinnedPost() const; void setDisplayPinnedPost(bool newDisplayPinnedPost); + QString uuid() const; QString service() const; QString did() const; QString handle() const; @@ -180,6 +180,8 @@ class AtpAbstractListModel : public QAbstractListModel void pinnedPostChanged(); void displayPinnedPostChanged(); + void uuidChanged(); + public slots: virtual Q_INVOKABLE bool getLatest() = 0; virtual Q_INVOKABLE bool getNext() = 0; diff --git a/app/qtquick/blog/blogentrylistmodel.cpp b/app/qtquick/blog/blogentrylistmodel.cpp index 00c890ad..c5ee78e2 100644 --- a/app/qtquick/blog/blogentrylistmodel.cpp +++ b/app/qtquick/blog/blogentrylistmodel.cpp @@ -21,6 +21,70 @@ QVariant BlogEntryListModel::data(const QModelIndex &index, int role) const } QVariant BlogEntryListModel::item(int row, BlogEntryListModelRoles role) const +{ + if (row < 0 || row >= m_blogEntryRecordList.count()) + return QVariant(); + + if (m_blogEntryRecordList.at(row).uri.split("/").contains("blue.linkat.board")) { + return itemFromLinkat(row, role); + } else { + return itemFromWhiteWind(row, role); + } + + return QVariant(); +} + +AtProtocolInterface::AccountData BlogEntryListModel::account() const +{ + return m_account; +} + +void BlogEntryListModel::setAccount(const QString &uuid) +{ + m_account.uuid = uuid; +} + +bool BlogEntryListModel::getLatest() +{ + if (running() || targetDid().isEmpty()) + return false; + setRunning(true); + + if (!m_blogEntryRecordList.isEmpty()) { + beginRemoveRows(QModelIndex(), 0, m_blogEntryRecordList.count() - 1); + m_blogEntryRecordList.clear(); + endRemoveRows(); + } + + getLatestFromLinkat([=](bool success) { + if (success) { + getLatestFromWhiteWind([=](bool success) { setRunning(false); }); + } else { + setRunning(false); + } + }); + + return true; +} + +QHash BlogEntryListModel::roleNames() const +{ + QHash roles; + + roles[CidRole] = "cid"; + roles[UriRole] = "uri"; + + roles[ServiceNameRole] = "serviceName"; + roles[TitleRole] = "title"; + roles[ContentRole] = "content"; + roles[CreatedAtRole] = "createdAt"; + roles[VisibilityRole] = "visibility"; + roles[PermalinkRole] = "permalink"; + + return roles; +} + +QVariant BlogEntryListModel::itemFromWhiteWind(int row, BlogEntryListModelRoles role) const { if (row < 0 || row >= m_blogEntryRecordList.count()) return QVariant(); @@ -69,35 +133,48 @@ QVariant BlogEntryListModel::item(int row, BlogEntryListModelRoles role) const return QVariant(); } -AtProtocolInterface::AccountData BlogEntryListModel::account() const +QVariant BlogEntryListModel::itemFromLinkat(int row, BlogEntryListModelRoles role) const { - return m_account; -} + if (row < 0 || row >= m_blogEntryRecordList.count()) + return QVariant(); -void BlogEntryListModel::setAccount(const QString &service, const QString &did, - const QString &handle, const QString &email, - const QString &accessJwt, const QString &refreshJwt) -{ - m_account.service = service; - m_account.did = did; - m_account.handle = handle; - m_account.email = email; - m_account.accessJwt = accessJwt; - m_account.refreshJwt = refreshJwt; -} + const auto ¤t = AtProtocolType::LexiconsTypeUnknown::fromQVariant< + AtProtocolType::BlueLinkatBoard::Main>(m_blogEntryRecordList.at(row).value); -bool BlogEntryListModel::getLatest() -{ - if (running() || targetDid().isEmpty()) - return false; - setRunning(true); + if (current.cards.isEmpty()) + return QVariant(); - if (!m_blogEntryRecordList.isEmpty()) { - beginRemoveRows(QModelIndex(), 0, m_blogEntryRecordList.count() - 1); - m_blogEntryRecordList.clear(); - endRemoveRows(); + if (role == CidRole) + return m_blogEntryRecordList.at(row).cid; + else if (role == UriRole) + return m_blogEntryRecordList.at(row).uri; + else if (role == ServiceNameRole) + return "Linkat"; + else if (role == TitleRole) { + if (current.cards.first().text.isEmpty()) { + return current.cards.first().url; + } else { + return current.cards.first().text; + } + } else if (role == ContentRole) + if (current.cards.length() > 1) { + return "and more ..."; + } else { + return QString(); + } + else if (role == CreatedAtRole) + return QString(); + else if (role == VisibilityRole) { + return QString(); + } else if (role == PermalinkRole) { + return QStringLiteral("https://linkat.blue/") + targetHandle(); } + return QVariant(); +} + +void BlogEntryListModel::getLatestFromWhiteWind(std::function callback) +{ ComAtprotoRepoListRecordsEx *record = new ComAtprotoRepoListRecordsEx(this); connect(record, &ComAtprotoRepoListRecordsEx::finished, this, [=](bool success) { if (success) { @@ -116,7 +193,7 @@ bool BlogEntryListModel::getLatest() } else { emit errorOccured(record->errorCode(), record->errorMessage()); } - setRunning(false); + callback(success); record->deleteLater(); }); record->setAccount(account()); @@ -124,25 +201,34 @@ bool BlogEntryListModel::getLatest() record->setService(targetServiceEndpoint()); } record->listWhiteWindItems(targetDid(), QString()); - - return true; } -QHash BlogEntryListModel::roleNames() const +void BlogEntryListModel::getLatestFromLinkat(std::function callback) { - QHash roles; - - roles[CidRole] = "cid"; - roles[UriRole] = "uri"; - - roles[ServiceNameRole] = "serviceName"; - roles[TitleRole] = "title"; - roles[ContentRole] = "content"; - roles[CreatedAtRole] = "createdAt"; - roles[VisibilityRole] = "visibility"; - roles[PermalinkRole] = "permalink"; - - return roles; + ComAtprotoRepoListRecordsEx *record = new ComAtprotoRepoListRecordsEx(this); + connect(record, &ComAtprotoRepoListRecordsEx::finished, this, [=](bool success) { + if (success) { + for (const auto &r : record->recordsList()) { + const auto ¤t = AtProtocolType::LexiconsTypeUnknown::fromQVariant< + AtProtocolType::BlueLinkatBoard::Main>(r.value); + if (!current.cards.isEmpty()) { + beginInsertRows(QModelIndex(), rowCount(), rowCount()); + m_blogEntryRecordList.append(r); + endInsertRows(); + break; + } + } + } else { + emit errorOccured(record->errorCode(), record->errorMessage()); + } + callback(success); + record->deleteLater(); + }); + record->setAccount(account()); + if (!targetServiceEndpoint().isEmpty()) { + record->setService(targetServiceEndpoint()); + } + record->listLinkatItems(targetDid(), QString()); } bool BlogEntryListModel::running() const @@ -183,3 +269,16 @@ void BlogEntryListModel::setTargetServiceEndpoint(const QString &newTargetServic m_targetServiceEndpoint = newTargetServiceEndpoint; emit targetServiceEndpointChanged(); } + +QString BlogEntryListModel::targetHandle() const +{ + return m_targetHandle; +} + +void BlogEntryListModel::setTargetHandle(const QString &newTargetHandle) +{ + if (m_targetHandle == newTargetHandle) + return; + m_targetHandle = newTargetHandle; + emit targetHandleChanged(); +} diff --git a/app/qtquick/blog/blogentrylistmodel.h b/app/qtquick/blog/blogentrylistmodel.h index b713bc2b..5b8d9597 100644 --- a/app/qtquick/blog/blogentrylistmodel.h +++ b/app/qtquick/blog/blogentrylistmodel.h @@ -9,6 +9,8 @@ class BlogEntryListModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(bool running READ running WRITE setRunning NOTIFY runningChanged) + Q_PROPERTY(QString targetHandle READ targetHandle WRITE setTargetHandle NOTIFY + targetHandleChanged FINAL) Q_PROPERTY(QString targetDid READ targetDid WRITE setTargetDid NOTIFY targetDidChanged) Q_PROPERTY(QString targetServiceEndpoint READ targetServiceEndpoint WRITE setTargetServiceEndpoint NOTIFY targetServiceEndpointChanged FINAL) @@ -37,9 +39,7 @@ class BlogEntryListModel : public QAbstractListModel Q_INVOKABLE QVariant item(int row, BlogEntryListModel::BlogEntryListModelRoles role) const; AtProtocolInterface::AccountData account() const; - Q_INVOKABLE void setAccount(const QString &service, const QString &did, const QString &handle, - const QString &email, const QString &accessJwt, - const QString &refreshJwt); + Q_INVOKABLE void setAccount(const QString &uuid); Q_INVOKABLE bool getLatest(); @@ -50,6 +50,9 @@ class BlogEntryListModel : public QAbstractListModel QString targetServiceEndpoint() const; void setTargetServiceEndpoint(const QString &newTargetServiceEndpoint); + QString targetHandle() const; + void setTargetHandle(const QString &newTargetHandle); + signals: void errorOccured(const QString &code, const QString &message); void runningChanged(); @@ -57,15 +60,23 @@ class BlogEntryListModel : public QAbstractListModel void countChanged(); void targetServiceEndpointChanged(); + void targetHandleChanged(); + protected: QHash roleNames() const; private: + QVariant itemFromWhiteWind(int row, BlogEntryListModel::BlogEntryListModelRoles role) const; + QVariant itemFromLinkat(int row, BlogEntryListModel::BlogEntryListModelRoles role) const; + void getLatestFromWhiteWind(std::function callback); + void getLatestFromLinkat(std::function callback); + QList m_blogEntryRecordList; AtProtocolInterface::AccountData m_account; bool m_running; QString m_targetDid; QString m_targetServiceEndpoint; + QString m_targetHandle; }; #endif // BLOGENTRYLISTMODEL_H diff --git a/app/qtquick/chat/atpchatabstractlistmodel.cpp b/app/qtquick/chat/atpchatabstractlistmodel.cpp index 4a86b7a3..1f4d0c9a 100644 --- a/app/qtquick/chat/atpchatabstractlistmodel.cpp +++ b/app/qtquick/chat/atpchatabstractlistmodel.cpp @@ -2,6 +2,7 @@ #include "extension/directory/plc/directoryplc.h" #include "atprotocol/chat/bsky/convo/chatbskyconvoupdateread.h" #include "tools/labelerprovider.h" +#include "tools/accountmanager.h" using AtProtocolInterface::ChatBskyConvoUpdateRead; using AtProtocolInterface::DirectoryPlc; @@ -19,24 +20,12 @@ void AtpChatAbstractListModel::clear() AtProtocolInterface::AccountData AtpChatAbstractListModel::account() const { - return m_account; + return AccountManager::getInstance()->getAccount(m_account.uuid); } -void AtpChatAbstractListModel::setAccount(const QString &service, const QString &did, - const QString &handle, const QString &email, - const QString &accessJwt, const QString &refreshJwt) +void AtpChatAbstractListModel::setAccount(const QString &uuid) { - m_account.service = service; - m_account.did = did; - m_account.handle = handle; - m_account.email = email; - m_account.accessJwt = accessJwt; - m_account.refreshJwt = refreshJwt; -} - -void AtpChatAbstractListModel::setServiceEndpoint(const QString &service_endpoint) -{ - m_account.service_endpoint = service_endpoint; + m_account.uuid = uuid; } void AtpChatAbstractListModel::updateRead(const QString &convoId, const QString &messageId) @@ -74,7 +63,7 @@ void AtpChatAbstractListModel::setRunning(bool newRunning) void AtpChatAbstractListModel::getServiceEndpoint(std::function callback) { - if (!m_account.service_endpoint.isEmpty()) { + if (!account().service_endpoint.isEmpty()) { callback(); return; } @@ -83,22 +72,22 @@ void AtpChatAbstractListModel::getServiceEndpoint(std::function callback return; } if (!account().service.startsWith("https://bsky.social")) { - m_account.service_endpoint = m_account.service; - qDebug().noquote() << "Update service endpoint(chat)" << m_account.service << "->" - << m_account.service_endpoint; + account().service_endpoint = account().service; + qDebug().noquote() << "Update service endpoint(chat)" << account().service << "->" + << account().service_endpoint; callback(); return; } DirectoryPlc *plc = new DirectoryPlc(this); connect(plc, &DirectoryPlc::finished, this, [=](bool success) { + QString service_endpoint = account().service; if (success && !plc->serviceEndpoint().isEmpty()) { - m_account.service_endpoint = plc->serviceEndpoint(); - } else { - m_account.service_endpoint = m_account.service; + service_endpoint = plc->serviceEndpoint(); } - qDebug().noquote() << "Update service endpoint(chat)" << m_account.service << "->" - << m_account.service_endpoint; + AccountManager::getInstance()->updateServiceEndpoint(account().uuid, service_endpoint); + qDebug().noquote() << "Update service endpoint(chat)" << account().service << "->" + << account().service_endpoint; callback(); plc->deleteLater(); }); diff --git a/app/qtquick/chat/atpchatabstractlistmodel.h b/app/qtquick/chat/atpchatabstractlistmodel.h index 5fb841db..9fcbcdc1 100644 --- a/app/qtquick/chat/atpchatabstractlistmodel.h +++ b/app/qtquick/chat/atpchatabstractlistmodel.h @@ -22,10 +22,7 @@ class AtpChatAbstractListModel : public QAbstractListModel Q_INVOKABLE void clear(); AtProtocolInterface::AccountData account() const; - Q_INVOKABLE void setAccount(const QString &service, const QString &did, const QString &handle, - const QString &email, const QString &accessJwt, - const QString &refreshJwt); - void setServiceEndpoint(const QString &service_endpoint); + Q_INVOKABLE void setAccount(const QString &uuid); virtual Q_INVOKABLE bool getLatest() = 0; virtual Q_INVOKABLE bool getNext() = 0; diff --git a/app/qtquick/controls/embedimagelistmodel.cpp b/app/qtquick/controls/embedimagelistmodel.cpp index 6a082728..28419c4f 100644 --- a/app/qtquick/controls/embedimagelistmodel.cpp +++ b/app/qtquick/controls/embedimagelistmodel.cpp @@ -2,6 +2,8 @@ #include #include +#include +#include EmbedImageListModel::EmbedImageListModel(QObject *parent) : QAbstractListModel { parent }, m_count(0), m_running(false) @@ -80,15 +82,29 @@ void EmbedImageListModel::remove(const int row) } } -void EmbedImageListModel::append(const QStringList &uris) +bool EmbedImageListModel::append(const QStringList &uris) { if (uris.isEmpty() || running()) - return; + return false; setRunning(true); - m_uriCue = uris; + m_uriCue.clear(); + + QStringList exts; + exts << "jpg" + << "jpeg" + << "png" + << "gif"; + for (const auto &uri : uris) { + QFileInfo info(QUrl(uri).toLocalFile()); + if (exts.contains(info.suffix().toLower())) { + m_uriCue.append(uri); + } + } QTimer::singleShot(10, this, &EmbedImageListModel::appendItem); + + return !m_uriCue.isEmpty(); } void EmbedImageListModel::updateAlt(const int row, const QString &alt) diff --git a/app/qtquick/controls/embedimagelistmodel.h b/app/qtquick/controls/embedimagelistmodel.h index 1919224f..c4524875 100644 --- a/app/qtquick/controls/embedimagelistmodel.h +++ b/app/qtquick/controls/embedimagelistmodel.h @@ -36,7 +36,7 @@ class EmbedImageListModel : public QAbstractListModel Q_INVOKABLE void clear(); Q_INVOKABLE void remove(const int row); - Q_INVOKABLE void append(const QStringList &uris); + Q_INVOKABLE bool append(const QStringList &uris); Q_INVOKABLE void updateAlt(const int row, const QString &alt); Q_INVOKABLE QStringList uris() const; diff --git a/app/qtquick/feedgenerator/feedgeneratorlistmodel.cpp b/app/qtquick/feedgenerator/feedgeneratorlistmodel.cpp index 66c77272..f84659d6 100644 --- a/app/qtquick/feedgenerator/feedgeneratorlistmodel.cpp +++ b/app/qtquick/feedgenerator/feedgeneratorlistmodel.cpp @@ -3,6 +3,7 @@ #include "atprotocol/app/bsky/actor/appbskyactorgetpreferences.h" #include "atprotocol/app/bsky/unspecced/appbskyunspeccedgetpopularfeedgenerators.h" #include "atprotocol/app/bsky/actor/appbskyactorputpreferences.h" +#include "tools/tid.h" #include #include @@ -229,13 +230,22 @@ void FeedGeneratorListModel::getSavedGenerators() AppBskyActorGetPreferences *pref = new AppBskyActorGetPreferences(this); connect(pref, &AppBskyActorGetPreferences::finished, [=](bool success) { if (success) { - for (const auto &feed : pref->preferences().savedFeedsPref) { - m_savedUriList.append(feed.saved); + for (const auto &prefs : pref->preferences().savedFeedsPrefV2) { + for (const auto &item : prefs.items) { + if (item.type == "feed") { + m_savedUriList.append(item.value); + } + } } - for (int i = 0; i < m_cidList.count(); i++) { - emit dataChanged(index(i), index(i), - QVector() << static_cast(SavingRole)); + for (const auto &feed : pref->preferences().savedFeedsPref) { + for (const auto &uri : feed.saved) { + if (!m_savedUriList.contains(uri)) { + m_savedUriList.append(uri); + } + } } + emit dataChanged(index(0), index(m_cidList.count() - 1), + QVector() << static_cast(SavingRole)); } else { emit errorOccured(pref->errorCode(), pref->errorMessage()); } @@ -281,14 +291,27 @@ QJsonArray FeedGeneratorListModel::appendGeneratorToPreference(const QString &sr if (!preferences.toArray().at(i).isObject()) continue; QJsonObject value = preferences.toArray().takeAt(i).toObject(); - if (value.value("$type") == QStringLiteral("app.bsky.actor.defs#savedFeedsPref") - && value.value("saved").isArray()) { - QJsonArray json_saved = value.value("saved").toArray(); - if (!json_saved.contains(QJsonValue(uri))) { + if (value.value("$type") == QStringLiteral("app.bsky.actor.defs#savedFeedsPrefV2") + && value.value("items").isArray()) { + QJsonArray json_items = value.value("items").toArray(); + QJsonArray json_items_dest; + bool exist = false; + for (const auto &v : qAsConst(json_items)) { + if (v.toObject().value("value").toString() == uri) { + exist = true; + } + json_items_dest.append(v); + } + if (!exist) { // 含まれていなければ追加 - json_saved.append(QJsonValue(uri)); - value.insert("saved", json_saved); + QJsonObject json_item; + json_item.insert("id", Tid::next()); + json_item.insert("pinned", false); + json_item.insert("type", "feed"); + json_item.insert("value", uri); + json_items_dest.append(json_item); } + value.insert("items", json_items_dest); } dest_preferences.append(value); } @@ -312,8 +335,20 @@ QJsonArray FeedGeneratorListModel::removeGeneratorToPreference(const QString &sr if (!preferences.toArray().at(i).isObject()) continue; QJsonObject value = preferences.toArray().takeAt(i).toObject(); - if (value.value("$type") == QStringLiteral("app.bsky.actor.defs#savedFeedsPref") - && value.value("saved").isArray()) { + if (value.value("$type") == QStringLiteral("app.bsky.actor.defs#savedFeedsPrefV2") + && value.value("items").isArray()) { + QJsonArray json_items = value.value("items").toArray(); + QJsonArray json_items_dest; + for (const auto &v : qAsConst(json_items)) { + if (v.toObject().value("value").toString() != uri) { + json_items_dest.append(v); + } + } + value.insert("items", json_items_dest); + + } else if (value.value("$type") == QStringLiteral("app.bsky.actor.defs#savedFeedsPref") + && value.value("saved").isArray()) { + // 互換性維持で読み込み側はv1とv2のORなので削除も両方から実施 QJsonArray json_saved; for (const auto &value : value.value("saved").toArray()) { if (value.toString() != uri) { diff --git a/app/qtquick/link/feedgeneratorlink.cpp b/app/qtquick/link/feedgeneratorlink.cpp index bdbbfd11..1c479689 100644 --- a/app/qtquick/link/feedgeneratorlink.cpp +++ b/app/qtquick/link/feedgeneratorlink.cpp @@ -2,6 +2,7 @@ #include "atprotocol/app/bsky/feed/appbskyfeedgetfeedgenerator.h" #include "atprotocol/app/bsky/actor/appbskyactorgetprofile.h" #include "atprotocol/lexicons_func_unknown.h" +#include "tools/accountmanager.h" using AtProtocolInterface::AppBskyActorGetProfile; using AtProtocolInterface::AppBskyFeedGetFeedGenerator; @@ -12,16 +13,14 @@ FeedGeneratorLink::FeedGeneratorLink(QObject *parent) m_rxHandle.setPattern(QString("^%1$").arg(REG_EXP_HANDLE)); } -void FeedGeneratorLink::setAccount(const QString &service, const QString &did, - const QString &handle, const QString &email, - const QString &accessJwt, const QString &refreshJwt) +AtProtocolInterface::AccountData FeedGeneratorLink::account() const { - m_account.service = service; - m_account.did = did; - m_account.handle = handle; - m_account.email = email; - m_account.accessJwt = accessJwt; - m_account.refreshJwt = refreshJwt; + return AccountManager::getInstance()->getAccount(m_account.uuid); +} + +void FeedGeneratorLink::setAccount(const QString &uuid) +{ + m_account.uuid = uuid; } bool FeedGeneratorLink::checkUri(const QString &uri, const QString &type) const @@ -65,7 +64,7 @@ void FeedGeneratorLink::convertToAtUri(const QString &base_at_uri, const QString } profile->deleteLater(); }); - profile->setAccount(m_account); + profile->setAccount(account()); profile->getProfile(user_id); } else { // did @@ -100,7 +99,7 @@ void FeedGeneratorLink::getFeedGenerator(const QString &uri) setRunning(false); generator->deleteLater(); }); - generator->setAccount(m_account); + generator->setAccount(account()); generator->getFeedGenerator(at_uri); }); } diff --git a/app/qtquick/link/feedgeneratorlink.h b/app/qtquick/link/feedgeneratorlink.h index c22a6e6b..db41be5e 100644 --- a/app/qtquick/link/feedgeneratorlink.h +++ b/app/qtquick/link/feedgeneratorlink.h @@ -21,9 +21,8 @@ class FeedGeneratorLink : public QObject public: explicit FeedGeneratorLink(QObject *parent = nullptr); - Q_INVOKABLE void setAccount(const QString &service, const QString &did, const QString &handle, - const QString &email, const QString &accessJwt, - const QString &refreshJwt); + AtProtocolInterface::AccountData account() const; + Q_INVOKABLE void setAccount(const QString &uuid); Q_INVOKABLE bool checkUri(const QString &uri, const QString &type) const; void convertToAtUri(const QString &base_at_uri, const QString &uri, std::function callback); @@ -60,9 +59,8 @@ class FeedGeneratorLink : public QObject void cidChanged(); protected: - AtProtocolInterface::AccountData m_account; - private: + AtProtocolInterface::AccountData m_account; QRegularExpression m_rxHandle; bool m_running; diff --git a/app/qtquick/link/listlink.cpp b/app/qtquick/link/listlink.cpp index f3a95d6c..f7548acd 100644 --- a/app/qtquick/link/listlink.cpp +++ b/app/qtquick/link/listlink.cpp @@ -33,7 +33,7 @@ void ListLink::getList(const QString &uri) setRunning(false); list->deleteLater(); }); - list->setAccount(m_account); + list->setAccount(account()); list->getList(at_uri, 0, QString()); }); } diff --git a/app/qtquick/link/postlink.cpp b/app/qtquick/link/postlink.cpp index 1506c03d..19ee1c2a 100644 --- a/app/qtquick/link/postlink.cpp +++ b/app/qtquick/link/postlink.cpp @@ -37,7 +37,7 @@ void PostLink::getPost(const QString &uri) setRunning(false); post->deleteLater(); }); - post->setAccount(m_account); + post->setAccount(account()); post->getPosts(QStringList() << at_uri); }); } diff --git a/app/qtquick/list/listitemlistmodel.cpp b/app/qtquick/list/listitemlistmodel.cpp index 15fc123b..2bff9832 100644 --- a/app/qtquick/list/listitemlistmodel.cpp +++ b/app/qtquick/list/listitemlistmodel.cpp @@ -126,8 +126,7 @@ void ListItemListModel::block() } ope->deleteLater(); }); - ope->setAccount(account().service, account().did, account().handle, account().email, - account().accessJwt, account().refreshJwt); + ope->setAccount(account().uuid); if (blocked()) { // -> unblock ope->deleteBlockList(blockedUri()); diff --git a/app/qtquick/list/listslistmodel.cpp b/app/qtquick/list/listslistmodel.cpp index 9f5424fa..e2d8d0df 100644 --- a/app/qtquick/list/listslistmodel.cpp +++ b/app/qtquick/list/listslistmodel.cpp @@ -185,8 +185,7 @@ bool ListsListModel::addRemoveFromList(const int row, const QString &did) setRunning(false); ope->deleteLater(); }); - ope->setAccount(account().service, account().did, account().handle, account().email, - account().accessJwt, account().refreshJwt); + ope->setAccount(account().uuid); if (status == SearchStatusTypeContains) { // delete // uriのレコードを消す(リストに登録しているユーザーの情報) @@ -264,8 +263,7 @@ void ListsListModel::block(const int row) } ope->deleteLater(); }); - ope->setAccount(account().service, account().did, account().handle, account().email, - account().accessJwt, account().refreshJwt); + ope->setAccount(account().uuid); if (blocked.toBool()) { // -> unblock ope->deleteBlockList(item(row, ListsListModel::BlockedUriRole).toString()); diff --git a/app/qtquick/log/logfeedlistmodel.cpp b/app/qtquick/log/logfeedlistmodel.cpp index 5d5eda5b..f95d5729 100644 --- a/app/qtquick/log/logfeedlistmodel.cpp +++ b/app/qtquick/log/logfeedlistmodel.cpp @@ -1,6 +1,5 @@ #include "logfeedlistmodel.h" #include "log/logmanager.h" -#include "atprotocol/lexicons.h" #include #include @@ -16,7 +15,7 @@ LogFeedListModel::LogFeedListModel(QObject *parent) bool LogFeedListModel::getLatest() { - if (running() || did().isEmpty() || selectCondition().isEmpty()) { + if (running() || targetDid().isEmpty() || selectCondition().isEmpty()) { emit finished(false); return false; } @@ -61,15 +60,15 @@ bool LogFeedListModel::getLatest() manager->deleteLater(); }); manager->setAccount(account()); - emit manager->selectRecords(did(), static_cast(feedType()), selectCondition(), QString(), - 0); + emit manager->selectRecords(targetDid(), static_cast(feedType()), selectCondition(), + QString(), 0); return true; } bool LogFeedListModel::getNext() { - if (running() || did().isEmpty() || selectCondition().isEmpty()) { + if (running() || targetDid().isEmpty() || selectCondition().isEmpty()) { emit finished(false); return false; } @@ -92,8 +91,8 @@ bool LogFeedListModel::getNext() manager->deleteLater(); }); manager->setAccount(account()); - emit manager->selectRecords(did(), static_cast(feedType()), selectCondition(), m_cursor, - 0); + emit manager->selectRecords(targetDid(), static_cast(feedType()), selectCondition(), + m_cursor, 0); return true; } @@ -132,27 +131,27 @@ void LogFeedListModel::setFeedType(LogFeedListModelFeedType newFeedType) QString LogFeedListModel::targetDid() const { - return account().did; + return m_targetDid; } void LogFeedListModel::setTargetDid(const QString &newTargetDid) { - if (account().did == newTargetDid) + if (m_targetDid == newTargetDid) return; - setAccount(service(), newTargetDid, handle(), email(), accessJwt(), refreshJwt()); + m_targetDid = newTargetDid; emit targetDidChanged(); } QString LogFeedListModel::targetHandle() const { - return account().handle; + return m_targetHandle; } void LogFeedListModel::setTargetHandle(const QString &newTargetHandle) { - if (account().handle == newTargetHandle) + if (m_targetHandle == newTargetHandle) return; - setAccount(service(), did(), newTargetHandle, email(), accessJwt(), refreshJwt()); + m_targetHandle = newTargetHandle; emit targetHandleChanged(); } diff --git a/app/qtquick/log/logfeedlistmodel.h b/app/qtquick/log/logfeedlistmodel.h index 0c523379..53a8f4f3 100644 --- a/app/qtquick/log/logfeedlistmodel.h +++ b/app/qtquick/log/logfeedlistmodel.h @@ -55,6 +55,8 @@ class LogFeedListModel : public TimelineListModel private: QString m_selectCondition; LogFeedListModelFeedType m_feedType; + QString m_targetDid; + QString m_targetHandle; QString m_targetAvatar; }; diff --git a/app/qtquick/moderation/reporter.cpp b/app/qtquick/moderation/reporter.cpp index 1ba81cde..5b0cff33 100644 --- a/app/qtquick/moderation/reporter.cpp +++ b/app/qtquick/moderation/reporter.cpp @@ -1,6 +1,7 @@ #include "reporter.h" #include "extension/com/atproto/moderation/comatprotomoderationcreatereportex.h" +#include "tools/accountmanager.h" using AtProtocolInterface::ComAtprotoModerationCreateReportEx; @@ -14,15 +15,14 @@ Reporter::Reporter(QObject *parent) : QObject { parent }, m_running(false) m_reasonHash[ReasonMisleading] = "reasonMisleading"; } -void Reporter::setAccount(const QString &service, const QString &did, const QString &handle, - const QString &email, const QString &accessJwt, const QString &refreshJwt) +AtProtocolInterface::AccountData Reporter::account() const { - m_account.service = service; - m_account.did = did; - m_account.handle = handle; - m_account.email = email; - m_account.accessJwt = accessJwt; - m_account.refreshJwt = refreshJwt; + return AccountManager::getInstance()->getAccount(m_account.uuid); +} + +void Reporter::setAccount(const QString &uuid) +{ + m_account.uuid = uuid; } void Reporter::reportPost(const QString &uri, const QString &cid, const QString &text, @@ -42,7 +42,7 @@ void Reporter::reportPost(const QString &uri, const QString &cid, const QString emit finished(success); report->deleteLater(); }); - report->setAccount(m_account); + report->setAccount(account()); if (!labelers.isEmpty() && !labelers.first().isEmpty()) { report->appendRawHeader("atproto-proxy", labelers.first() + "#atproto_labeler"); } @@ -66,7 +66,7 @@ void Reporter::reportAccount(const QString &did, const QString &text, const QStr emit finished(success); report->deleteLater(); }); - report->setAccount(m_account); + report->setAccount(account()); if (!labelers.isEmpty() && !labelers.first().isEmpty()) { report->appendRawHeader("atproto-proxy", labelers.first() + "#atproto_labeler"); } @@ -91,7 +91,7 @@ void Reporter::reportMessage(const QString &did, const QString &convo_id, const emit finished(success); report->deleteLater(); }); - report->setAccount(m_account); + report->setAccount(account()); report->reportMessage(did, convo_id, message_id, text, m_reasonHash[reason]); } diff --git a/app/qtquick/moderation/reporter.h b/app/qtquick/moderation/reporter.h index f235d0a9..a25a2798 100644 --- a/app/qtquick/moderation/reporter.h +++ b/app/qtquick/moderation/reporter.h @@ -22,9 +22,8 @@ class Reporter : public QObject }; Q_ENUM(ReportReason) - Q_INVOKABLE void setAccount(const QString &service, const QString &did, const QString &handle, - const QString &email, const QString &accessJwt, - const QString &refreshJwt); + AtProtocolInterface::AccountData account() const; + Q_INVOKABLE void setAccount(const QString &uuid); Q_INVOKABLE void reportPost(const QString &uri, const QString &cid, const QString &text, const QStringList &labelers, Reporter::ReportReason reason); Q_INVOKABLE void reportAccount(const QString &did, const QString &text, diff --git a/app/qtquick/notification/notificationlistmodel.cpp b/app/qtquick/notification/notificationlistmodel.cpp index 4d331b11..ab9e6e34 100644 --- a/app/qtquick/notification/notificationlistmodel.cpp +++ b/app/qtquick/notification/notificationlistmodel.cpp @@ -768,8 +768,7 @@ bool NotificationListModel::repost(int row) setRunningRepost(row, false); ope->deleteLater(); }); - ope->setAccount(account().service, account().did, account().handle, account().email, - account().accessJwt, account().refreshJwt); + ope->setAccount(account().uuid); if (!current) ope->repost(item(row, CidRole).toString(), item(row, UriRole).toString()); else @@ -800,8 +799,7 @@ bool NotificationListModel::like(int row) setRunningLike(row, false); ope->deleteLater(); }); - ope->setAccount(account().service, account().did, account().handle, account().email, - account().accessJwt, account().refreshJwt); + ope->setAccount(account().uuid); if (!current) ope->like(item(row, CidRole).toString(), item(row, UriRole).toString()); else @@ -906,8 +904,7 @@ bool NotificationListModel::detachQuote(int row) } ope->deleteLater(); }); - ope->setAccount(account().service, account().did, account().handle, account().email, - account().accessJwt, account().refreshJwt); + ope->setAccount(account().uuid); ope->updateDetachedStatusOfQuote(detached, target_uri, detach_uri); return true; } diff --git a/app/qtquick/operation/recordoperator.cpp b/app/qtquick/operation/recordoperator.cpp index 26a197be..4330b677 100644 --- a/app/qtquick/operation/recordoperator.cpp +++ b/app/qtquick/operation/recordoperator.cpp @@ -8,6 +8,7 @@ #include "extension/com/atproto/repo/comatprotorepogetrecordex.h" #include "extension/com/atproto/repo/comatprotorepolistrecordsex.h" #include "extension/com/atproto/repo/comatprotorepoputrecordex.h" +#include "tools/accountmanager.h" #include @@ -31,16 +32,14 @@ RecordOperator::RecordOperator(QObject *parent) { } -void RecordOperator::setAccount(const QString &service, const QString &did, const QString &handle, - const QString &email, const QString &accessJwt, - const QString &refreshJwt) +AtProtocolInterface::AccountData RecordOperator::account() const { - m_account.service = service; - m_account.did = did; - m_account.handle = handle; - m_account.email = email; - m_account.accessJwt = accessJwt; - m_account.refreshJwt = refreshJwt; + return AccountManager::getInstance()->getAccount(m_account.uuid); +} + +void RecordOperator::setAccount(const QString &uuid) +{ + m_account.uuid = uuid; } void RecordOperator::setText(const QString &text) @@ -182,7 +181,7 @@ void RecordOperator::post() } LexiconsTypeUnknown::makeFacets( - this, m_account, m_text, + this, account(), m_text, [=](const QList &facets) { ComAtprotoRepoCreateRecordEx *create_record = new ComAtprotoRepoCreateRecordEx(this); @@ -234,7 +233,7 @@ void RecordOperator::post() } create_record->deleteLater(); }); - create_record->setAccount(m_account); + create_record->setAccount(account()); create_record->setReply(m_replyParent.cid, m_replyParent.uri, m_replyRoot.cid, m_replyRoot.uri); create_record->setQuote(m_embedQuote.cid, m_embedQuote.uri); @@ -289,7 +288,7 @@ void RecordOperator::repost(const QString &cid, const QString &uri) // 成功なら、受け取ったデータでTLデータの更新をしないと値が大きくならない create_record->deleteLater(); }); - create_record->setAccount(m_account); + create_record->setAccount(account()); create_record->repost(cid, uri); } @@ -313,7 +312,7 @@ void RecordOperator::like(const QString &cid, const QString &uri) // 成功なら、受け取ったデータでTLデータの更新をしないと値が大きくならない create_record->deleteLater(); }); - create_record->setAccount(m_account); + create_record->setAccount(account()); create_record->like(cid, uri); } @@ -335,7 +334,7 @@ void RecordOperator::follow(const QString &did) setRunning(false); create_record->deleteLater(); }); - create_record->setAccount(m_account); + create_record->setAccount(account()); create_record->follow(did); } @@ -358,7 +357,7 @@ void RecordOperator::mute(const QString &did) setRunning(false); mute->deleteLater(); }); - mute->setAccount(m_account); + mute->setAccount(account()); mute->muteActor(did); } @@ -380,7 +379,7 @@ void RecordOperator::block(const QString &did) setRunning(false); create_record->deleteLater(); }); - create_record->setAccount(m_account); + create_record->setAccount(account()); create_record->block(did); } @@ -402,7 +401,7 @@ void RecordOperator::blockList(const QString &uri) setRunning(false); create_record->deleteLater(); }); - create_record->setAccount(m_account); + create_record->setAccount(account()); create_record->blockList(uri); } @@ -427,7 +426,7 @@ bool RecordOperator::list(const QString &name, const RecordOperator::ListPurpose create_record->deleteLater(); }); create_record->setImageBlobs(m_embedImageBlobs); - create_record->setAccount(m_account); + create_record->setAccount(account()); create_record->list( name, static_cast( @@ -462,7 +461,7 @@ bool RecordOperator::listItem(const QString &uri, const QString &did) setRunning(false); create_record->deleteLater(); }); - create_record->setAccount(m_account); + create_record->setAccount(account()); create_record->listItem(uri, did); return true; } @@ -487,7 +486,7 @@ void RecordOperator::deletePost(const QString &uri) setRunning(false); delete_record->deleteLater(); }); - delete_record->setAccount(m_account); + delete_record->setAccount(account()); delete_record->deletePost(r_key); } @@ -511,7 +510,7 @@ void RecordOperator::deleteLike(const QString &uri) setRunning(false); delete_record->deleteLater(); }); - delete_record->setAccount(m_account); + delete_record->setAccount(account()); delete_record->deleteLike(r_key); } @@ -535,7 +534,7 @@ void RecordOperator::deleteRepost(const QString &uri) setRunning(false); delete_record->deleteLater(); }); - delete_record->setAccount(m_account); + delete_record->setAccount(account()); delete_record->deleteRepost(r_key); } @@ -559,7 +558,7 @@ void RecordOperator::deleteFollow(const QString &uri) setRunning(false); delete_record->deleteLater(); }); - delete_record->setAccount(m_account); + delete_record->setAccount(account()); delete_record->unfollow(r_key); } @@ -582,7 +581,7 @@ void RecordOperator::deleteMute(const QString &did) setRunning(false); unmute->deleteLater(); }); - unmute->setAccount(m_account); + unmute->setAccount(account()); unmute->unmuteActor(did); } @@ -606,7 +605,7 @@ void RecordOperator::deleteBlock(const QString &uri) setRunning(false); delete_record->deleteLater(); }); - delete_record->setAccount(m_account); + delete_record->setAccount(account()); delete_record->deleteBlock(r_key); } @@ -630,7 +629,7 @@ void RecordOperator::deleteBlockList(const QString &uri) setRunning(false); delete_record->deleteLater(); }); - delete_record->setAccount(m_account); + delete_record->setAccount(account()); delete_record->deleteBlockList(r_key); } @@ -662,7 +661,7 @@ bool RecordOperator::deleteList(const QString &uri) setRunning(false); delete_record->deleteLater(); }); - delete_record->setAccount(m_account); + delete_record->setAccount(account()); delete_record->deleteList(r_key); } else { setProgressMessage(QString()); @@ -713,7 +712,7 @@ bool RecordOperator::deleteListItem(const QString &uri) setRunning(false); delete_record->deleteLater(); }); - delete_record->setAccount(m_account); + delete_record->setAccount(account()); delete_record->deleteListItem(r_key); return true; } @@ -725,7 +724,7 @@ void RecordOperator::updateProfile(const QString &avatar_url, const QString &ban return; setRunning(true); - setProgressMessage(tr("Update profile ... (%1)").arg(m_account.handle)); + setProgressMessage(tr("Update profile ... (%1)").arg(account().handle)); QStringList images; QStringList alts; @@ -770,7 +769,7 @@ void RecordOperator::updateProfile(const QString &avatar_url, const QString &ban setRunning(false); new_profile->deleteLater(); }); - new_profile->setAccount(m_account); + new_profile->setAccount(account()); new_profile->profile(avatar, banner, description, display_name, old_record.pinnedPost, old_cid); } else { @@ -787,8 +786,8 @@ void RecordOperator::updateProfile(const QString &avatar_url, const QString &ban } old_profile->deleteLater(); }); - old_profile->setAccount(m_account); - old_profile->profile(m_account.did); + old_profile->setAccount(account()); + old_profile->profile(account().did); } void RecordOperator::updatePostPinning(const QString &post_uri, const QString &post_cid) @@ -797,7 +796,7 @@ void RecordOperator::updatePostPinning(const QString &post_uri, const QString &p return; setRunning(true); - setProgressMessage(tr("Update post pinning ... (%1)").arg(m_account.handle)); + setProgressMessage(tr("Update post pinning ... (%1)").arg(account().handle)); ComAtprotoRepoGetRecordEx *old_profile = new ComAtprotoRepoGetRecordEx(this); connect(old_profile, &ComAtprotoRepoGetRecordEx::finished, [=](bool success1) { @@ -819,7 +818,7 @@ void RecordOperator::updatePostPinning(const QString &post_uri, const QString &p ComAtprotoRepoStrongRef::Main pinned_post; pinned_post.uri = post_uri; pinned_post.cid = post_cid; - new_profile->setAccount(m_account); + new_profile->setAccount(account()); new_profile->profile(old_record.avatar, old_record.banner, old_record.description, old_record.displayName, pinned_post, old_cid); } else { @@ -830,8 +829,8 @@ void RecordOperator::updatePostPinning(const QString &post_uri, const QString &p } old_profile->deleteLater(); }); - old_profile->setAccount(m_account); - old_profile->profile(m_account.did); + old_profile->setAccount(account()); + old_profile->profile(account().did); } void RecordOperator::updateList(const QString &uri, const QString &avatar_url, @@ -877,7 +876,7 @@ void RecordOperator::updateList(const QString &uri, const QString &avatar_url, setRunning(false); new_list->deleteLater(); }); - new_list->setAccount(m_account); + new_list->setAccount(account()); new_list->list(avatar, old_record.purpose, description, name, r_key); } else { setProgressMessage(QString()); @@ -893,8 +892,8 @@ void RecordOperator::updateList(const QString &uri, const QString &avatar_url, } old_list->deleteLater(); }); - old_list->setAccount(m_account); - old_list->list(m_account.did, r_key); + old_list->setAccount(account()); + old_list->list(account().did, r_key); } void RecordOperator::updateThreadGate(const QString &uri, const QString &threadgate_uri, @@ -936,7 +935,7 @@ void RecordOperator::updateThreadGate(const QString &uri, const QString &threadg } delete_record->deleteLater(); }); - delete_record->setAccount(m_account); + delete_record->setAccount(account()); delete_record->deleteThreadGate(r_key); } @@ -971,13 +970,13 @@ void RecordOperator::updateQuoteEnabled(const QString &uri, bool enabled) emit finished(success2, put->uri(), put->cid()); put->deleteLater(); }); - put->setAccount(m_account); + put->setAccount(account()); put->postGate(uri, rule, old_record.detachedEmbeddingUris); record->deleteLater(); }); - record->setAccount(m_account); - record->postGate(m_account.did, target_rkey); + record->setAccount(account()); + record->postGate(account().did, target_rkey); } void RecordOperator::updateDetachedStatusOfQuote(bool detached, QString target_uri, @@ -1031,13 +1030,13 @@ void RecordOperator::updateDetachedStatusOfQuote(bool detached, QString target_u emit finished(success2, put->uri(), put->cid()); put->deleteLater(); }); - put->setAccount(m_account); + put->setAccount(account()); put->postGate(target_uri, rule, old_record.detachedEmbeddingUris); record->deleteLater(); }); - record->setAccount(m_account); - record->postGate(m_account.did, target_rkey); + record->setAccount(account()); + record->postGate(account().did, target_rkey); } void RecordOperator::requestPostGate(const QString &uri) @@ -1063,8 +1062,8 @@ void RecordOperator::requestPostGate(const QString &uri) emit finishedRequestPostGate(success, enabled, uris); record->deleteLater(); }); - record->setAccount(m_account); - record->postGate(m_account.did, rkey); + record->setAccount(account()); + record->postGate(account().did, rkey); } bool RecordOperator::running() const @@ -1122,7 +1121,7 @@ void RecordOperator::uploadBlob(std::function callback) } upload_blob->deleteLater(); }); - upload_blob->setAccount(m_account); + upload_blob->setAccount(account()); upload_blob->uploadBlob(path); } @@ -1165,8 +1164,8 @@ bool RecordOperator::getAllListItems(const QString &list_uri, std::functiondeleteLater(); }); - list->setAccount(m_account); - list->listListItems(m_account.did, cursor); + list->setAccount(account()); + list->listListItems(account().did, cursor); } void RecordOperator::deleteAllListItems(std::function callback) @@ -1190,7 +1189,7 @@ void RecordOperator::deleteAllListItems(std::function callback) } delete_record->deleteLater(); }); - delete_record->setAccount(m_account); + delete_record->setAccount(account()); delete_record->deleteListItem(r_key); } @@ -1239,7 +1238,7 @@ bool RecordOperator::threadGate( callback(success, create_record->uri(), create_record->cid()); create_record->deleteLater(); }); - create_record->setAccount(m_account); + create_record->setAccount(account()); create_record->threadGate(uri, type, rules); return true; } @@ -1270,7 +1269,7 @@ void RecordOperator::postGate(const QString &uri, callback(success, create_record->uri(), create_record->cid()); create_record->deleteLater(); }); - create_record->setAccount(m_account); + create_record->setAccount(account()); create_record->postGate(uri, type, m_postGateDetachedEmbeddingUris); } @@ -1286,3 +1285,8 @@ void RecordOperator::setProgressMessage(const QString &newProgressMessage) m_progressMessage = newProgressMessage; emit progressMessageChanged(); } + +QString RecordOperator::handle() const +{ + return account().handle; +} diff --git a/app/qtquick/operation/recordoperator.h b/app/qtquick/operation/recordoperator.h index c07e63f5..f6bf61e3 100644 --- a/app/qtquick/operation/recordoperator.h +++ b/app/qtquick/operation/recordoperator.h @@ -19,6 +19,7 @@ class RecordOperator : public QObject Q_PROPERTY(bool running READ running WRITE setRunning NOTIFY runningChanged) Q_PROPERTY(QString progressMessage READ progressMessage WRITE setProgressMessage NOTIFY progressMessageChanged FINAL) + Q_PROPERTY(QString handle READ handle CONSTANT) public: explicit RecordOperator(QObject *parent = nullptr); @@ -29,9 +30,8 @@ class RecordOperator : public QObject }; Q_ENUM(ListPurpose); - Q_INVOKABLE void setAccount(const QString &service, const QString &did, const QString &handle, - const QString &email, const QString &accessJwt, - const QString &refreshJwt); + AtProtocolInterface::AccountData account() const; + Q_INVOKABLE void setAccount(const QString &uuid); Q_INVOKABLE void setText(const QString &text); Q_INVOKABLE void setReply(const QString &parent_cid, const QString &parent_uri, const QString &root_cid, const QString &root_uri); @@ -84,9 +84,9 @@ class RecordOperator : public QObject bool running() const; void setRunning(bool newRunning); - QString progressMessage() const; void setProgressMessage(const QString &newProgressMessage); + QString handle() const; signals: void errorOccured(const QString &code, const QString &message); diff --git a/app/qtquick/operation/translator.cpp b/app/qtquick/operation/translator.cpp index dea6f7bc..fce01c86 100644 --- a/app/qtquick/operation/translator.cpp +++ b/app/qtquick/operation/translator.cpp @@ -1,5 +1,5 @@ #include "translator.h" -#include "encryption.h" +#include "tools/encryption.h" #include #include diff --git a/app/qtquick/profile/userprofile.cpp b/app/qtquick/profile/userprofile.cpp index 123ac5fe..50b85573 100644 --- a/app/qtquick/profile/userprofile.cpp +++ b/app/qtquick/profile/userprofile.cpp @@ -6,6 +6,7 @@ #include "extension/directory/plc/directoryplc.h" #include "extension/directory/plc/directoryplclogaudit.h" #include "tools/labelerprovider.h" +#include "tools/accountmanager.h" #include @@ -38,16 +39,10 @@ UserProfile::~UserProfile() &UserProfile::updatedBelongingLists); } -void UserProfile::setAccount(const QString &service, const QString &did, const QString &handle, - const QString &email, const QString &accessJwt, - const QString &refreshJwt) +void UserProfile::setAccount(const QString &uuid) { - m_account.service = service; - m_account.did = did; - m_account.handle = handle; - m_account.email = email; - m_account.accessJwt = accessJwt; - m_account.refreshJwt = refreshJwt; + m_account.uuid = uuid; + m_account = AccountManager::getInstance()->getAccount(m_account.uuid); } void UserProfile::getProfile(const QString &did) @@ -118,7 +113,7 @@ void UserProfile::getProfile(const QString &did) } profile->deleteLater(); }); - profile->setAccount(m_account); + profile->setAccount(AccountManager::getInstance()->getAccount(m_account.uuid)); profile->setLabelers(labelerDids()); profile->getProfile(did); }); @@ -367,7 +362,8 @@ QString UserProfile::formattedDescription() const void UserProfile::updatedBelongingLists(const QString &account_did, const QString &user_did) { - if (m_account.did == account_did && did() == user_did) { + if (AccountManager::getInstance()->getAccount(m_account.uuid).did == account_did + && did() == user_did) { setBelongingLists(ListItemsCache::getInstance()->getListNames(account_did, user_did)); } } @@ -382,8 +378,9 @@ void UserProfile::updateContentFilterLabels(std::function callback) callback(); connector->deleteLater(); }); - provider->setAccount(m_account); - provider->update(m_account, connector, LabelerProvider::RefleshAuto); + provider->setAccount(AccountManager::getInstance()->getAccount(m_account.uuid)); + provider->update(AccountManager::getInstance()->getAccount(m_account.uuid), connector, + LabelerProvider::RefleshAuto); } void UserProfile::getServiceEndpoint(const QString &did, @@ -493,12 +490,15 @@ void UserProfile::getRawProfile() QString UserProfile::labelsTitle(const QString &label, const bool for_image, const QString &labeler_did) const { - return LabelerProvider::getInstance()->title(m_account, label, for_image, labeler_did); + return LabelerProvider::getInstance()->title( + AccountManager::getInstance()->getAccount(m_account.uuid), label, for_image, + labeler_did); } QStringList UserProfile::labelerDids() const { - return LabelerProvider::getInstance()->labelerDids(m_account); + return LabelerProvider::getInstance()->labelerDids( + AccountManager::getInstance()->getAccount(m_account.uuid)); } QStringList UserProfile::labels() const diff --git a/app/qtquick/profile/userprofile.h b/app/qtquick/profile/userprofile.h index 4091364d..07c4007e 100644 --- a/app/qtquick/profile/userprofile.h +++ b/app/qtquick/profile/userprofile.h @@ -62,9 +62,7 @@ class UserProfile : public QObject explicit UserProfile(QObject *parent = nullptr); ~UserProfile(); - Q_INVOKABLE void setAccount(const QString &service, const QString &did, const QString &handle, - const QString &email, const QString &accessJwt, - const QString &refreshJwt); + Q_INVOKABLE void setAccount(const QString &uuid); Q_INVOKABLE void getProfile(const QString &did); bool running() const; diff --git a/app/qtquick/qtquick.pri b/app/qtquick/qtquick.pri index e7ed1942..cce03fdd 100644 --- a/app/qtquick/qtquick.pri +++ b/app/qtquick/qtquick.pri @@ -14,7 +14,6 @@ SOURCES += \ $$PWD/controls/calendartablemodel.cpp \ $$PWD/controls/embedimagelistmodel.cpp \ $$PWD/controls/languagelistmodel.cpp \ - $$PWD/encryption.cpp \ $$PWD/feedgenerator/actorfeedgeneratorlistmodel.cpp \ $$PWD/feedgenerator/feedgeneratorlistmodel.cpp \ $$PWD/link/externallink.cpp \ @@ -67,12 +66,9 @@ HEADERS += \ $$PWD/chat/chatmessagelistmodel.h \ $$PWD/column/columnlistmodel.h \ $$PWD/column/feedtypelistmodel.h \ - $$PWD/common.h \ $$PWD/controls/calendartablemodel.h \ $$PWD/controls/embedimagelistmodel.h \ $$PWD/controls/languagelistmodel.h \ - $$PWD/encryption.h \ - $$PWD/encryption_seed.h \ $$PWD/feedgenerator/actorfeedgeneratorlistmodel.h \ $$PWD/feedgenerator/feedgeneratorlistmodel.h \ $$PWD/link/externallink.h \ diff --git a/app/qtquick/timeline/customfeedlistmodel.cpp b/app/qtquick/timeline/customfeedlistmodel.cpp index 2828d8d2..d1adf913 100644 --- a/app/qtquick/timeline/customfeedlistmodel.cpp +++ b/app/qtquick/timeline/customfeedlistmodel.cpp @@ -52,9 +52,9 @@ void CustomFeedListModel::updateFeedSaveStatus() connect(pref, &AppBskyActorGetPreferences::finished, [=](bool success) { if (success) { bool exist = false; - for (const auto &feed : pref->preferences().savedFeedsPref) { - for (const auto &saved : feed.saved) { - if (saved == uri()) { + for (const auto &prefs : pref->preferences().savedFeedsPrefV2) { + for (const auto &item : prefs.items) { + if (item.type == "feed" && item.value == uri()) { exist = true; break; } @@ -74,8 +74,7 @@ void CustomFeedListModel::saveGenerator() { if (uri().isEmpty()) return; - m_feedGeneratorListModel.setAccount(account().service, account().did, account().handle, - account().email, account().accessJwt, account().refreshJwt); + m_feedGeneratorListModel.setAccount(account().uuid); m_feedGeneratorListModel.saveGenerator(uri()); } @@ -83,8 +82,7 @@ void CustomFeedListModel::removeGenerator() { if (uri().isEmpty()) return; - m_feedGeneratorListModel.setAccount(account().service, account().did, account().handle, - account().email, account().accessJwt, account().refreshJwt); + m_feedGeneratorListModel.setAccount(account().uuid); m_feedGeneratorListModel.removeGenerator(uri()); } diff --git a/app/qtquick/timeline/timelinelistmodel.cpp b/app/qtquick/timeline/timelinelistmodel.cpp index f4e307b9..0e9e9d35 100644 --- a/app/qtquick/timeline/timelinelistmodel.cpp +++ b/app/qtquick/timeline/timelinelistmodel.cpp @@ -535,8 +535,7 @@ bool TimelineListModel::deletePost(int row) setRunningdeletePost(row, false); ope->deleteLater(); }); - ope->setAccount(account().service, account().did, account().handle, account().email, - account().accessJwt, account().refreshJwt); + ope->setAccount(account().uuid); ope->deletePost(item(row, UriRole).toString()); return true; @@ -564,8 +563,7 @@ bool TimelineListModel::repost(int row) setRunningRepost(row, false); ope->deleteLater(); }); - ope->setAccount(account().service, account().did, account().handle, account().email, - account().accessJwt, account().refreshJwt); + ope->setAccount(account().uuid); if (!current) ope->repost(item(row, CidRole).toString(), item(row, UriRole).toString()); else @@ -597,8 +595,7 @@ bool TimelineListModel::like(int row) setRunningLike(row, false); ope->deleteLater(); }); - ope->setAccount(account().service, account().did, account().handle, account().email, - account().accessJwt, account().refreshJwt); + ope->setAccount(account().uuid); if (!current) ope->like(item(row, CidRole).toString(), item(row, UriRole).toString()); else @@ -642,8 +639,7 @@ bool TimelineListModel::pin(int row) setRunningPostPinning(row, false); ope->deleteLater(); }); - ope->setAccount(account().service, account().did, account().handle, account().email, - account().accessJwt, account().refreshJwt); + ope->setAccount(account().uuid); ope->updatePostPinning(pin_uri, pin_cid); return true; @@ -742,8 +738,7 @@ bool TimelineListModel::detachQuote(int row) } ope->deleteLater(); }); - ope->setAccount(account().service, account().did, account().handle, account().email, - account().accessJwt, account().refreshJwt); + ope->setAccount(account().uuid); ope->updateDetachedStatusOfQuote(detached, target_uri, detach_uri); return true; } diff --git a/app/qtquick/timeline/userpost.cpp b/app/qtquick/timeline/userpost.cpp index fd16fa0b..efbf814e 100644 --- a/app/qtquick/timeline/userpost.cpp +++ b/app/qtquick/timeline/userpost.cpp @@ -3,6 +3,7 @@ #include "atprotocol/app/bsky/feed/appbskyfeedgetposts.h" #include "atprotocol/app/bsky/actor/appbskyactorgetprofile.h" #include "atprotocol/lexicons_func_unknown.h" +#include "tools/accountmanager.h" #include "tools/labelerprovider.h" using AtProtocolInterface::AppBskyActorGetProfile; @@ -11,16 +12,10 @@ using namespace AtProtocolType; UserPost::UserPost(QObject *parent) : QObject { parent }, m_running(false), m_authorMuted(false) { } -void UserPost::setAccount(const QString &service, const QString &did, const QString &handle, - const QString &email, const QString &accessJwt, const QString &refreshJwt) +void UserPost::setAccount(const QString &uuid) { - qDebug().noquote() << this << service << handle << accessJwt; - m_account.service = service; - m_account.did = did; - m_account.handle = handle; - m_account.email = email; - m_account.accessJwt = accessJwt; - m_account.refreshJwt = refreshJwt; + qDebug().noquote() << this << uuid; + m_account.uuid = uuid; } void UserPost::getPost(const QString &uri) @@ -69,7 +64,7 @@ void UserPost::getPost(const QString &uri) setRunning(false); posts->deleteLater(); }); - posts->setAccount(m_account); + posts->setAccount(AccountManager::getInstance()->getAccount(m_account.uuid)); posts->setLabelers(labelerDids()); posts->getPosts(QStringList() << at_uri); }); @@ -302,7 +297,7 @@ void UserPost::convertToAtUri(const QString &base_at_uri, const QString &uri, } profile->deleteLater(); }); - profile->setAccount(m_account); + profile->setAccount(AccountManager::getInstance()->getAccount(m_account.uuid)); profile->getProfile(user_id); } } diff --git a/app/qtquick/timeline/userpost.h b/app/qtquick/timeline/userpost.h index a3488931..da2760f7 100644 --- a/app/qtquick/timeline/userpost.h +++ b/app/qtquick/timeline/userpost.h @@ -39,9 +39,7 @@ class UserPost : public QObject public: explicit UserPost(QObject *parent = nullptr); - Q_INVOKABLE void setAccount(const QString &service, const QString &did, const QString &handle, - const QString &email, const QString &accessJwt, - const QString &refreshJwt); + Q_INVOKABLE void setAccount(const QString &uuid); Q_INVOKABLE void getPost(const QString &uri); Q_INVOKABLE void clear(); diff --git a/lib/atprotocol/accessatprotocol.cpp b/lib/atprotocol/accessatprotocol.cpp index bd8bfb17..bd568547 100644 --- a/lib/atprotocol/accessatprotocol.cpp +++ b/lib/atprotocol/accessatprotocol.cpp @@ -107,7 +107,8 @@ QString AtProtocolAccount::refreshJwt() const return m_account.refreshJwt; } -AccessAtProtocol::AccessAtProtocol(QObject *parent) : AtProtocolAccount { parent } +AccessAtProtocol::AccessAtProtocol(QObject *parent) + : AtProtocolAccount { parent }, m_contentType("application/json") { qDebug().noquote() << LOG_DATETIME << "AccessAtProtocol::AccessAtProtocol()" << this; if (m_manager == nullptr) { @@ -134,7 +135,12 @@ void AccessAtProtocol::get(const QString &endpoint, const QUrlQuery &query, qDebug().noquote() << LOG_DATETIME << " " << endpoint; qDebug().noquote() << LOG_DATETIME << " " << query.toString(); - QUrl url = QString("%1/%2").arg(service(), endpoint); + QUrl url; + if (endpoint.isEmpty()) { + url = service(); + } else { + url = QString("%1/%2").arg(service(), endpoint); + } url.setQuery(query); QNetworkRequest request(url); request.setRawHeader(QByteArray("Cache-Control"), QByteArray("no-cache")); @@ -191,9 +197,15 @@ void AccessAtProtocol::post(const QString &endpoint, const QByteArray &json, qDebug().noquote() << LOG_DATETIME << " " << endpoint; qDebug().noquote() << LOG_DATETIME << " " << json; - QNetworkRequest request(QUrl(QString("%1/%2").arg(service(), endpoint))); + QUrl url; + if (endpoint.isEmpty()) { + url = service(); + } else { + url = QString("%1/%2").arg(service(), endpoint); + } + QNetworkRequest request(url); request.setRawHeader(QByteArray("Cache-Control"), QByteArray("no-cache")); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setHeader(QNetworkRequest::ContentTypeHeader, m_contentType); if (with_auth_header) { if (accessJwt().isEmpty()) { qCritical() << LOG_DATETIME << "AccessAtProtocol::post()" @@ -294,19 +306,21 @@ bool AccessAtProtocol::checkReply(HttpReply *reply) m_errorCode.clear(); m_errorMessage.clear(); -#ifdef QT_DEBUG + QByteArray header_key; for (const auto &header : reply->rawHeaderPairs()) { - if (header.first.toLower().startsWith("ratelimit-")) { - if (header.first.toLower() == "ratelimit-reset") { + header_key = header.first.toLower(); + if (header_key.startsWith("ratelimit-")) { + if (header_key == "ratelimit-reset") { qDebug().noquote() << LOG_DATETIME << header.first << QDateTime::fromSecsSinceEpoch(header.second.toInt()) .toString("yyyy/MM/dd hh:mm:ss"); } else { qDebug().noquote() << LOG_DATETIME << header.first << header.second; } + } else if (header_key == "dpop-nonce") { + m_dPopNonce = header.second; } } -#endif QJsonDocument json_doc = QJsonDocument::fromJson(m_replyJson.toUtf8()); if (reply->error() != HttpReply::Success) { @@ -431,6 +445,16 @@ void AccessAtProtocol::setAdditionalRawHeader(QNetworkRequest &request) } } +QString AccessAtProtocol::dPopNonce() const +{ + return m_dPopNonce; +} + +void AccessAtProtocol::setContentType(const QString &newContentType) +{ + m_contentType = newContentType; +} + QString AccessAtProtocol::cursor() const { return m_cursor; diff --git a/lib/atprotocol/accessatprotocol.h b/lib/atprotocol/accessatprotocol.h index 8aa3eb1c..ed662b16 100644 --- a/lib/atprotocol/accessatprotocol.h +++ b/lib/atprotocol/accessatprotocol.h @@ -89,6 +89,9 @@ class AccessAtProtocol : public AtProtocolAccount void setCursor(const QString &newCursor); void appendRawHeader(const QString &name, const QString &value); + void setContentType(const QString &newContentType); + QString dPopNonce() const; + signals: void finished(bool success); @@ -126,7 +129,9 @@ public slots: QString m_errorMessage; QString m_cursor; + QString m_contentType; QHash m_additionalRawHeaders; + QString m_dPopNonce; }; } diff --git a/lib/atprotocol/com/atproto/repo/comatprotorepodescriberepo.cpp b/lib/atprotocol/com/atproto/repo/comatprotorepodescriberepo.cpp new file mode 100644 index 00000000..18c27f5c --- /dev/null +++ b/lib/atprotocol/com/atproto/repo/comatprotorepodescriberepo.cpp @@ -0,0 +1,71 @@ +#include "comatprotorepodescriberepo.h" +#include "atprotocol/lexicons_func.h" +#include "atprotocol/lexicons_func_unknown.h" + +#include +#include +#include + +namespace AtProtocolInterface { + +ComAtprotoRepoDescribeRepo::ComAtprotoRepoDescribeRepo(QObject *parent) + : AccessAtProtocol { parent } +{ +} + +void ComAtprotoRepoDescribeRepo::describeRepo(const QString &repo) +{ + QUrlQuery url_query; + if (!repo.isEmpty()) { + url_query.addQueryItem(QStringLiteral("repo"), repo); + } + + get(QStringLiteral("xrpc/com.atproto.repo.describeRepo"), url_query, false); +} + +const QString &ComAtprotoRepoDescribeRepo::handle() const +{ + return m_handle; +} + +const QString &ComAtprotoRepoDescribeRepo::did() const +{ + return m_did; +} + +const QVariant &ComAtprotoRepoDescribeRepo::didDoc() const +{ + return m_didDoc; +} + +const QStringList &ComAtprotoRepoDescribeRepo::collectionsList() const +{ + return m_collectionsList; +} + +const bool &ComAtprotoRepoDescribeRepo::handleIsCorrect() const +{ + return m_handleIsCorrect; +} + +bool ComAtprotoRepoDescribeRepo::parseJson(bool success, const QString reply_json) +{ + QJsonDocument json_doc = QJsonDocument::fromJson(reply_json.toUtf8()); + if (json_doc.isEmpty()) { + success = false; + } else { + AtProtocolType::LexiconsTypeUnknown::copyString(json_doc.object().value("handle"), + m_handle); + AtProtocolType::LexiconsTypeUnknown::copyString(json_doc.object().value("did"), m_did); + AtProtocolType::LexiconsTypeUnknown::copyUnknown( + json_doc.object().value("didDoc").toObject(), m_didDoc); + AtProtocolType::LexiconsTypeUnknown::copyStringList( + json_doc.object().value("collections").toArray(), m_collectionsList); + AtProtocolType::LexiconsTypeUnknown::copyBool(json_doc.object().value("handleIsCorrect"), + m_handleIsCorrect); + } + + return success; +} + +} diff --git a/lib/atprotocol/com/atproto/repo/comatprotorepodescriberepo.h b/lib/atprotocol/com/atproto/repo/comatprotorepodescriberepo.h new file mode 100644 index 00000000..717cb12b --- /dev/null +++ b/lib/atprotocol/com/atproto/repo/comatprotorepodescriberepo.h @@ -0,0 +1,33 @@ +#ifndef COMATPROTOREPODESCRIBEREPO_H +#define COMATPROTOREPODESCRIBEREPO_H + +#include "atprotocol/accessatprotocol.h" + +namespace AtProtocolInterface { + +class ComAtprotoRepoDescribeRepo : public AccessAtProtocol +{ +public: + explicit ComAtprotoRepoDescribeRepo(QObject *parent = nullptr); + + void describeRepo(const QString &repo); + + const QString &handle() const; + const QString &did() const; + const QVariant &didDoc() const; + const QStringList &collectionsList() const; + const bool &handleIsCorrect() const; + +private: + virtual bool parseJson(bool success, const QString reply_json); + + QString m_handle; + QString m_did; + QVariant m_didDoc; + QStringList m_collectionsList; + bool m_handleIsCorrect; +}; + +} + +#endif // COMATPROTOREPODESCRIBEREPO_H diff --git a/lib/atprotocol/lexicons.h b/lib/atprotocol/lexicons.h index 08918ae5..e85fe0d8 100644 --- a/lib/atprotocol/lexicons.h +++ b/lib/atprotocol/lexicons.h @@ -990,7 +990,8 @@ struct Main tags; // Additional hashtags, in addition to any included in post text and facets. QString createdAt; // datetime , Client-declared timestamp when this post was originally // created. - QString via; // client name(Unofficial field) + QString via; // client name(Unofficial field) old + QString space_aoisora_post_via; // client name(Unofficial field) }; } @@ -1981,6 +1982,24 @@ struct Member }; } +// blue.linkat.defs +namespace BlueLinkatDefs { +struct Card +{ + QString url; // URL of the link + QString text; // Text of the card + QString emoji; // Emoji of the card +}; +} + +// blue.linkat.board +namespace BlueLinkatBoard { +struct Main +{ + QList cards; +}; +} + // com.whtwnd.blog.defs namespace ComWhtwndBlogDefs { struct BlogEntry @@ -2103,6 +2122,55 @@ struct PlcAuditLogDetail typedef QList PlcAuditLog; } +// oauth.defs +namespace OauthDefs { +struct PushedAuthorizationResponse +{ + QString request_uri; + int expires_in = 0; +}; +struct TokenResponse +{ + QString access_token; + QString token_type; + QString refresh_token; + QString scope; + QString sub; + int expires_in = 0; +}; +} + +// wellKnown.defs +namespace WellKnownDefs { +struct ResourceMetadata +{ + QString resource; + QList authorization_servers; + QList scopes_supported; + QList bearer_methods_supported; + QString resource_documentation; +}; +struct ServerMetadata +{ + QString issuer; + QList response_types_supported; + QList grant_types_supported; + QList code_challenge_methods_supported; + QList token_endpoint_auth_methods_supported; + QList token_endpoint_auth_signing_alg_values_supported; + QList scopes_supported; + QList subject_types_supported; + bool authorization_response_iss_parameter_supported = false; + QString pushed_authorization_request_endpoint; + QString token_endpoint; + bool require_pushed_authorization_requests = false; + QList dpop_signing_alg_values_supported; + bool require_request_uri_registration = false; + bool client_id_metadata_document_supported = false; + QString authorization_endpoint; +}; +} + } Q_DECLARE_METATYPE(AtProtocolType::AppBskyFeedPost::Main) Q_DECLARE_METATYPE(AtProtocolType::AppBskyFeedLike::Main) @@ -2113,5 +2181,7 @@ Q_DECLARE_METATYPE(AtProtocolType::AppBskyGraphList::Main) Q_DECLARE_METATYPE(AtProtocolType::AppBskyFeedThreadgate::Main) Q_DECLARE_METATYPE(AtProtocolType::AppBskyFeedPostgate::Main) Q_DECLARE_METATYPE(AtProtocolType::ComWhtwndBlogEntry::Main) +Q_DECLARE_METATYPE(AtProtocolType::BlueLinkatBoard::Main) +Q_DECLARE_METATYPE(AtProtocolType::DirectoryPlcDefs::DidDoc) #endif // LEXICONS_H diff --git a/lib/atprotocol/lexicons_func.cpp b/lib/atprotocol/lexicons_func.cpp index 43ca6f22..85825db7 100644 --- a/lib/atprotocol/lexicons_func.cpp +++ b/lib/atprotocol/lexicons_func.cpp @@ -1428,6 +1428,7 @@ void copyMain(const QJsonObject &src, AppBskyFeedPost::Main &dest) } dest.createdAt = src.value("createdAt").toString(); dest.via = src.value("via").toString(); + dest.space_aoisora_post_via = src.value("space.aoisora.post.via").toString(); } } } @@ -2799,6 +2800,30 @@ void copyMember(const QJsonObject &src, ToolsOzoneTeamDefs::Member &dest) } } } +// blue.linkat.defs +namespace BlueLinkatDefs { +void copyCard(const QJsonObject &src, BlueLinkatDefs::Card &dest) +{ + if (!src.isEmpty()) { + dest.url = src.value("url").toString(); + dest.text = src.value("text").toString(); + dest.emoji = src.value("emoji").toString(); + } +} +} +// blue.linkat.board +namespace BlueLinkatBoard { +void copyMain(const QJsonObject &src, BlueLinkatBoard::Main &dest) +{ + if (!src.isEmpty()) { + for (const auto &s : src.value("cards").toArray()) { + BlueLinkatDefs::Card child; + BlueLinkatDefs::copyCard(s.toObject(), child); + dest.cards.append(child); + } + } +} +} // com.whtwnd.blog.defs namespace ComWhtwndBlogDefs { void copyBlogEntry(const QJsonObject &src, ComWhtwndBlogDefs::BlogEntry &dest) @@ -2977,6 +3002,90 @@ void copyPlcAuditLog(const QJsonArray &src, DirectoryPlcDefs::PlcAuditLog &dest) } } } +// oauth.defs +namespace OauthDefs { +void copyPushedAuthorizationResponse(const QJsonObject &src, + OauthDefs::PushedAuthorizationResponse &dest) +{ + if (!src.isEmpty()) { + dest.request_uri = src.value("request_uri").toString(); + dest.expires_in = src.value("expires_in").toInt(); + } +} +void copyTokenResponse(const QJsonObject &src, OauthDefs::TokenResponse &dest) +{ + if (!src.isEmpty()) { + dest.access_token = src.value("access_token").toString(); + dest.token_type = src.value("token_type").toString(); + dest.refresh_token = src.value("refresh_token").toString(); + dest.scope = src.value("scope").toString(); + dest.sub = src.value("sub").toString(); + dest.expires_in = src.value("expires_in").toInt(); + } +} +} +// wellKnown.defs +namespace WellKnownDefs { +void copyResourceMetadata(const QJsonObject &src, WellKnownDefs::ResourceMetadata &dest) +{ + if (!src.isEmpty()) { + dest.resource = src.value("resource").toString(); + for (const auto &value : src.value("authorization_servers").toArray()) { + dest.authorization_servers.append(value.toString()); + } + for (const auto &value : src.value("scopes_supported").toArray()) { + dest.scopes_supported.append(value.toString()); + } + for (const auto &value : src.value("bearer_methods_supported").toArray()) { + dest.bearer_methods_supported.append(value.toString()); + } + dest.resource_documentation = src.value("resource_documentation").toString(); + } +} +void copyServerMetadata(const QJsonObject &src, WellKnownDefs::ServerMetadata &dest) +{ + if (!src.isEmpty()) { + dest.issuer = src.value("issuer").toString(); + for (const auto &value : src.value("response_types_supported").toArray()) { + dest.response_types_supported.append(value.toString()); + } + for (const auto &value : src.value("grant_types_supported").toArray()) { + dest.grant_types_supported.append(value.toString()); + } + for (const auto &value : src.value("code_challenge_methods_supported").toArray()) { + dest.code_challenge_methods_supported.append(value.toString()); + } + for (const auto &value : src.value("token_endpoint_auth_methods_supported").toArray()) { + dest.token_endpoint_auth_methods_supported.append(value.toString()); + } + for (const auto &value : + src.value("token_endpoint_auth_signing_alg_values_supported").toArray()) { + dest.token_endpoint_auth_signing_alg_values_supported.append(value.toString()); + } + for (const auto &value : src.value("scopes_supported").toArray()) { + dest.scopes_supported.append(value.toString()); + } + for (const auto &value : src.value("subject_types_supported").toArray()) { + dest.subject_types_supported.append(value.toString()); + } + dest.authorization_response_iss_parameter_supported = + src.value("authorization_response_iss_parameter_supported").toBool(); + dest.pushed_authorization_request_endpoint = + src.value("pushed_authorization_request_endpoint").toString(); + dest.token_endpoint = src.value("token_endpoint").toString(); + dest.require_pushed_authorization_requests = + src.value("require_pushed_authorization_requests").toBool(); + for (const auto &value : src.value("dpop_signing_alg_values_supported").toArray()) { + dest.dpop_signing_alg_values_supported.append(value.toString()); + } + dest.require_request_uri_registration = + src.value("require_request_uri_registration").toBool(); + dest.client_id_metadata_document_supported = + src.value("client_id_metadata_document_supported").toBool(); + dest.authorization_endpoint = src.value("authorization_endpoint").toString(); + } +} +} } diff --git a/lib/atprotocol/lexicons_func.h b/lib/atprotocol/lexicons_func.h index 5ba028dd..0465cb28 100644 --- a/lib/atprotocol/lexicons_func.h +++ b/lib/atprotocol/lexicons_func.h @@ -395,6 +395,14 @@ void copyViewerConfig(const QJsonObject &src, ToolsOzoneServerGetConfig::ViewerC namespace ToolsOzoneTeamDefs { void copyMember(const QJsonObject &src, ToolsOzoneTeamDefs::Member &dest); } +// blue.linkat.defs +namespace BlueLinkatDefs { +void copyCard(const QJsonObject &src, BlueLinkatDefs::Card &dest); +} +// blue.linkat.board +namespace BlueLinkatBoard { +void copyMain(const QJsonObject &src, BlueLinkatBoard::Main &dest); +} // com.whtwnd.blog.defs namespace ComWhtwndBlogDefs { void copyBlogEntry(const QJsonObject &src, ComWhtwndBlogDefs::BlogEntry &dest); @@ -422,6 +430,17 @@ void copyCreate(const QJsonObject &src, DirectoryPlcDefs::Create &dest); void copyPlcAuditLogDetail(const QJsonObject &src, DirectoryPlcDefs::PlcAuditLogDetail &dest); void copyPlcAuditLog(const QJsonArray &src, DirectoryPlcDefs::PlcAuditLog &dest); } +// oauth.defs +namespace OauthDefs { +void copyPushedAuthorizationResponse(const QJsonObject &src, + OauthDefs::PushedAuthorizationResponse &dest); +void copyTokenResponse(const QJsonObject &src, OauthDefs::TokenResponse &dest); +} +// wellKnown.defs +namespace WellKnownDefs { +void copyResourceMetadata(const QJsonObject &src, WellKnownDefs::ResourceMetadata &dest); +void copyServerMetadata(const QJsonObject &src, WellKnownDefs::ServerMetadata &dest); +} } diff --git a/lib/atprotocol/lexicons_func_unknown.cpp b/lib/atprotocol/lexicons_func_unknown.cpp index 0e290c1f..ee8320cc 100644 --- a/lib/atprotocol/lexicons_func_unknown.cpp +++ b/lib/atprotocol/lexicons_func_unknown.cpp @@ -22,6 +22,12 @@ void copyUnknown(const QJsonObject &src, QVariant &dest) return; QString type = src.value("$type").toString(); + QStringList context; + if (src.contains("@context")) { + for (const auto item : src.value("@context").toArray()) { + context.append(item.toString()); + } + } if (type == QStringLiteral("app.bsky.feed.post")) { AppBskyFeedPost::Main record; AppBskyFeedPost::copyMain(src, record); @@ -58,6 +64,14 @@ void copyUnknown(const QJsonObject &src, QVariant &dest) ComWhtwndBlogEntry::Main record; ComWhtwndBlogEntry::copyMain(src, record); dest.setValue(record); + } else if (type == QStringLiteral("blue.linkat.board")) { + BlueLinkatBoard::Main record; + BlueLinkatBoard::copyMain(src, record); + dest.setValue(record); + } else if (context.contains("https://www.w3.org/ns/did/v1")) { + DirectoryPlcDefs::DidDoc doc; + DirectoryPlcDefs::copyDidDoc(src, doc); + dest.setValue(doc); } } diff --git a/app/qtquick/common.h b/lib/common.h similarity index 95% rename from app/qtquick/common.h rename to lib/common.h index 3fda90bd..331db3c3 100644 --- a/app/qtquick/common.h +++ b/lib/common.h @@ -16,9 +16,7 @@ inline QString appDataFolder() .arg(QCoreApplication::organizationName()) .arg(QCoreApplication::applicationName()) .arg( -#if defined(HAGOROMO_UNIT_TEST) - QStringLiteral("_unittest") -#elif defined(QT_DEBUG) +#ifdef QT_DEBUG QStringLiteral("_debug") #else QString() diff --git a/lib/extension/com/atproto/repo/comatprotorepocreaterecordex.cpp b/lib/extension/com/atproto/repo/comatprotorepocreaterecordex.cpp index fcc359c2..2d251a6d 100644 --- a/lib/extension/com/atproto/repo/comatprotorepocreaterecordex.cpp +++ b/lib/extension/com/atproto/repo/comatprotorepocreaterecordex.cpp @@ -28,7 +28,7 @@ void ComAtprotoRepoCreateRecordEx::post(const QString &text) QJsonObject json_record; json_record.insert("text", text); json_record.insert("createdAt", QDateTime::currentDateTimeUtc().toString(Qt::ISODateWithMs)); - json_record.insert("via", "Hagoromo"); + json_record.insert("space.aoisora.post.via", "Hagoromo"); if (!m_postLanguages.isEmpty()) { QJsonArray json_langs; for (const auto &lang : qAsConst(m_postLanguages)) { diff --git a/lib/extension/com/atproto/repo/comatprotorepolistrecordsex.cpp b/lib/extension/com/atproto/repo/comatprotorepolistrecordsex.cpp index 17c0e35d..f0df81f8 100644 --- a/lib/extension/com/atproto/repo/comatprotorepolistrecordsex.cpp +++ b/lib/extension/com/atproto/repo/comatprotorepolistrecordsex.cpp @@ -27,6 +27,12 @@ void ComAtprotoRepoListRecordsEx::listWhiteWindItems(const QString &repo, const listRecords(repo, "com.whtwnd.blog.entry", 10, cursor, false); } +void ComAtprotoRepoListRecordsEx::listLinkatItems(const QString &repo, const QString &cursor) +{ + Q_UNUSED(cursor) + listRecords(repo, "blue.linkat.board", 1, QString(), false); +} + bool ComAtprotoRepoListRecordsEx::parseJson(bool success, const QString reply_json) { success = ComAtprotoRepoListRecords::parseJson(success, reply_json); diff --git a/lib/extension/com/atproto/repo/comatprotorepolistrecordsex.h b/lib/extension/com/atproto/repo/comatprotorepolistrecordsex.h index 98acc449..e45170df 100644 --- a/lib/extension/com/atproto/repo/comatprotorepolistrecordsex.h +++ b/lib/extension/com/atproto/repo/comatprotorepolistrecordsex.h @@ -14,6 +14,7 @@ class ComAtprotoRepoListRecordsEx : public ComAtprotoRepoListRecords void listReposts(const QString &repo, const QString &cursor); void listListItems(const QString &repo, const QString &cursor); void listWhiteWindItems(const QString &repo, const QString &cursor); + void listLinkatItems(const QString &repo, const QString &cursor); private: virtual bool parseJson(bool success, const QString reply_json); diff --git a/lib/extension/com/atproto/sync/comatprotosyncsubscribereposex.cpp b/lib/extension/com/atproto/sync/comatprotosyncsubscribereposex.cpp index 86ce548e..8fdb008a 100644 --- a/lib/extension/com/atproto/sync/comatprotosyncsubscribereposex.cpp +++ b/lib/extension/com/atproto/sync/comatprotosyncsubscribereposex.cpp @@ -161,17 +161,17 @@ void ComAtprotoSyncSubscribeReposEx::messageReceivedFromJetStream(const QByteArr QJsonDocument doc = QJsonDocument::fromJson(message); QJsonObject json_src = doc.object(); - QHash commit_type_to; - commit_type_to["c"] = "create"; - commit_type_to["u"] = "update"; - commit_type_to["d"] = "delete"; + QHash commit_kind_to; + commit_kind_to["create"] = "create"; + commit_kind_to["update"] = "update"; + commit_kind_to["delete"] = "delete"; if (doc.isNull() || json_src.isEmpty()) { qDebug().noquote() << "Invalid data"; qDebug().noquote() << "message:" << message; emit errorOccured("InvalidData", "Unreadable JSON data."); m_webSocket.close(); - } else if (!json_src.contains("type") || !json_src.contains("did") + } else if (!json_src.contains("kind") || !json_src.contains("did") || !json_src.contains("commit")) { qDebug().noquote() << "Unsupport data:" << message; // emit errorOccured("InvalidData", "Unanticipated data structure."); @@ -194,12 +194,12 @@ void ComAtprotoSyncSubscribeReposEx::messageReceivedFromJetStream(const QByteArr json_dest.insert("commit", json_dest_commit); QJsonObject json_dest_op; - QString commit_type = commit_type_to.value(json_src_commit.value("type").toString()); - json_dest_op.insert("action", commit_type); + QString commit_op = commit_kind_to.value(json_src_commit.value("operation").toString()); + json_dest_op.insert("action", commit_op); json_dest_op.insert("path", QString("%1/%2").arg(json_src_commit.value("collection").toString(), json_src_commit.value("rkey").toString())); - if (commit_type == "delete") { + if (commit_op == "delete") { json_dest_op.insert("cid", QJsonValue()); } else { json_dest_op.insert("cid", json_dest_commit); diff --git a/lib/extension/oauth/oauthpushedauthorizationrequest.cpp b/lib/extension/oauth/oauthpushedauthorizationrequest.cpp new file mode 100644 index 00000000..bb226e16 --- /dev/null +++ b/lib/extension/oauth/oauthpushedauthorizationrequest.cpp @@ -0,0 +1,38 @@ +#include "oauthpushedauthorizationrequest.h" +#include "atprotocol/lexicons_func.h" + +#include +#include + +namespace AtProtocolInterface { + +OauthPushedAuthorizationRequest::OauthPushedAuthorizationRequest(QObject *parent) + : AccessAtProtocol { parent } +{ +} + +void OauthPushedAuthorizationRequest::pushedAuthorizationRequest(const QByteArray &payload) +{ + post(QString(), payload, false); +} + +const AtProtocolType::OauthDefs::PushedAuthorizationResponse & +OauthPushedAuthorizationRequest::pushedAuthorizationResponse() const +{ + return m_pushedAuthorizationResponse; +} + +bool OauthPushedAuthorizationRequest::parseJson(bool success, const QString reply_json) +{ + QJsonDocument json_doc = QJsonDocument::fromJson(reply_json.toUtf8()); + if (json_doc.isEmpty()) { + success = false; + } else { + AtProtocolType::OauthDefs::copyPushedAuthorizationResponse(json_doc.object(), + m_pushedAuthorizationResponse); + } + + return success; +} + +} diff --git a/lib/extension/oauth/oauthpushedauthorizationrequest.h b/lib/extension/oauth/oauthpushedauthorizationrequest.h new file mode 100644 index 00000000..c229f133 --- /dev/null +++ b/lib/extension/oauth/oauthpushedauthorizationrequest.h @@ -0,0 +1,26 @@ +#ifndef OAUTHPUSHEDAUTHORIZATIONREQUEST_H +#define OAUTHPUSHEDAUTHORIZATIONREQUEST_H + +#include "atprotocol/accessatprotocol.h" + +namespace AtProtocolInterface { + +class OauthPushedAuthorizationRequest : public AccessAtProtocol +{ +public: + explicit OauthPushedAuthorizationRequest(QObject *parent = nullptr); + + void pushedAuthorizationRequest(const QByteArray &payload); + + const AtProtocolType::OauthDefs::PushedAuthorizationResponse & + pushedAuthorizationResponse() const; + +private: + virtual bool parseJson(bool success, const QString reply_json); + + AtProtocolType::OauthDefs::PushedAuthorizationResponse m_pushedAuthorizationResponse; +}; + +} + +#endif // OAUTHPUSHEDAUTHORIZATIONREQUEST_H diff --git a/lib/extension/oauth/oauthrequesttoken.cpp b/lib/extension/oauth/oauthrequesttoken.cpp new file mode 100644 index 00000000..2c72874f --- /dev/null +++ b/lib/extension/oauth/oauthrequesttoken.cpp @@ -0,0 +1,33 @@ +#include "oauthrequesttoken.h" +#include "atprotocol/lexicons_func.h" + +#include +#include + +namespace AtProtocolInterface { + +OauthRequestToken::OauthRequestToken(QObject *parent) : AccessAtProtocol { parent } { } + +void OauthRequestToken::requestToken(const QByteArray &payload) +{ + post(QString(""), payload, false); +} + +const AtProtocolType::OauthDefs::TokenResponse &OauthRequestToken::tokenResponse() const +{ + return m_tokenResponse; +} + +bool OauthRequestToken::parseJson(bool success, const QString reply_json) +{ + QJsonDocument json_doc = QJsonDocument::fromJson(reply_json.toUtf8()); + if (json_doc.isEmpty()) { + success = false; + } else { + AtProtocolType::OauthDefs::copyTokenResponse(json_doc.object(), m_tokenResponse); + } + + return success; +} + +} diff --git a/lib/extension/oauth/oauthrequesttoken.h b/lib/extension/oauth/oauthrequesttoken.h new file mode 100644 index 00000000..400df02f --- /dev/null +++ b/lib/extension/oauth/oauthrequesttoken.h @@ -0,0 +1,25 @@ +#ifndef OAUTHREQUESTTOKEN_H +#define OAUTHREQUESTTOKEN_H + +#include "atprotocol/accessatprotocol.h" + +namespace AtProtocolInterface { + +class OauthRequestToken : public AccessAtProtocol +{ +public: + explicit OauthRequestToken(QObject *parent = nullptr); + + void requestToken(const QByteArray &payload); + + const AtProtocolType::OauthDefs::TokenResponse &tokenResponse() const; + +private: + virtual bool parseJson(bool success, const QString reply_json); + + AtProtocolType::OauthDefs::TokenResponse m_tokenResponse; +}; + +} + +#endif // OAUTHREQUESTTOKEN_H diff --git a/lib/extension/well-known/wellknownoauthauthorizationserver.cpp b/lib/extension/well-known/wellknownoauthauthorizationserver.cpp new file mode 100644 index 00000000..0f8745cc --- /dev/null +++ b/lib/extension/well-known/wellknownoauthauthorizationserver.cpp @@ -0,0 +1,40 @@ +#include "wellknownoauthauthorizationserver.h" +#include "atprotocol/lexicons_func.h" + +#include +#include +#include + +namespace AtProtocolInterface { + +WellKnownOauthAuthorizationServer::WellKnownOauthAuthorizationServer(QObject *parent) + : AccessAtProtocol { parent } +{ +} + +void WellKnownOauthAuthorizationServer::oauthAuthorizationServer() +{ + QUrlQuery url_query; + + get(QStringLiteral(".well-known/oauth-authorization-server"), url_query, false); +} + +const AtProtocolType::WellKnownDefs::ServerMetadata & +WellKnownOauthAuthorizationServer::serverMetadata() const +{ + return m_serverMetadata; +} + +bool WellKnownOauthAuthorizationServer::parseJson(bool success, const QString reply_json) +{ + QJsonDocument json_doc = QJsonDocument::fromJson(reply_json.toUtf8()); + if (json_doc.isEmpty()) { + success = false; + } else { + AtProtocolType::WellKnownDefs::copyServerMetadata(json_doc.object(), m_serverMetadata); + } + + return success; +} + +} diff --git a/lib/extension/well-known/wellknownoauthauthorizationserver.h b/lib/extension/well-known/wellknownoauthauthorizationserver.h new file mode 100644 index 00000000..c22122c0 --- /dev/null +++ b/lib/extension/well-known/wellknownoauthauthorizationserver.h @@ -0,0 +1,25 @@ +#ifndef WELLKNOWNOAUTHAUTHORIZATIONSERVER_H +#define WELLKNOWNOAUTHAUTHORIZATIONSERVER_H + +#include "atprotocol/accessatprotocol.h" + +namespace AtProtocolInterface { + +class WellKnownOauthAuthorizationServer : public AccessAtProtocol +{ +public: + explicit WellKnownOauthAuthorizationServer(QObject *parent = nullptr); + + void oauthAuthorizationServer(); + + const AtProtocolType::WellKnownDefs::ServerMetadata &serverMetadata() const; + +private: + virtual bool parseJson(bool success, const QString reply_json); + + AtProtocolType::WellKnownDefs::ServerMetadata m_serverMetadata; +}; + +} + +#endif // WELLKNOWNOAUTHAUTHORIZATIONSERVER_H diff --git a/lib/extension/well-known/wellknownoauthprotectedresource.cpp b/lib/extension/well-known/wellknownoauthprotectedresource.cpp new file mode 100644 index 00000000..aceaf604 --- /dev/null +++ b/lib/extension/well-known/wellknownoauthprotectedresource.cpp @@ -0,0 +1,40 @@ +#include "wellknownoauthprotectedresource.h" +#include "atprotocol/lexicons_func.h" + +#include +#include +#include + +namespace AtProtocolInterface { + +WellKnownOauthProtectedResource::WellKnownOauthProtectedResource(QObject *parent) + : AccessAtProtocol { parent } +{ +} + +void WellKnownOauthProtectedResource::oauthProtectedResource() +{ + QUrlQuery url_query; + + get(QStringLiteral(".well-known/oauth-protected-resource"), url_query, false); +} + +const AtProtocolType::WellKnownDefs::ResourceMetadata & +WellKnownOauthProtectedResource::resourceMetadata() const +{ + return m_resourceMetadata; +} + +bool WellKnownOauthProtectedResource::parseJson(bool success, const QString reply_json) +{ + QJsonDocument json_doc = QJsonDocument::fromJson(reply_json.toUtf8()); + if (json_doc.isEmpty()) { + success = false; + } else { + AtProtocolType::WellKnownDefs::copyResourceMetadata(json_doc.object(), m_resourceMetadata); + } + + return success; +} + +} diff --git a/lib/extension/well-known/wellknownoauthprotectedresource.h b/lib/extension/well-known/wellknownoauthprotectedresource.h new file mode 100644 index 00000000..f89d0cce --- /dev/null +++ b/lib/extension/well-known/wellknownoauthprotectedresource.h @@ -0,0 +1,25 @@ +#ifndef WELLKNOWNOAUTHPROTECTEDRESOURCE_H +#define WELLKNOWNOAUTHPROTECTEDRESOURCE_H + +#include "atprotocol/accessatprotocol.h" + +namespace AtProtocolInterface { + +class WellKnownOauthProtectedResource : public AccessAtProtocol +{ +public: + explicit WellKnownOauthProtectedResource(QObject *parent = nullptr); + + void oauthProtectedResource(); + + const AtProtocolType::WellKnownDefs::ResourceMetadata &resourceMetadata() const; + +private: + virtual bool parseJson(bool success, const QString reply_json); + + AtProtocolType::WellKnownDefs::ResourceMetadata m_resourceMetadata; +}; + +} + +#endif // WELLKNOWNOAUTHPROTECTEDRESOURCE_H diff --git a/lib/http/httpaccess.cpp b/lib/http/httpaccess.cpp index f3bd9eda..caa9aa87 100644 --- a/lib/http/httpaccess.cpp +++ b/lib/http/httpaccess.cpp @@ -138,7 +138,7 @@ bool HttpAccess::Private::process(HttpReply *reply) QByteArray::fromStdString(header.second)); } reply->setRecvData(QByteArray::fromStdString(res->body)); - if (res->status == 200) { + if (res->status >= 200 && res->status <= 299) { reply->setError(HttpReply::Success); result = true; } else { diff --git a/lib/http/simplehttpserver.cpp b/lib/http/simplehttpserver.cpp new file mode 100644 index 00000000..29e1a4e0 --- /dev/null +++ b/lib/http/simplehttpserver.cpp @@ -0,0 +1,74 @@ +#include "simplehttpserver.h" + +SimpleHttpServer::SimpleHttpServer(QObject *parent) : QAbstractHttpServer { parent } +{ + connect(&m_timeout, &QTimer::timeout, this, [=]() { emit timeout(); }); +} + +void SimpleHttpServer::setTimeout(int sec) +{ + if (m_timeout.isActive()) { + qDebug().noquote() << "SimpleHttpServer::Restart timeout:" << sec; + m_timeout.stop(); + } else { + qDebug().noquote() << "SimpleHttpServer::Start timeout:" << sec; + } + m_timeout.start(sec * 1000); +} + +void SimpleHttpServer::clearTimeout() +{ + if (m_timeout.isActive()) { + qDebug().noquote() << "SimpleHttpServer::Stop timeout"; + m_timeout.stop(); + } +} + +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) +#else +void SimpleHttpServer::missingHandler(const QHttpServerRequest &request, + QHttpServerResponder &&responder) +{ + Q_UNUSED(request) + Q_UNUSED(responder) +} +#endif + +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) +bool SimpleHttpServer::handleRequest(const QHttpServerRequest &request, QTcpSocket *socket) +# define MAKE_RESPONDER makeResponder(request, socket) +#else +# define MAKE_RESPONDER responder +bool SimpleHttpServer::handleRequest(const QHttpServerRequest &request, + QHttpServerResponder &responder) +#endif +{ + bool result = false; + QByteArray data; + QByteArray mime_type; + emit received(request, result, data, mime_type); + if (result) { + MAKE_RESPONDER.write(data, mime_type, QHttpServerResponder::StatusCode::Ok); + } else { + MAKE_RESPONDER.write(QHttpServerResponder::StatusCode::InternalServerError); + } + return true; +} + +QString SimpleHttpServer::convertResoucePath(const QUrl &url) +{ + QFileInfo file_info(url.path()); + return ":" + file_info.filePath(); +} + +bool SimpleHttpServer::readFile(const QString &path, QByteArray &data) +{ + QFile file(path); + if (file.open(QFile::ReadOnly)) { + data = file.readAll(); + file.close(); + return true; + } else { + return false; + } +} diff --git a/lib/http/simplehttpserver.h b/lib/http/simplehttpserver.h new file mode 100644 index 00000000..45f0b511 --- /dev/null +++ b/lib/http/simplehttpserver.h @@ -0,0 +1,33 @@ +#ifndef SIMPLEHTTPSERVER_H +#define SIMPLEHTTPSERVER_H + +#include +#include + +class SimpleHttpServer : public QAbstractHttpServer +{ + Q_OBJECT +public: + explicit SimpleHttpServer(QObject *parent = nullptr); + + void setTimeout(int sec); + void clearTimeout(); +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) + bool handleRequest(const QHttpServerRequest &request, QTcpSocket *socket) override; +#else + virtual bool handleRequest(const QHttpServerRequest &request, QHttpServerResponder &responder); + virtual void missingHandler(const QHttpServerRequest &request, + QHttpServerResponder &&responder); +#endif + static QString convertResoucePath(const QUrl &url); + static bool readFile(const QString &path, QByteArray &data); +signals: + void received(const QHttpServerRequest &request, bool &result, QByteArray &data, + QByteArray &mime_type); + void timeout(); + +private: + QTimer m_timeout; +}; + +#endif // SIMPLEHTTPSERVER_H diff --git a/lib/lib.pro b/lib/lib.pro index 5b6763c6..e4025e5f 100644 --- a/lib/lib.pro +++ b/lib/lib.pro @@ -1,4 +1,7 @@ -QT += xml sql websockets +QT += xml sql websockets httpserver +greaterThan(QT_MAJOR_VERSION, 5) { +QT += core5compat +} TEMPLATE = lib CONFIG += staticlib @@ -8,7 +11,11 @@ CONFIG += c++11 INCLUDEPATH += $$PWD \ $$PWD/../3rdparty/cpp-httplib \ $$PWD/../zlib/include +greaterThan(QT_MAJOR_VERSION, 5) { +win32:INCLUDEPATH += $$dirname(QMAKE_QMAKE)/../../../Tools/OpenSSLv3/Win_x64/include +}else{ win32:INCLUDEPATH += $$dirname(QMAKE_QMAKE)/../../../Tools/OpenSSL/Win_x64/include +} unix:INCLUDEPATH += ../openssl/include DEFINES += CPPHTTPLIB_ZLIB_SUPPORT # zlib support for cpp-httplib @@ -75,6 +82,7 @@ SOURCES += \ $$PWD/atprotocol/com/atproto/moderation/comatprotomoderationcreatereport.cpp \ $$PWD/atprotocol/com/atproto/repo/comatprotorepocreaterecord.cpp \ $$PWD/atprotocol/com/atproto/repo/comatprotorepodeleterecord.cpp \ + $$PWD/atprotocol/com/atproto/repo/comatprotorepodescriberepo.cpp \ $$PWD/atprotocol/com/atproto/repo/comatprotorepogetrecord.cpp \ $$PWD/atprotocol/com/atproto/repo/comatprotorepolistrecords.cpp \ $$PWD/atprotocol/com/atproto/repo/comatprotorepoputrecord.cpp \ @@ -96,9 +104,14 @@ SOURCES += \ $$PWD/extension/com/atproto/sync/comatprotosyncsubscribereposex.cpp \ $$PWD/extension/directory/plc/directoryplc.cpp \ $$PWD/extension/directory/plc/directoryplclogaudit.cpp \ + $$PWD/extension/oauth/oauthpushedauthorizationrequest.cpp \ + $$PWD/extension/oauth/oauthrequesttoken.cpp \ + $$PWD/extension/well-known/wellknownoauthauthorizationserver.cpp \ + $$PWD/extension/well-known/wellknownoauthprotectedresource.cpp \ $$PWD/http/httpaccess.cpp \ $$PWD/http/httpaccessmanager.cpp \ $$PWD/http/httpreply.cpp \ + $$PWD/http/simplehttpserver.cpp \ $$PWD/log/logaccess.cpp \ $$PWD/log/logmanager.cpp \ $$PWD/realtime/abstractpostselector.cpp \ @@ -110,16 +123,22 @@ SOURCES += \ $$PWD/realtime/notpostselector.cpp \ $$PWD/realtime/orpostselector.cpp \ $$PWD/realtime/xorpostselector.cpp \ + $$PWD/tools/accountmanager.cpp \ + $$PWD/tools/authorization.cpp \ $$PWD/tools/base32.cpp \ $$PWD/tools/cardecoder.cpp \ $$PWD/tools/chatlogsubscriber.cpp \ $$PWD/tools/configurablelabels.cpp \ + $$PWD/tools/encryption.cpp \ + $$PWD/tools/es256.cpp \ $$PWD/tools/imagecompressor.cpp \ + $$PWD/tools/jsonwebtoken.cpp \ $$PWD/tools/labelerprovider.cpp \ $$PWD/tools/leb128.cpp \ $$PWD/tools/listitemscache.cpp \ $$PWD/tools/opengraphprotocol.cpp \ - $$PWD/tools/pinnedpostcache.cpp + $$PWD/tools/pinnedpostcache.cpp \ + tools/tid.cpp HEADERS += \ $$PWD/atprotocol/accessatprotocol.h \ @@ -181,6 +200,7 @@ HEADERS += \ $$PWD/atprotocol/com/atproto/moderation/comatprotomoderationcreatereport.h \ $$PWD/atprotocol/com/atproto/repo/comatprotorepocreaterecord.h \ $$PWD/atprotocol/com/atproto/repo/comatprotorepodeleterecord.h \ + $$PWD/atprotocol/com/atproto/repo/comatprotorepodescriberepo.h \ $$PWD/atprotocol/com/atproto/repo/comatprotorepogetrecord.h \ $$PWD/atprotocol/com/atproto/repo/comatprotorepolistrecords.h \ $$PWD/atprotocol/com/atproto/repo/comatprotorepoputrecord.h \ @@ -203,9 +223,14 @@ HEADERS += \ $$PWD/extension/com/atproto/sync/comatprotosyncsubscribereposex.h \ $$PWD/extension/directory/plc/directoryplc.h \ $$PWD/extension/directory/plc/directoryplclogaudit.h \ + $$PWD/extension/oauth/oauthpushedauthorizationrequest.h \ + $$PWD/extension/oauth/oauthrequesttoken.h \ + $$PWD/extension/well-known/wellknownoauthauthorizationserver.h \ + $$PWD/extension/well-known/wellknownoauthprotectedresource.h \ $$PWD/http/httpaccess.h \ $$PWD/http/httpaccessmanager.h \ $$PWD/http/httpreply.h \ + $$PWD/http/simplehttpserver.h \ $$PWD/log/logaccess.h \ $$PWD/log/logmanager.h \ $$PWD/realtime/abstractpostselector.h \ @@ -218,14 +243,25 @@ HEADERS += \ $$PWD/realtime/orpostselector.h \ $$PWD/realtime/xorpostselector.h \ $$PWD/search/search.h \ + $$PWD/tools/accountmanager.h \ + $$PWD/tools/authorization.h \ $$PWD/tools/base32.h \ $$PWD/tools/cardecoder.h \ $$PWD/tools/chatlogsubscriber.h \ $$PWD/tools/configurablelabels.h \ + $$PWD/tools/encryption.h \ + $$PWD/tools/encryption_seed.h \ + $$PWD/tools/es256.h \ $$PWD/tools/imagecompressor.h \ + $$PWD/tools/jsonwebtoken.h \ $$PWD/tools/labelerprovider.h \ $$PWD/tools/leb128.h \ $$PWD/tools/listitemscache.h \ $$PWD/tools/opengraphprotocol.h \ $$PWD/tools/pinnedpostcache.h \ - $$PWD/tools/qstringex.h + $$PWD/tools/qstringex.h \ + common.h \ + tools/tid.h + +RESOURCES += \ + $$PWD/lib.qrc diff --git a/lib/lib.qrc b/lib/lib.qrc new file mode 100644 index 00000000..4b6bda64 --- /dev/null +++ b/lib/lib.qrc @@ -0,0 +1,6 @@ + + + tools/oauth/oauth_fail.html + tools/oauth/oauth_success.html + + diff --git a/lib/realtime/firehosereceiver.cpp b/lib/realtime/firehosereceiver.cpp index c474b238..48d6ad97 100644 --- a/lib/realtime/firehosereceiver.cpp +++ b/lib/realtime/firehosereceiver.cpp @@ -121,6 +121,7 @@ void FirehoseReceiver::start() void FirehoseReceiver::stop() { + m_wdgTimer.stop(); if (m_client.state() == QAbstractSocket::SocketState::UnconnectedState || m_client.state() == QAbstractSocket::SocketState::ClosingState) return; diff --git a/lib/tools/accountmanager.cpp b/lib/tools/accountmanager.cpp new file mode 100644 index 00000000..c464698b --- /dev/null +++ b/lib/tools/accountmanager.cpp @@ -0,0 +1,616 @@ +#include "accountmanager.h" +#include "encryption.h" +#include "extension/com/atproto/server/comatprotoservercreatesessionex.h" +#include "extension/com/atproto/server/comatprotoserverrefreshsessionex.h" +#include "extension/com/atproto/repo/comatprotorepogetrecordex.h" +#include "atprotocol/app/bsky/actor/appbskyactorgetprofile.h" +#include "atprotocol/com/atproto/repo/comatprotorepodescriberepo.h" +#include "extension/directory/plc/directoryplc.h" +#include "atprotocol/lexicons_func_unknown.h" +#include "tools/pinnedpostcache.h" +#include "common.h" + +#include +#include +#include +#include + +using AtProtocolInterface::AccountData; +using AtProtocolInterface::AccountStatus; +using AtProtocolInterface::AppBskyActorGetProfile; +using AtProtocolInterface::ComAtprotoRepoDescribeRepo; +using AtProtocolInterface::ComAtprotoRepoGetRecordEx; +using AtProtocolInterface::ComAtprotoServerCreateSessionEx; +using AtProtocolInterface::ComAtprotoServerRefreshSessionEx; +using AtProtocolInterface::DirectoryPlc; + +class AccountManager::Private : public QObject +{ +public: + explicit Private(AccountManager *parent); + ~Private(); + + QJsonObject save() const; + void load(const QJsonObject &object); + + bool update(AccountManager::AccountManagerRoles role, const QVariant &value); + + AccountData getAccount() const; + void updateAccount(const QString &uuid, const QString &service, const QString &identifier, + const QString &password, const QString &did, const QString &handle, + const QString &email, const QString &accessJwt, const QString &refreshJwt, + const QString &thread_gate_type, const AccountStatus status); + + void createSession(); + void refreshSession(bool initial = false); + void getProfile(); + void getServiceEndpoint(const QString &did, const QString &service, + std::function callback); + void setMain(bool is); + +private: + AccountManager *q; + + AccountData m_account; + Encryption m_encryption; +}; + +AccountManager::Private::Private(AccountManager *parent) : q(parent) +{ + qDebug().noquote() << this << "AccountManager::Private()"; +} + +AccountManager::Private::~Private() +{ + qDebug().noquote() << this << "AccountManager::~Private()"; +} + +QJsonObject AccountManager::Private::save() const +{ + QJsonObject account_item; + account_item["uuid"] = m_account.uuid; + account_item["is_main"] = m_account.is_main; + account_item["service"] = m_account.service; + account_item["identifier"] = m_account.identifier; + account_item["password"] = m_encryption.encrypt(m_account.password); + account_item["refresh_jwt"] = m_encryption.encrypt(m_account.refreshJwt); + + if (!m_account.post_languages.isEmpty()) { + QJsonArray post_langs; + for (const auto &lang : m_account.post_languages) { + post_langs.append(lang); + } + account_item["post_languages"] = post_langs; + } + + if (m_account.thread_gate_type.isEmpty()) { + account_item["thread_gate_type"] = "everybody"; + } else { + account_item["thread_gate_type"] = m_account.thread_gate_type; + } + if (!m_account.thread_gate_options.isEmpty()) { + QJsonArray thread_gate_options; + for (const auto &option : m_account.thread_gate_options) { + thread_gate_options.append(option); + } + account_item["thread_gate_options"] = thread_gate_options; + } + account_item["post_gate_quote_enabled"] = m_account.post_gate_quote_enabled; + + return account_item; +} + +void AccountManager::Private::load(const QJsonObject &object) +{ + + QString temp_refresh = object.value("refresh_jwt").toString(); + m_account.uuid = object.value("uuid").toString(); + m_account.is_main = object.value("is_main").toBool(); + m_account.service = object.value("service").toString(); + m_account.service_endpoint = m_account.service; + m_account.identifier = object.value("identifier").toString(); + m_account.password = m_encryption.decrypt(object.value("password").toString()); + m_account.refreshJwt = m_encryption.decrypt(temp_refresh); + m_account.handle = m_account.identifier; + for (const auto &value : object.value("post_languages").toArray()) { + m_account.post_languages.append(value.toString()); + } + + m_account.thread_gate_type = object.value("thread_gate_type").toString("everybody"); + if (m_account.thread_gate_type.isEmpty()) { + m_account.thread_gate_type = "everybody"; + } + for (const auto &value : object.value("thread_gate_options").toArray()) { + m_account.thread_gate_options.append(value.toString()); + } + m_account.post_gate_quote_enabled = object.value("post_gate_quote_enabled").toBool(true); + + if (temp_refresh.isEmpty()) { + createSession(); + } else { + refreshSession(true); + } +} + +bool AccountManager::Private::update(AccountManagerRoles role, const QVariant &value) +{ + bool need_save = false; + + if (role == UuidRole) + m_account.uuid = value.toString(); + else if (role == ServiceRole) + m_account.service = value.toString(); + else if (role == ServiceEndpointRole) + m_account.service_endpoint = value.toString(); + else if (role == IdentifierRole) + m_account.identifier = value.toString(); + else if (role == PasswordRole) + m_account.password = value.toString(); + else if (role == DidRole) + m_account.did = value.toString(); + else if (role == HandleRole) + m_account.handle = value.toString(); + else if (role == EmailRole) + m_account.email = value.toString(); + else if (role == AccessJwtRole) + m_account.accessJwt = value.toString(); + else if (role == RefreshJwtRole) + m_account.refreshJwt = value.toString(); + + else if (role == DisplayNameRole) + m_account.displayName = value.toString(); + else if (role == DescriptionRole) + m_account.description = value.toString(); + else if (role == AvatarRole) + m_account.avatar = value.toString(); + + else if (role == PostLanguagesRole) { + m_account.post_languages = value.toStringList(); + need_save = true; + } else if (role == ThreadGateTypeRole) { + m_account.thread_gate_type = value.toString(); + need_save = true; + } else if (role == ThreadGateOptionsRole) { + m_account.thread_gate_options = value.toStringList(); + need_save = true; + } else if (role == PostGateQuoteEnabledRole) { + m_account.post_gate_quote_enabled = value.toBool(); + } + return need_save; +} + +AccountData AccountManager::Private::getAccount() const +{ + return m_account; +} + +void AccountManager::Private::updateAccount(const QString &uuid, const QString &service, + const QString &identifier, const QString &password, + const QString &did, const QString &handle, + const QString &email, const QString &accessJwt, + const QString &refreshJwt, + const QString &thread_gate_type, + const AccountStatus status) +{ + m_account.uuid = uuid; + m_account.service = service; + m_account.identifier = identifier; + m_account.password = password; + m_account.did = did; + m_account.handle = handle; + m_account.email = email; + m_account.accessJwt = accessJwt; + m_account.refreshJwt = refreshJwt; + m_account.thread_gate_type = thread_gate_type; + m_account.status = status; +} + +void AccountManager::Private::createSession() +{ + ComAtprotoServerCreateSessionEx *session = new ComAtprotoServerCreateSessionEx(this); + connect(session, &ComAtprotoServerCreateSessionEx::finished, [=](bool success) { + // qDebug() << session << session->service() << session->did() << + // session->handle() + // << session->email() << session->accessJwt() << session->refreshJwt(); + // qDebug() << service << identifier << password; + if (success) { + qDebug() << "Create session" << session->did() << session->handle(); + m_account.did = session->did(); + m_account.handle = session->handle(); + m_account.email = session->email(); + m_account.accessJwt = session->accessJwt(); + m_account.refreshJwt = session->refreshJwt(); + + // 詳細を取得 + getProfile(); + } else { + qDebug() << "Fail createSession."; + m_account.status = AccountStatus::Unauthorized; + emit q->errorOccured(session->errorCode(), session->errorMessage()); + + q->checkAllAccountsReady(); + if (q->allAccountTried()) { + emit q->finished(); + } + } + session->deleteLater(); + }); + session->setAccount(m_account); + session->createSession(m_account.identifier, m_account.password, QString()); +} + +void AccountManager::Private::refreshSession(bool initial) +{ + + ComAtprotoServerRefreshSessionEx *session = new ComAtprotoServerRefreshSessionEx(this); + connect(session, &ComAtprotoServerRefreshSessionEx::finished, [=](bool success) { + if (success) { + qDebug() << "Refresh session" << session->did() << session->handle() + << session->email(); + m_account.did = session->did(); + m_account.handle = session->handle(); + m_account.email = session->email(); + m_account.accessJwt = session->accessJwt(); + m_account.refreshJwt = session->refreshJwt(); + + // 詳細を取得 + getProfile(); + } else { + if (initial) { + // 初期化時のみ(つまりloadから呼ばれたときだけは失敗したらcreateSessionで再スタート) + qDebug() << "Initial refresh session fail."; + m_account.status = AccountStatus::Unknown; + createSession(); + } else { + m_account.status = AccountStatus::Unauthorized; + emit q->errorOccured(session->errorCode(), session->errorMessage()); + + q->checkAllAccountsReady(); + if (q->allAccountTried()) { + emit q->finished(); + } + } + } + session->deleteLater(); + }); + session->setAccount(m_account); + session->refreshSession(); +} + +void AccountManager::Private::getProfile() +{ + + getServiceEndpoint(m_account.did, m_account.service, [=](const QString &service_endpoint) { + m_account.service_endpoint = service_endpoint; + qDebug().noquote() << "Update service endpoint" << m_account.service << "->" + << m_account.service_endpoint; + + AppBskyActorGetProfile *profile = new AppBskyActorGetProfile(this); + connect(profile, &AppBskyActorGetProfile::finished, [=](bool success) { + if (success) { + AtProtocolType::AppBskyActorDefs::ProfileViewDetailed detail = + profile->profileViewDetailed(); + qDebug() << "Update profile detailed" << detail.displayName << detail.description; + m_account.displayName = detail.displayName; + m_account.description = detail.description; + m_account.avatar = detail.avatar; + m_account.banner = detail.banner; + m_account.status = AccountStatus::Authorized; + + q->save(); + + emit q->updatedAccount(m_account.uuid); + + qDebug() << "Update pinned post" << detail.pinnedPost.uri; + PinnedPostCache::getInstance()->update(m_account.did, detail.pinnedPost.uri); + } else { + emit q->errorOccured(profile->errorCode(), profile->errorMessage()); + } + + q->checkAllAccountsReady(); + if (q->allAccountTried()) { + emit q->finished(); + } + + profile->deleteLater(); + }); + profile->setAccount(m_account); + profile->getProfile(m_account.did); + }); +} + +void AccountManager::Private::getServiceEndpoint(const QString &did, const QString &service, + std::function callback) +{ + if (did.isEmpty()) { + callback(service); + return; + } + // if (!service.startsWith("https://bsky.social")) { + // callback(service); + // return; + // } + + ComAtprotoRepoDescribeRepo *repo = new ComAtprotoRepoDescribeRepo(this); + connect(repo, &ComAtprotoRepoDescribeRepo::finished, this, [=](bool success) { + if (success) { + AtProtocolType::DirectoryPlcDefs::DidDoc doc = + AtProtocolType::LexiconsTypeUnknown::fromQVariant< + AtProtocolType::DirectoryPlcDefs::DidDoc>(repo->didDoc()); + if (!doc.service.isEmpty()) { + callback(doc.service.first().serviceEndpoint); + } else { + callback(service); + } + } else { + callback(service); + } + repo->deleteLater(); + }); + repo->setAccount(m_account); + repo->describeRepo(did); +} + +void AccountManager::Private::setMain(bool is) +{ + m_account.is_main = is; +} + +AccountManager::AccountManager(QObject *parent) : QObject { parent }, m_allAccountsReady(false) +{ + qDebug().noquote() << this << "AccountManager()"; +} + +AccountManager::~AccountManager() +{ + qDebug().noquote() << this << "~AccountManager()"; + clear(); +} + +AccountManager *AccountManager::getInstance() +{ + static AccountManager instance; + return &instance; +} + +void AccountManager::clear() +{ + for (const auto d : dList) { + delete d; + } + dList.clear(); + dIndex.clear(); + emit countChanged(); +} + +void AccountManager::save() const +{ + QJsonArray account_array; + + for (const auto d : dList) { + account_array.append(d->save()); + } + + Common::saveJsonDocument(QJsonDocument(account_array), QStringLiteral("account.json")); +} + +void AccountManager::load() +{ + QJsonDocument doc = Common::loadJsonDocument(QStringLiteral("account.json")); + + if (doc.isArray()) { + bool has_main = false; + for (const auto &item : doc.array()) { + QString uuid = item.toObject().value("uuid").toString(); + if (!dIndex.contains(uuid)) { + dList.append(new AccountManager::Private(this)); + dIndex[uuid] = dList.count() - 1; + + emit countChanged(); + } + dList.at(dIndex[uuid])->load(item.toObject()); + } + if (!has_main && !dList.isEmpty()) { + // mainになっているものがない + dList.at(0)->setMain(true); + } + } + if (dList.isEmpty()) { + emit finished(); + } +} + +void AccountManager::update(int row, AccountManagerRoles role, const QVariant &value) +{ + if (row < 0 || row >= count()) + return; + + if (dList.at(row)->update(role, value)) { + save(); + } +} + +AccountData AccountManager::getAccount(const QString &uuid) const +{ + if (!dIndex.contains(uuid)) { + return AccountData(); + } + return dList.at(dIndex.value(uuid))->getAccount(); +} + +QString AccountManager::updateAccount(const QString &uuid, const QString &service, + const QString &identifier, const QString &password, + const QString &did, const QString &handle, + const QString &email, const QString &accessJwt, + const QString &refreshJwt, const bool authorized) +{ + QString ret; + if (!uuid.isEmpty() && dIndex.contains(uuid)) { + AccountData account = dList.at(dIndex[uuid])->getAccount(); + dList.at(dIndex[uuid]) + ->updateAccount(account.uuid, service, identifier, password, did, handle, email, + accessJwt, refreshJwt, account.thread_gate_type, + authorized ? AccountStatus::Authorized + : AccountStatus::Unauthorized); + ret = uuid; + } else { + // append + QString new_uuid = QUuid::createUuid().toString(QUuid::WithoutBraces); + dList.append(new AccountManager::Private(this)); + dIndex[new_uuid] = dList.count() - 1; + dList.last()->updateAccount( + new_uuid, service, identifier, password, did, handle, email, accessJwt, refreshJwt, + "everybody", authorized ? AccountStatus::Authorized : AccountStatus::Unauthorized); + + ret = new_uuid; + emit countChanged(); + } + save(); + checkAllAccountsReady(); + return ret; +} + +void AccountManager::removeAccount(const QString &uuid) +{ + if (!dIndex.contains(uuid)) { + return; + } + int i = dIndex.value(uuid); + delete dList.at(i); + dList.removeAt(i); + + // refresh index + dIndex.clear(); + for (int i = 0; i < dList.count(); i++) { + dIndex[dList.at(i)->getAccount().uuid] = i; + } + + save(); + emit countChanged(); + checkAllAccountsReady(); +} + +void AccountManager::updateAccountProfile(const QString &uuid) +{ + if (!dIndex.contains(uuid)) { + return; + } + dList.at(dIndex.value(uuid))->getProfile(); +} + +void AccountManager::updateServiceEndpoint(const QString &uuid, const QString &service_endpoint) +{ + update(indexAt(uuid), AccountManager::AccountManagerRoles::ServiceEndpointRole, + service_endpoint); +} + +int AccountManager::getMainAccountIndex() const +{ + if (dList.isEmpty()) + return -1; + + for (int i = 0; i < dList.count(); i++) { + if (dList.at(i)->getAccount().is_main) { + return i; + } + } + return 0; +} + +void AccountManager::setMainAccount(int row) +{ + if (row < 0 || row >= count()) + return; + + for (int i = 0; i < dList.count(); i++) { + bool new_val = (row == i); + if (dList.at(i)->getAccount().is_main != new_val) { + dList.at(i)->setMain(new_val); + } + } + + save(); +} + +bool AccountManager::checkAllAccountsReady() +{ + int ready_count = 0; + for (const auto d : qAsConst(dList)) { + if (d->getAccount().status == AccountStatus::Authorized) { + ready_count++; + } + } + setAllAccountsReady(!dList.isEmpty() && dList.count() == ready_count); + return allAccountsReady(); +} + +int AccountManager::indexAt(const QString &uuid) +{ + return dIndex.value(uuid, -1); +} + +QStringList AccountManager::getUuids() const +{ + QStringList uuids; + for (const auto d : dList) { + uuids.append(d->getAccount().uuid); + } + return uuids; +} + +QString AccountManager::getUuid(int row) const +{ + if (row < 0 || row >= count()) + return QString(); + return dList.at(row)->getAccount().uuid; +} + +bool AccountManager::allAccountTried() const +{ + int count = 0; + for (const auto d : dList) { + if (d->getAccount().status != AccountStatus::Unknown) { + count++; + } + } + return (dList.count() == count); +} + +bool AccountManager::allAccountsReady() const +{ + return m_allAccountsReady; +} + +void AccountManager::setAllAccountsReady(bool newAllAccountsReady) +{ + if (m_allAccountsReady == newAllAccountsReady) + return; + m_allAccountsReady = newAllAccountsReady; + emit allAccountsReadyChanged(); +} + +int AccountManager::count() const +{ + return dList.count(); +} + +void AccountManager::createSession(int row) +{ + if (row < 0 || row >= count()) + return; + dList.at(row)->createSession(); +} + +void AccountManager::refreshSession(int row, bool initial) +{ + if (row < 0 || row >= count()) + return; + dList.at(row)->refreshSession(initial); +} + +void AccountManager::getProfile(int row) +{ + if (row < 0 || row >= count()) + return; + dList.at(row)->getProfile(); +} diff --git a/lib/tools/accountmanager.h b/lib/tools/accountmanager.h new file mode 100644 index 00000000..e68be5dd --- /dev/null +++ b/lib/tools/accountmanager.h @@ -0,0 +1,91 @@ +#ifndef ACCOUNTMANAGER_H +#define ACCOUNTMANAGER_H + +#include "atprotocol/accessatprotocol.h" + +#include +#include +#include + +class AccountManager : public QObject +{ + Q_OBJECT + explicit AccountManager(QObject *parent = nullptr); + ~AccountManager(); + + Q_PROPERTY(bool allAccountsReady READ allAccountsReady WRITE setAllAccountsReady NOTIFY + allAccountsReadyChanged FINAL) +public: + static AccountManager *getInstance(); + + enum AccountManagerRoles { + UnknownRole, + UuidRole, + IsMainRole, + ServiceRole, + ServiceEndpointRole, + IdentifierRole, + PasswordRole, + DidRole, + HandleRole, + EmailRole, + AccessJwtRole, + RefreshJwtRole, + DisplayNameRole, + DescriptionRole, + AvatarRole, + PostLanguagesRole, + ThreadGateTypeRole, + ThreadGateOptionsRole, + PostGateQuoteEnabledRole, + StatusRole, + AuthorizedRole, + }; + + void clear(); + void save() const; + void load(); + + void update(int row, AccountManager::AccountManagerRoles role, const QVariant &value); + + AtProtocolInterface::AccountData getAccount(const QString &uuid) const; + QString updateAccount(const QString &uuid, const QString &service, const QString &identifier, + const QString &password, const QString &did, const QString &handle, + const QString &email, const QString &accessJwt, const QString &refreshJwt, + const bool authorized); + void removeAccount(const QString &uuid); + void updateAccountProfile(const QString &uuid); + void updateServiceEndpoint(const QString &uuid, const QString &service_endpoint); + int getMainAccountIndex() const; + void setMainAccount(int row); + bool checkAllAccountsReady(); + int indexAt(const QString &uuid); + + QStringList getUuids() const; + QString getUuid(int row) const; + bool allAccountsReady() const; + void setAllAccountsReady(bool newAllAccountsReady); + int count() const; + + void createSession(int row); + void refreshSession(int row, bool initial = false); + void getProfile(int row); + +signals: + void errorOccured(const QString &code, const QString &message); + void updatedAccount(const QString &uuid); + void countChanged(); + void finished(); + void allAccountsReadyChanged(); + +private: + class Private; + QList dList; + QHash dIndex; + Q_DISABLE_COPY_MOVE(AccountManager) + + bool allAccountTried() const; + bool m_allAccountsReady; +}; + +#endif // ACCOUNTMANAGER_H diff --git a/lib/tools/authorization.cpp b/lib/tools/authorization.cpp new file mode 100644 index 00000000..a4b316cf --- /dev/null +++ b/lib/tools/authorization.cpp @@ -0,0 +1,615 @@ +#include "authorization.h" +#include "http/httpaccess.h" +#include "http/simplehttpserver.h" +#include "atprotocol/com/atproto/repo/comatprotorepodescriberepo.h" +#include "extension/well-known/wellknownoauthprotectedresource.h" +#include "extension/well-known/wellknownoauthauthorizationserver.h" +#include "extension/oauth/oauthpushedauthorizationrequest.h" +#include "extension/oauth/oauthrequesttoken.h" +#include "atprotocol/lexicons_func_unknown.h" +#include "tools/jsonwebtoken.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using AtProtocolInterface::ComAtprotoRepoDescribeRepo; +using AtProtocolInterface::OauthPushedAuthorizationRequest; +using AtProtocolInterface::OauthRequestToken; +using AtProtocolInterface::WellKnownOauthAuthorizationServer; +using AtProtocolInterface::WellKnownOauthProtectedResource; + +Authorization::Authorization(QObject *parent) : QObject { parent }, m_redirectTimeout(300) { } + +void Authorization::reset() +{ + // user + m_handle.clear(); + // server info + m_serviceEndpoint.clear(); + m_authorizationServer.clear(); + // server meta data + m_pushedAuthorizationRequestEndpoint.clear(); + m_authorizationEndpoint.clear(); + m_tokenEndopoint.clear(); + m_scopesSupported.clear(); + // + m_redirectUri.clear(); + m_clientId.clear(); + // par + m_codeChallenge.clear(); + m_codeVerifier.clear(); + m_state.clear(); + // request token + m_code.clear(); + m_token = AtProtocolType::OauthDefs::TokenResponse(); + // + m_listenPort.clear(); +} + +void Authorization::start(const QString &pds, const QString &handle) +{ + if (pds.isEmpty() || handle.isEmpty()) + return; + + startRedirectServer(); + + AtProtocolInterface::AccountData account; + account.service = pds; + m_handle = handle; + + ComAtprotoRepoDescribeRepo *repo = new ComAtprotoRepoDescribeRepo(this); + connect(repo, &ComAtprotoRepoDescribeRepo::finished, this, [=](bool success) { + if (success) { + auto did_doc = AtProtocolType::LexiconsTypeUnknown::fromQVariant< + AtProtocolType::DirectoryPlcDefs::DidDoc>(repo->didDoc()); + if (!did_doc.service.isEmpty()) { + setServiceEndpoint(did_doc.service.first().serviceEndpoint); + + // next step + requestOauthProtectedResource(); + } else { + emit errorOccured("Invalid oauth-protected-resource", + "authorization_servers is empty."); + emit finished(false); + } + } else { + emit errorOccured(repo->errorCode(), repo->errorMessage()); + emit finished(false); + } + repo->deleteLater(); + }); + repo->setAccount(account); + repo->describeRepo(handle); +} + +void Authorization::requestOauthProtectedResource() +{ + // /.well-known/oauth-protected-resource + if (serviceEndpoint().isEmpty()) + return; + + AtProtocolInterface::AccountData account; + account.service = serviceEndpoint(); + + WellKnownOauthProtectedResource *resource = new WellKnownOauthProtectedResource(this); + connect(resource, &WellKnownOauthProtectedResource::finished, this, [=](bool success) { + if (success) { + if (!resource->resourceMetadata().authorization_servers.isEmpty()) { + setAuthorizationServer(resource->resourceMetadata().authorization_servers.first()); + // next step + requestOauthAuthorizationServer(); + } else { + emit errorOccured("Invalid oauth-protected-resource", + "authorization_servers is empty."); + emit finished(false); + } + } else { + emit errorOccured(resource->errorCode(), resource->errorMessage()); + emit finished(false); + } + resource->deleteLater(); + }); + resource->setAccount(account); + resource->oauthProtectedResource(); +} + +void Authorization::requestOauthAuthorizationServer() +{ + // /.well-known/oauth-authorization-server + + if (authorizationServer().isEmpty()) + return; + + AtProtocolInterface::AccountData account; + account.service = authorizationServer(); + + WellKnownOauthAuthorizationServer *server = new WellKnownOauthAuthorizationServer(this); + connect(server, &WellKnownOauthAuthorizationServer::finished, this, [=](bool success) { + if (success) { + QString error_message; + if (validateServerMetadata(server->serverMetadata(), error_message)) { + const QStringList scope_candidates = QStringList() << "atproto" + << "transition:generic" + << "transition:chat.bsky"; + setPushedAuthorizationRequestEndpoint( + server->serverMetadata().pushed_authorization_request_endpoint); + setAuthorizationEndpoint(server->serverMetadata().authorization_endpoint); + setTokenEndopoint(server->serverMetadata().token_endpoint); + for (const auto &scope : scope_candidates) { + if (server->serverMetadata().scopes_supported.contains(scope)) { + m_scopesSupported.append(scope); + } + } + + // next step + par(); + } else { + qDebug().noquote() << error_message; + emit errorOccured("Invalid oauth-authorization-server", error_message); + emit finished(false); + } + } else { + emit errorOccured(server->errorCode(), server->errorMessage()); + emit finished(false); + } + server->deleteLater(); + }); + server->setAccount(account); + server->oauthAuthorizationServer(); +} + +bool Authorization::validateServerMetadata( + const AtProtocolType::WellKnownDefs::ServerMetadata &server_metadata, + QString &error_message) +{ + bool ret = false; + if (QUrl(server_metadata.issuer).host() != QUrl(authorizationServer()).host()) { + // リダイレクトされると変わるので対応しないといけない + error_message = QString("'issuer' is an invalid value(%1).").arg(server_metadata.issuer); + } else if (!server_metadata.response_types_supported.contains("code")) { + error_message = QStringLiteral("'response_types_supported' must contain 'code'."); + } else if (!server_metadata.grant_types_supported.contains("authorization_code")) { + error_message = + QStringLiteral("'grant_types_supported' must contain 'authorization_code'."); + } else if (!server_metadata.grant_types_supported.contains("refresh_token")) { + error_message = QStringLiteral("'grant_types_supported' must contain 'refresh_token'."); + } else if (!server_metadata.code_challenge_methods_supported.contains("S256")) { + error_message = QStringLiteral("'code_challenge_methods_supported' must contain 'S256'."); + } else if (!server_metadata.token_endpoint_auth_methods_supported.contains("private_key_jwt")) { + error_message = QStringLiteral( + "'token_endpoint_auth_methods_supported' must contain 'private_key_jwt'."); + } else if (!server_metadata.token_endpoint_auth_methods_supported.contains("none")) { + error_message = + QStringLiteral("'token_endpoint_auth_methods_supported' must contain 'none'."); + } else if (!server_metadata.token_endpoint_auth_signing_alg_values_supported.contains( + "ES256")) { + error_message = QStringLiteral( + "'token_endpoint_auth_signing_alg_values_supported' must contain 'ES256'."); + } else if (!server_metadata.scopes_supported.contains("atproto")) { + error_message = QStringLiteral("'scopes_supported' must contain 'atproto'."); + } else if (!(server_metadata.subject_types_supported.isEmpty() + || (!server_metadata.subject_types_supported.isEmpty() + && server_metadata.subject_types_supported.contains("public")))) { + error_message = QStringLiteral("'subject_types_supported' must contain 'public'."); + } else if (!server_metadata.authorization_response_iss_parameter_supported) { + error_message = + QString("'authorization_response_iss_parameter_supported' is an invalid value(%1).") + .arg(server_metadata.authorization_response_iss_parameter_supported); + } else if (server_metadata.pushed_authorization_request_endpoint.isEmpty()) { + error_message = QStringLiteral("pushed_authorization_request_endpoint must be set'."); + } else if (!server_metadata.require_pushed_authorization_requests) { + error_message = QString("'require_pushed_authorization_requests' is an invalid value(%1).") + .arg(server_metadata.require_pushed_authorization_requests); + } else if (!server_metadata.dpop_signing_alg_values_supported.contains("ES256")) { + error_message = QStringLiteral("'dpop_signing_alg_values_supported' must contain 'ES256'."); + } else if (!server_metadata.require_request_uri_registration) { + error_message = QString("'require_request_uri_registration' is an invalid value(%1).") + .arg(server_metadata.require_request_uri_registration); + } else if (!server_metadata.client_id_metadata_document_supported) { + error_message = QString("'client_id_metadata_document_supported' is an invalid value(%1).") + .arg(server_metadata.client_id_metadata_document_supported); + } else { + ret = true; + } + return ret; +} + +QByteArray Authorization::state() const +{ + return m_state; +} + +void Authorization::setListenPort(const QString &newListenPort) +{ + m_listenPort = newListenPort; +} + +QString Authorization::listenPort() const +{ + return m_listenPort; +} + +void Authorization::makeClientId() +{ + QString port; + if (!m_listenPort.isEmpty()) { + port.append(":"); + port.append(m_listenPort); + } + m_redirectUri.append("http://127.0.0.1"); + m_redirectUri.append(port); + m_redirectUri.append("/tech/relog/hagoromo/oauth-callback"); + m_clientId = "https://oauth.hagoromo.relog.tech/client-metadata.json"; +} + +void Authorization::makeCodeChallenge() +{ + m_codeVerifier = generateRandomValues().toBase64(QByteArray::Base64UrlEncoding + | QByteArray::OmitTrailingEquals); + m_codeChallenge = + QCryptographicHash::hash(m_codeVerifier, QCryptographicHash::Sha256) + .toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); +} + +QByteArray Authorization::makeParPayload() +{ + makeClientId(); + makeCodeChallenge(); + m_state = QCryptographicHash::hash(m_codeVerifier, QCryptographicHash::Sha256) + .toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); + + QString login_hint = m_handle; + + QUrlQuery query; + query.addQueryItem("response_type", "code"); + query.addQueryItem("code_challenge", m_codeChallenge); + query.addQueryItem("code_challenge_method", "S256"); + query.addQueryItem("client_id", simplyEncode(m_clientId)); + query.addQueryItem("state", m_state); + query.addQueryItem("redirect_uri", simplyEncode(m_redirectUri)); + query.addQueryItem("scope", m_scopesSupported.join(" ")); + query.addQueryItem("login_hint", simplyEncode(login_hint)); + + return query.query(QUrl::FullyEncoded).toLocal8Bit(); +} + +void Authorization::par() +{ + if (pushedAuthorizationRequestEndpoint().isEmpty()) + return; + + AtProtocolInterface::AccountData account; + account.service = pushedAuthorizationRequestEndpoint(); + + OauthPushedAuthorizationRequest *req = new OauthPushedAuthorizationRequest(this); + connect(req, &OauthPushedAuthorizationRequest::finished, this, [=](bool success) { + if (success) { + if (!req->pushedAuthorizationResponse().request_uri.isEmpty()) { + // next step + setDPopNonce(req->dPopNonce()); + authorization(req->pushedAuthorizationResponse().request_uri); + } else { + emit errorOccured("Invalid Pushed Authorization Request", + "'request_uri' is empty."); + emit finished(false); + } + } else { + emit errorOccured(req->errorCode(), req->errorMessage()); + emit finished(false); + } + req->deleteLater(); + }); + req->setContentType("application/x-www-form-urlencoded"); + req->setAccount(account); + req->pushedAuthorizationRequest(makeParPayload()); +} + +void Authorization::authorization(const QString &request_uri) +{ + if (request_uri.isEmpty() || authorizationEndpoint().isEmpty() || m_listenPort.isEmpty()) + return; + + QString authorization_endpoint = authorizationEndpoint(); + + QUrl url(authorization_endpoint); + QUrlQuery query; + query.addQueryItem("client_id", simplyEncode(m_clientId)); + query.addQueryItem("request_uri", simplyEncode(request_uri)); + url.setQuery(query); + + qDebug().noquote() << "redirect" << url.toEncoded(); + + emit madeRequestUrl(url.toString()); + // QDesktopServices::openUrl(url); +} + +void Authorization::startRedirectServer() +{ + SimpleHttpServer *server = new SimpleHttpServer(this); + QPointer alive = server; + connect(server, &SimpleHttpServer::received, this, + [=](const QHttpServerRequest &request, bool &result, QByteArray &data, + QByteArray &mime_type) { + qDebug().noquote() << "received by startRedirectServer"; + qDebug().noquote() << " " << request.url().toString(); + qDebug().noquote() << " " << request.url().path(); + + if (request.url().path() != "/tech/relog/hagoromo/oauth-callback") { + result = SimpleHttpServer::readFile(":/tools/oauth/" + request.url().fileName(), + data); + mime_type = m_MimeDb.mimeTypeForFile(request.url().fileName()).name().toUtf8(); + qDebug().noquote() << "Other file:" << result << request.url().fileName() + << ", " << mime_type; + return; + } + + if (request.query().hasQueryItem("iss") && request.query().hasQueryItem("state") + && request.query().hasQueryItem("code")) { + // authorize + QString state = request.query().queryItemValue("state"); + result = (state.toUtf8() == m_state); + if (result) { + m_code = request.query().queryItemValue("code").toUtf8(); + requestToken(); + } else { + qDebug().noquote() << "Unknown state in authorization redirect :" << state; + emit finished(false); + m_code.clear(); + } + } + if (result) { + SimpleHttpServer::readFile(":/tools/oauth/oauth_success.html", data); + } else { + SimpleHttpServer::readFile(":/tools/oauth/oauth_fail.html", data); + } + data.replace("%HANDLE%", m_handle.toLocal8Bit()); + qDebug().noquote() << "Result html:" << data; + mime_type = "text/html"; + + server->clearTimeout(); + // delete after 5 sec. + QTimer::singleShot(5 * 1000, [=]() { + if (alive) { + server->deleteLater(); + } else { + qDebug().noquote() << "Already deleted server"; + } + }); + }); + connect(server, &SimpleHttpServer::timeout, this, [=]() { + // token取得に進んでたらfinishedは発火しない + if (m_code.isEmpty()) { + qDebug().noquote() << "Authorization timeout"; + emit finished(false); + } + server->deleteLater(); + }); + connect(server, &QObject::destroyed, [this]() { + qDebug().noquote() << "Destory webserver"; + m_listenPort.clear(); + }); + + server->setTimeout(redirectTimeout()); + quint16 port = server->listen(QHostAddress::LocalHost, 0); + m_listenPort = QString::number(port); + + qDebug().noquote() << "Listen" << m_listenPort; +} + +QByteArray Authorization::makeRequestTokenPayload(bool refresh) +{ + QUrlQuery query; + + if (refresh) { + query.addQueryItem("grant_type", "refresh_token"); + query.addQueryItem("refresh_token", token().refresh_token); + query.addQueryItem("client_id", simplyEncode(m_clientId)); + } else { + query.addQueryItem("grant_type", "authorization_code"); + query.addQueryItem("code", m_code); + query.addQueryItem("code_verifier", m_codeVerifier); + query.addQueryItem("client_id", simplyEncode(m_clientId)); + query.addQueryItem("redirect_uri", simplyEncode(m_redirectUri)); + } + + return query.query(QUrl::FullyEncoded).toLocal8Bit(); +} + +void Authorization::requestToken(bool refresh) +{ + if (tokenEndopoint().isEmpty()) + return; + + AtProtocolInterface::AccountData account; + account.service = tokenEndopoint(); + + OauthRequestToken *req = new OauthRequestToken(this); + connect(req, &OauthRequestToken::finished, this, [=](bool success) { + bool ret = false; + if (success) { + if (!req->tokenResponse().access_token.isEmpty() + && !req->tokenResponse().refresh_token.isEmpty() + && req->tokenResponse().token_type.toLower() == "dpop") { + + if (!req->dPopNonce().isEmpty()) { + setDPopNonce(req->dPopNonce()); + } + setToken(req->tokenResponse()); + + qDebug().noquote() << "--- Success oauth ----"; + qDebug().noquote() << " handle :" << m_handle; + qDebug().noquote() << " access :" << m_token.access_token; + qDebug().noquote() << " refresh:" << m_token.refresh_token; + qDebug().noquote() << req->replyJson(); + qDebug().noquote() << "----------------------"; + // finish oauth sequence + ret = true; + } else { + emit errorOccured("Invalid token response", req->replyJson()); + } + } else { + emit errorOccured(req->errorCode(), req->errorMessage()); + } + emit finished(ret); + req->deleteLater(); + }); + req->appendRawHeader("DPoP", + JsonWebToken::generate(tokenEndopoint(), clientId(), "POST", dPopNonce())); + req->setContentType("application/x-www-form-urlencoded"); + req->setAccount(account); + req->requestToken(makeRequestTokenPayload(refresh)); +} + +QByteArray Authorization::generateRandomValues() const +{ + QByteArray values; +#ifdef HAGOROMO_UNIT_TEST_ + const uint8_t base[] = { 116, 24, 223, 180, 151, 153, 224, 37, 79, 250, 96, + 125, 216, 173, 187, 186, 22, 212, 37, 77, 105, 214, + 191, 240, 91, 88, 5, 88, 83, 132, 141, 121 }; + for (int i = 0; i < sizeof(base); i++) { + values.append(base[i]); + } +#else + for (int i = 0; i < 32; i++) { + values.append(static_cast(QRandomGenerator::global()->bounded(256))); + } +#endif + return values; +} + +QString Authorization::simplyEncode(QString text) const +{ + return text.replace("%", "%25").replace(":", "%3A").replace("/", "%2F").replace("?", "%3F"); +} + +QString Authorization::serviceEndpoint() const +{ + return m_serviceEndpoint; +} + +void Authorization::setServiceEndpoint(const QString &newServiceEndpoint) +{ + if (m_serviceEndpoint == newServiceEndpoint) + return; + m_serviceEndpoint = newServiceEndpoint; + emit serviceEndpointChanged(); +} + +QString Authorization::authorizationServer() const +{ + return m_authorizationServer; +} + +void Authorization::setAuthorizationServer(const QString &newAuthorizationServer) +{ + if (m_authorizationServer == newAuthorizationServer) + return; + m_authorizationServer = newAuthorizationServer; + emit authorizationServerChanged(); +} + +QString Authorization::pushedAuthorizationRequestEndpoint() const +{ + return m_pushedAuthorizationRequestEndpoint; +} + +void Authorization::setPushedAuthorizationRequestEndpoint( + const QString &newPushedAuthorizationRequestEndpoint) +{ + if (m_pushedAuthorizationRequestEndpoint == newPushedAuthorizationRequestEndpoint) + return; + m_pushedAuthorizationRequestEndpoint = newPushedAuthorizationRequestEndpoint; + emit pushedAuthorizationRequestEndpointChanged(); +} + +QString Authorization::authorizationEndpoint() const +{ + return m_authorizationEndpoint; +} + +void Authorization::setAuthorizationEndpoint(const QString &newAuthorizationEndpoint) +{ + if (m_authorizationEndpoint == newAuthorizationEndpoint) + return; + m_authorizationEndpoint = newAuthorizationEndpoint; + emit authorizationEndpointChanged(); +} + +QString Authorization::tokenEndopoint() const +{ + return m_tokenEndopoint; +} + +void Authorization::setTokenEndopoint(const QString &newTokenEndopoint) +{ + if (m_tokenEndopoint == newTokenEndopoint) + return; + m_tokenEndopoint = newTokenEndopoint; + emit tokenEndopointChanged(); +} + +int Authorization::redirectTimeout() const +{ + return m_redirectTimeout; +} + +void Authorization::setRedirectTimeout(int newRedirectTimeout) +{ + m_redirectTimeout = newRedirectTimeout; +} + +AtProtocolType::OauthDefs::TokenResponse Authorization::token() const +{ + return m_token; +} + +void Authorization::setToken(const AtProtocolType::OauthDefs::TokenResponse &newToken) +{ + if (m_token.access_token == newToken.access_token && m_token.expires_in == newToken.expires_in + && m_token.refresh_token == newToken.refresh_token + && m_token.token_type == newToken.token_type && m_token.sub == newToken.sub + && m_token.scope == newToken.scope) + return; + m_token = newToken; + emit tokenChanged(); +} + +QString Authorization::clientId() const +{ + return m_clientId; +} + +void Authorization::setClientId(const QString &newClientId) +{ + m_clientId = newClientId; +} + +QString Authorization::dPopNonce() const +{ + return m_dPopNonce; +} + +void Authorization::setDPopNonce(const QString &newDPopNonce) +{ + m_dPopNonce = newDPopNonce; +} + +QByteArray Authorization::codeChallenge() const +{ + return m_codeChallenge; +} + +QByteArray Authorization::codeVerifier() const +{ + return m_codeVerifier; +} diff --git a/lib/tools/authorization.h b/lib/tools/authorization.h new file mode 100644 index 00000000..0a56649f --- /dev/null +++ b/lib/tools/authorization.h @@ -0,0 +1,105 @@ +#ifndef AUTHORIZATION_H +#define AUTHORIZATION_H + +#include +#include +#include "atprotocol/lexicons.h" + +class Authorization : public QObject +{ + Q_OBJECT +public: + explicit Authorization(QObject *parent = nullptr); + + void reset(); + + void start(const QString &pds, const QString &handle); + + void makeClientId(); + void makeCodeChallenge(); + QByteArray makeParPayload(); + void par(); + void authorization(const QString &request_uri); + void startRedirectServer(); + + QByteArray makeRequestTokenPayload(bool refresh); + void requestToken(bool refresh = false); + + QString serviceEndpoint() const; + void setServiceEndpoint(const QString &newServiceEndpoint); + QString authorizationServer() const; + void setAuthorizationServer(const QString &newAuthorizationServer); + QString pushedAuthorizationRequestEndpoint() const; + void + setPushedAuthorizationRequestEndpoint(const QString &newPushedAuthorizationRequestEndpoint); + QString authorizationEndpoint() const; + void setAuthorizationEndpoint(const QString &newAuthorizationEndpoint); + QString tokenEndopoint() const; + void setTokenEndopoint(const QString &newTokenEndopoint); + int redirectTimeout() const; + void setRedirectTimeout(int newRedirectTimeout); + AtProtocolType::OauthDefs::TokenResponse token() const; + void setToken(const AtProtocolType::OauthDefs::TokenResponse &newToken); + + QString clientId() const; + void setClientId(const QString &newClientId); + QString dPopNonce() const; + void setDPopNonce(const QString &newDPopNonce); + + QByteArray codeVerifier() const; + QByteArray codeChallenge() const; + + QString listenPort() const; + void setListenPort(const QString &newListenPort); + QByteArray state() const; + +signals: + void errorOccured(const QString &code, const QString &message); + void serviceEndpointChanged(); + void authorizationServerChanged(); + void pushedAuthorizationRequestEndpointChanged(); + void authorizationEndpointChanged(); + void tokenEndopointChanged(); + void tokenChanged(); + void finished(bool success); + void madeRequestUrl(const QString &url); // このシグナルを受けてブラウザに飛ばすなりする + +private: + QByteArray generateRandomValues() const; + QString simplyEncode(QString text) const; + + // server info + void requestOauthProtectedResource(); + void requestOauthAuthorizationServer(); + bool + validateServerMetadata(const AtProtocolType::WellKnownDefs::ServerMetadata &server_metadata, + QString &error_message); + + // user + QString m_handle; + // server info + QString m_serviceEndpoint; + QString m_authorizationServer; + // server meta data + QString m_pushedAuthorizationRequestEndpoint; + QString m_authorizationEndpoint; + QString m_tokenEndopoint; + QStringList m_scopesSupported; + // + QString m_redirectUri; + QString m_clientId; + QString m_dPopNonce; + // par + QByteArray m_codeChallenge; + QByteArray m_codeVerifier; + QByteArray m_state; + // request token + QByteArray m_code; + AtProtocolType::OauthDefs::TokenResponse m_token; + + QString m_listenPort; + int m_redirectTimeout; + QMimeDatabase m_MimeDb; +}; + +#endif // AUTHORIZATION_H diff --git a/lib/tools/base32.cpp b/lib/tools/base32.cpp index 14ab7d5d..4c4e7560 100644 --- a/lib/tools/base32.cpp +++ b/lib/tools/base32.cpp @@ -27,3 +27,15 @@ QString Base32::encode(const QByteArray &data, const bool with_padding) return result; } + +QString Base32::encode_s(qint64 num) +{ + static const char alphabet[] = "234567abcdefghijklmnopqrstuvwxyz"; + QString result; + + for (int i = 0; i < 13; i++) { + result.insert(0, QChar(alphabet[num & 0x1F])); + num >>= 5; + } + return result; +} diff --git a/lib/tools/base32.h b/lib/tools/base32.h index 10c7c708..dce0d972 100644 --- a/lib/tools/base32.h +++ b/lib/tools/base32.h @@ -8,6 +8,7 @@ class Base32 { public: static QString encode(const QByteArray &data, const bool with_padding = false); + static QString encode_s(qint64 num); }; #endif // BASE32_H diff --git a/app/qtquick/encryption.cpp b/lib/tools/encryption.cpp similarity index 100% rename from app/qtquick/encryption.cpp rename to lib/tools/encryption.cpp diff --git a/app/qtquick/encryption.h b/lib/tools/encryption.h similarity index 100% rename from app/qtquick/encryption.h rename to lib/tools/encryption.h diff --git a/app/qtquick/encryption_seed_template.h b/lib/tools/encryption_seed_template.h similarity index 100% rename from app/qtquick/encryption_seed_template.h rename to lib/tools/encryption_seed_template.h diff --git a/lib/tools/es256.cpp b/lib/tools/es256.cpp new file mode 100644 index 00000000..4be3ab57 --- /dev/null +++ b/lib/tools/es256.cpp @@ -0,0 +1,199 @@ +#include "es256.h" + +#include +#include +#include +#include +#include + +Es256::Es256() : m_pKey(nullptr) +{ + qDebug().noquote() << this << "Es256()"; +} + +Es256::~Es256() +{ + qDebug().noquote() << this << "~Es256()"; +} + +Es256 *Es256::getInstance() +{ + static Es256 instance; + return &instance; +} + +void Es256::clear() +{ + if (m_pKey != nullptr) { + EVP_PKEY_free(m_pKey); + m_pKey = nullptr; + } +} + +QByteArray Es256::sign(const QByteArray &data) +{ + loadKey(); + if (m_pKey == nullptr) { + return QByteArray(); + } + + QByteArray der_sig; + + // ECDSA署名を生成 + EVP_MD_CTX *mdctx = EVP_MD_CTX_new(); + if (EVP_DigestSignInit(mdctx, nullptr, EVP_sha256(), nullptr, m_pKey) <= 0) { + qWarning() << "Failed to initialize digest sign"; + EVP_MD_CTX_free(mdctx); + // EVP_PKEY_free(pkey); + return QByteArray(); + } + + if (EVP_DigestSignUpdate(mdctx, data.constData(), data.size()) <= 0) { + qWarning() << "Failed to update digest sign"; + EVP_MD_CTX_free(mdctx); + // EVP_PKEY_free(pkey); + return QByteArray(); + } + + size_t sigLen; + if (EVP_DigestSignFinal(mdctx, nullptr, &sigLen) <= 0) { + qWarning() << "Failed to finalize digest sign 1"; + EVP_MD_CTX_free(mdctx); + // EVP_PKEY_free(pkey); + return QByteArray(); + } + + der_sig.resize(sigLen); + if (EVP_DigestSignFinal(mdctx, reinterpret_cast(der_sig.data()), &sigLen) + <= 0) { + qWarning() << "Failed to finalize digest sign 2"; + return QByteArray(); + } + + // Convert DER to IEEE P1363 + const int ec_sig_len = 64; // ES256のときの値 + const unsigned char *temp = reinterpret_cast(der_sig.constData()); + ECDSA_SIG *ec_sig = d2i_ECDSA_SIG(NULL, &temp, der_sig.length()); + if (ec_sig == NULL) { + return QByteArray(); + } + + const BIGNUM *ec_sig_r = NULL; + const BIGNUM *ec_sig_s = NULL; + ECDSA_SIG_get0(ec_sig, &ec_sig_r, &ec_sig_s); + + QByteArray rr = bn2ba(ec_sig_r); + QByteArray ss = bn2ba(ec_sig_s); + + if (rr.size() > (ec_sig_len / 2) || ss.size() > (ec_sig_len / 2)) { + return QByteArray(); + } + rr.insert(0, ec_sig_len / 2 - rr.size(), '\0'); + ss.insert(0, ec_sig_len / 2 - ss.size(), '\0'); + + EVP_MD_CTX_free(mdctx); + + return rr + ss; +} + +void Es256::getAffineCoordinates(QByteArray &x_coord, QByteArray &y_coord) +{ + loadKey(); + + EC_KEY *ec_key = EVP_PKEY_get1_EC_KEY(m_pKey); + if (!ec_key) { + qWarning() << "Failed to get EC key from EVP_PKEY"; + } + + const EC_GROUP *group = EC_KEY_get0_group(ec_key); + const EC_POINT *point = EC_KEY_get0_public_key(ec_key); + BIGNUM *x = BN_new(); + BIGNUM *y = BN_new(); + if (!EC_POINT_get_affine_coordinates(group, point, x, y, nullptr)) { + qWarning() << "Failed to get affine coordinates"; + x_coord.clear(); + y_coord.clear(); + } else { + x_coord = bn2ba(x).toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); + y_coord = bn2ba(y).toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); + } + BN_free(x); + BN_free(y); +} + +void Es256::loadKey() +{ + if (m_pKey != nullptr) { + return; + } + + QString path = + QString("%1/%2/%3%4/private_key.pem") + .arg(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation)) + .arg(QCoreApplication::organizationName()) + .arg(QCoreApplication::applicationName()) + .arg( +#if defined(QT_DEBUG) + QStringLiteral("_debug") +#else + QString() +#endif + ); + + if (!QFile::exists(path)) { + createPrivateKey(path); + } else { + FILE *fp = fopen(path.toStdString().c_str(), "r"); + if (!fp) { + qWarning() << "Failed to open private key file"; + } else { + m_pKey = PEM_read_PrivateKey(fp, nullptr, nullptr, nullptr); + fclose(fp); + } + } +} + +void Es256::createPrivateKey(const QString &path) +{ + if (m_pKey != nullptr) { + return; + } + + EVP_PKEY_CTX *pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr); + if (pctx == nullptr) { + return; + } + + if (EVP_PKEY_keygen_init(pctx) <= 0) { + qWarning() << "Failed to initialize keygen context"; + } else if (EVP_PKEY_CTX_set_ec_paramgen_curve_nid(pctx, NID_X9_62_prime256v1) <= 0) { + qWarning() << "Failed to set curve parameter"; + } else if (EVP_PKEY_keygen(pctx, &m_pKey) <= 0) { + qWarning() << "Failed to generate EC key"; + m_pKey = nullptr; + } else { + FILE *fp = fopen(path.toStdString().c_str(), "w"); + if (!fp) { + qWarning() << "Failed to open private key file"; + } else { + if (PEM_write_PrivateKey(fp, m_pKey, nullptr, nullptr, 0, nullptr, nullptr) <= 0) { + qWarning() << "Failed to write private key to file"; + } + fclose(fp); + } + } + EVP_PKEY_CTX_free(pctx); +} + +QByteArray Es256::bn2ba(const BIGNUM *bn) +{ + QByteArray ba; + ba.resize(BN_num_bytes(bn)); + BN_bn2bin(bn, reinterpret_cast(ba.data())); + return ba; +} + +EVP_PKEY *Es256::pKey() const +{ + return m_pKey; +} diff --git a/lib/tools/es256.h b/lib/tools/es256.h new file mode 100644 index 00000000..38879ae4 --- /dev/null +++ b/lib/tools/es256.h @@ -0,0 +1,33 @@ +#ifndef ES256_H +#define ES256_H + +#include + +#include +#include +#include +#include +#include + +class Es256 +{ + explicit Es256(); + ~Es256(); + +public: + static Es256 *getInstance(); + + void clear(); + void loadKey(); + QByteArray sign(const QByteArray &data); + void getAffineCoordinates(QByteArray &x_coord, QByteArray &y_coord); + EVP_PKEY *pKey() const; + +private: + void createPrivateKey(const QString &path); + QByteArray bn2ba(const BIGNUM *bn); + + EVP_PKEY *m_pKey; +}; + +#endif // ES256_H diff --git a/lib/tools/jsonwebtoken.cpp b/lib/tools/jsonwebtoken.cpp new file mode 100644 index 00000000..5816fa01 --- /dev/null +++ b/lib/tools/jsonwebtoken.cpp @@ -0,0 +1,70 @@ +#include "jsonwebtoken.h" +#include "es256.h" + +#include +#include +#include +#include +#include + +inline QByteArray base64UrlEncode(const QByteArray &data) +{ + QByteArray encoded = + data.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); + return encoded; +} + +inline QJsonObject createJwk() +{ + QJsonObject jwk; + + QByteArray x_coord; + QByteArray y_coord; + Es256::getInstance()->getAffineCoordinates(x_coord, y_coord); + + if (!x_coord.isEmpty() && !y_coord.isEmpty()) { + jwk["kty"] = "EC"; // Key Type + jwk["crv"] = "P-256"; // Curve + jwk["x"] = QString::fromUtf8(x_coord); + jwk["y"] = QString::fromUtf8(y_coord); + } + + return jwk; +} + +QByteArray JsonWebToken::generate(const QString &endpoint, const QString &client_id, + const QString &method, const QString &nonce) +{ + // ヘッダー + QJsonObject header; + header["alg"] = "ES256"; + header["typ"] = "dpop+jwt"; + header["jwk"] = createJwk(); + QByteArray headerJson = QJsonDocument(header).toJson(QJsonDocument::Compact); + QByteArray headerBase64 = base64UrlEncode(headerJson); + + // ペイロード + qint64 epoch = QDateTime::currentSecsSinceEpoch(); + QJsonObject payload; + payload["iss"] = "tech/relog/hagoromo"; + payload["sub"] = client_id; + payload["htu"] = endpoint; + payload["htm"] = method; + payload["exp"] = epoch + 60000; + payload["jti"] = QString(QString::number(epoch).toUtf8().toBase64()); + payload["iat"] = epoch; // 発行時間 + if (!nonce.isEmpty()) { + payload["nonce"] = nonce; + } + QByteArray payloadJson = QJsonDocument(payload).toJson(QJsonDocument::Compact); + QByteArray payloadBase64 = base64UrlEncode(payloadJson); + + // 署名 + QByteArray message = headerBase64 + "." + payloadBase64; + QByteArray signature = Es256::getInstance()->sign(message); + QByteArray signatureBase64 = base64UrlEncode(signature); + + // JWTトークン + QByteArray jwt = headerBase64 + "." + payloadBase64 + "." + signatureBase64; + return jwt; +} diff --git a/lib/tools/jsonwebtoken.h b/lib/tools/jsonwebtoken.h new file mode 100644 index 00000000..213cb013 --- /dev/null +++ b/lib/tools/jsonwebtoken.h @@ -0,0 +1,13 @@ +#ifndef JSONWEBTOKEN_H +#define JSONWEBTOKEN_H + +#include + +class JsonWebToken +{ +public: + static QByteArray generate(const QString &endpoint, const QString &client_id, + const QString &method, const QString &nonce); +}; + +#endif // JSONWEBTOKEN_H diff --git a/lib/tools/oauth/client-metadata.json b/lib/tools/oauth/client-metadata.json new file mode 100644 index 00000000..91dd5f4d --- /dev/null +++ b/lib/tools/oauth/client-metadata.json @@ -0,0 +1,19 @@ +{ + "client_id": "https://oauth.hagoromo.relog.tech/client-metadata.json", + "application_type": "native", + "client_name": "Hagoromo", + "client_uri": "https://oauth.hagoromo.relog.tech", + "dpop_bound_access_tokens": true, + "grant_types": [ + "authorization_code", + "refresh_token" + ], + "redirect_uris": [ + "http://127.0.0.1/tech/relog/hagoromo/oauth-callback" + ], + "response_types": [ + "code" + ], + "scope": "atproto transition:generic transition:chat.bsky", + "token_endpoint_auth_method": "none" +} diff --git a/lib/tools/oauth/oauth_fail.html b/lib/tools/oauth/oauth_fail.html new file mode 100644 index 00000000..a5093494 --- /dev/null +++ b/lib/tools/oauth/oauth_fail.html @@ -0,0 +1,30 @@ + + + + + Hagoromo + + + + +

羽衣 -Hagoromo-

+
+

%HANDLE%」の認証処理で失敗しました。

+

このウインドウを閉じて羽衣へ戻ってください。

+
+ +
+

The authentication process for "%HANDLE%" failed.

+

Please close this window and return to Hagoromo.

+
+ + + diff --git a/lib/tools/oauth/oauth_success.html b/lib/tools/oauth/oauth_success.html new file mode 100644 index 00000000..744f4990 --- /dev/null +++ b/lib/tools/oauth/oauth_success.html @@ -0,0 +1,30 @@ + + + + + Hagoromo + + + + +

羽衣 -Hagoromo-

+
+

%HANDLE%」が羽衣でBlueskyを使用する認証処理を実施中です。

+

このウインドウを閉じて羽衣へ戻ってください。

+
+ +
+

Hagoromo is currently processing authentication using Bluesky for "%HANDLE%".

+

Please close this window and return to Hagoromo.

+
+ + + diff --git a/lib/tools/tid.cpp b/lib/tools/tid.cpp new file mode 100644 index 00000000..04988545 --- /dev/null +++ b/lib/tools/tid.cpp @@ -0,0 +1,26 @@ +#include "tid.h" +#include "base32.h" +#include +#include +#include + +QString Tid::next() +{ + static qint64 prev_time = 0; + static qint64 prev = 0; + qint64 current_time = QDateTime::currentMSecsSinceEpoch(); + if (current_time <= prev_time) { + current_time = prev_time + 1; + } + prev_time = current_time; + + qint64 clock_id = QRandomGenerator::global()->bounded(0, 1024); + qint64 id = (((current_time * 1000) << 10) & 0x7FFFFFFFFFFFFC00) | (clock_id & 0x3FF); + + if (prev == id) { + prev_time = current_time++; + id = (((current_time * 1000) << 10) & 0x7FFFFFFFFFFFFC00) | (clock_id & 0x3FF); + } + prev = id; + return Base32::encode_s(id); +} diff --git a/lib/tools/tid.h b/lib/tools/tid.h new file mode 100644 index 00000000..511ad38b --- /dev/null +++ b/lib/tools/tid.h @@ -0,0 +1,12 @@ +#ifndef TID_H +#define TID_H + +#include + +class Tid +{ +public: + static QString next(); +}; + +#endif // TID_H diff --git a/openssl/openssl.pri b/openssl/openssl.pri index 85d486ba..5eb86061 100644 --- a/openssl/openssl.pri +++ b/openssl/openssl.pri @@ -1,11 +1,14 @@ - -bin_dir=$$dirname(QMAKE_QMAKE) -open_ssl_dir=$${bin_dir}/../../../Tools/OpenSSL -open_ssl_dir=$$clean_path($$open_ssl_dir) - win32:{ - open_ssl_dir=$${open_ssl_dir}/Win_x64 + bin_dir=$$dirname(QMAKE_QMAKE) + greaterThan(QT_MAJOR_VERSION, 5) { + open_ssl_dir=$${bin_dir}/../../../Tools/OpenSSLv3 + }else{ + open_ssl_dir=$${bin_dir}/../../../Tools/OpenSSL + } + open_ssl_dir=$$clean_path($$open_ssl_dir) + SOURCES += $${open_ssl_dir}/src/ms/applink.c + open_ssl_dir=$${open_ssl_dir}/Win_x64 LIBS += $${open_ssl_dir}/lib/libssl.lib \ $${open_ssl_dir}/lib/libcrypto.lib INCLUDEPATH += $${open_ssl_dir}/include @@ -14,15 +17,28 @@ win32:{ else: install_dir = $$OUT_PWD/release depend_files.path = $$install_dir - depend_files.files = \ - $${open_ssl_dir}/bin/libcrypto-1_1-x64.dll \ - $${open_ssl_dir}/bin/libssl-1_1-x64.dll - + greaterThan(QT_MAJOR_VERSION, 5) { + depend_files.files = \ + $${open_ssl_dir}/bin/libcrypto-3-x64.dll \ + $${open_ssl_dir}/bin/libssl-3-x64.dll + }else{ + depend_files.files = \ + $${open_ssl_dir}/bin/libcrypto-1_1-x64.dll \ + $${open_ssl_dir}/bin/libssl-1_1-x64.dll + } INSTALLS += depend_files # QMAKE_POST_LINK += nmake -f $(MAKEFILE) install -} -unix: { + +}else{ open_ssl_dir=$${PWD} INCLUDEPATH += $${open_ssl_dir}/include - LIBS += -L$${open_ssl_dir}/lib -lssl -lcrypto + mac:{ + LIBS += -L$${open_ssl_dir}/lib -lssl -lcrypto + }else{ + greaterThan(QT_MAJOR_VERSION, 5) { + LIBS += -L$${open_ssl_dir}/lib64 -lssl -lcrypto + }else{ + LIBS += -L$${open_ssl_dir}/lib -lssl -lcrypto + } + } } diff --git a/scripts/build.sh b/scripts/build.sh index 11787ce9..9cdcf7a1 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -80,8 +80,13 @@ deploy_hagoromo(){ cp ${build_dir}/app/Hagoromo ${work_dir}/bin cp ${SCRIPT_FOLDER}/deploy/Hagoromo.sh ${work_dir} - cp "openssl/lib/libcrypto.so.1.1" ${work_dir}/lib - cp "openssl/lib/libssl.so.1.1" ${work_dir}/lib + if [ $QT_VERSION == 6 ]; then + cp "openssl/lib64/libcrypto.so.3" ${work_dir}/lib + cp "openssl/lib64/libssl.so.3" ${work_dir}/lib + else + cp "openssl/lib/libcrypto.so.1.1" ${work_dir}/lib + cp "openssl/lib/libssl.so.1.1" ${work_dir}/lib + fi cp "zlib/lib/libz.so.1.3.1" ${work_dir}/lib cp "zlib/lib/libz.so.1" ${work_dir}/lib cp "app/i18n/app_ja.qm" ${work_dir}/bin/translations @@ -125,6 +130,12 @@ ROOT_FOLDER=$(pwd) PLATFORM_TYPE=$1 QT_BIN_FOLDER=$2 +if [[ "$QT_BIN_FOLDER" == */6.* ]]; then +QT_VERSION=6 +else +QT_VERSION=5 +fi + if [ -z "${QT_BIN_FOLDER}" ] || [ -z "${PLATFORM_TYPE}" ]; then echo "usage $(basename $0) PLATFORM_TYPE QT_BIN_FOLDER" echo " PLATFORM_TYPE linux or mac" diff --git a/scripts/defs2struct.py b/scripts/defs2struct.py index 0ff47338..54312f77 100644 --- a/scripts/defs2struct.py +++ b/scripts/defs2struct.py @@ -122,7 +122,9 @@ def __init__(self) -> None: 'AppBskyGraphList::Main', 'AppBskyFeedThreadgate::Main', 'AppBskyFeedPostgate::Main', - 'ComWhtwndBlogEntry::Main' + 'ComWhtwndBlogEntry::Main', + 'BlueLinkatBoard::Main', + 'DirectoryPlcDefs::DidDoc', ) self.inheritance = { 'app.bsky.actor.defs#profileView': { @@ -164,7 +166,6 @@ def __init__(self) -> None: 'com.atproto.identity.', 'com.atproto.label.', 'com.atproto.repo.applyWrites', - 'com.atproto.repo.describeRepo', 'com.atproto.repo.importRepo', 'com.atproto.repo.uploadBlob', 'com.atproto.repo.listMissingBlobs', @@ -219,6 +220,7 @@ def __init__(self) -> None: self.unuse_auth = [ 'com.atproto.server.createSession', 'com.atproto.sync.getBlob', + 'com.atproto.repo.describeRepo', 'com.atproto.repo.listRecords' ] self.need_extension = [ @@ -249,6 +251,9 @@ def to_namespace_style(self, name: str) -> str: # app.bsky.embed.recordWithMediaがあるのでcapitalize()は使えない return ''.join(dest) + def to_property_style(self, name: str) -> str: + return name.replace('.', '_') + def to_header_path(self, namespace: str) -> str: srcs = namespace.split('.') dest = ['atprotocol'] @@ -538,13 +543,13 @@ def output_type(self, namespace: str, type_name: str, obj: dict): (temp_pointer, temp_list_pointer, temp_enum) = self.output_union(namespace, type_name, property_name, properties[property_name].get('refs', []), p_comment) enum_text.extend(temp_enum) elif p_type == 'unknown': - self.output_text[namespace].append(' QVariant %s;%s' % (property_name, p_comment, )) + self.output_text[namespace].append(' QVariant %s;%s' % (self.to_property_style(property_name), p_comment, )) elif p_type == 'integer': - self.output_text[namespace].append(' int %s = 0;%s' % (property_name, p_comment, )) + self.output_text[namespace].append(' int %s = 0;%s' % (self.to_property_style(property_name), p_comment, )) elif p_type == 'boolean': - self.output_text[namespace].append(' bool %s = false;%s' % (property_name, p_comment, )) + self.output_text[namespace].append(' bool %s = false;%s' % (self.to_property_style(property_name), p_comment, )) elif p_type == 'string': - self.output_text[namespace].append(' QString %s;%s' % (property_name, p_comment, )) + self.output_text[namespace].append(' QString %s;%s' % (self.to_property_style(property_name), p_comment, )) elif p_type == 'array': items_type = properties[property_name].get('items', {}).get('type', '') if items_type == 'ref': @@ -553,13 +558,13 @@ def output_type(self, namespace: str, type_name: str, obj: dict): (temp_pointer, temp_list_pointer, temp_enum) = self.output_union(namespace, type_name, property_name, properties[property_name].get('items', {}).get('refs', []), p_comment, True) enum_text.extend(temp_enum) elif items_type == 'integer': - self.output_text[namespace].append(' QList %s;%s' % (property_name, p_comment, )) + self.output_text[namespace].append(' QList %s;%s' % (self.to_property_style(property_name), p_comment, )) elif items_type == 'boolean': - self.output_text[namespace].append(' QList %s;%s' % (property_name, p_comment, )) + self.output_text[namespace].append(' QList %s;%s' % (self.to_property_style(property_name), p_comment, )) elif items_type == 'string': - self.output_text[namespace].append(' QList %s;%s' % (property_name, p_comment, )) + self.output_text[namespace].append(' QList %s;%s' % (self.to_property_style(property_name), p_comment, )) elif p_type == 'blob': - self.output_text[namespace].append(' Blob %s;%s' % (property_name, p_comment, )) + self.output_text[namespace].append(' Blob %s;%s' % (self.to_property_style(property_name), p_comment, )) self.output_text[namespace].append('};') @@ -689,16 +694,16 @@ def output_function(self, namespace: str, type_name: str, obj: dict): extend_ns = '%s::' % (self.to_namespace_style(ref_namespace), ) forward_type = self.history_type[ref_namespace + '#' + ref_type_name] if self.check_pointer(namespace, type_name, property_name, ref_namespace, ref_type_name): - self.output_func_text[namespace].append(' if (dest.%s.isNull())' % (property_name, )) - self.output_func_text[namespace].append(' dest.%s = QSharedPointer<%s%s>(new %s%s());' % (property_name, extend_ns, self.to_struct_style(ref_type_name), extend_ns, self.to_struct_style(ref_type_name), )) - self.output_func_text[namespace].append(' %scopy%s(src.value("%s").toObject(), *dest.%s);' % (extend_ns, self.to_struct_style(ref_type_name), property_name, property_name, )) + self.output_func_text[namespace].append(' if (dest.%s.isNull())' % (self.to_property_style(property_name), )) + self.output_func_text[namespace].append(' dest.%s = QSharedPointer<%s%s>(new %s%s());' % (self.to_property_style(property_name), extend_ns, self.to_struct_style(ref_type_name), extend_ns, self.to_struct_style(ref_type_name), )) + self.output_func_text[namespace].append(' %scopy%s(src.value("%s").toObject(), *dest.%s);' % (extend_ns, self.to_struct_style(ref_type_name), property_name, self.to_property_style(property_name), )) else: if forward_type in ['integer', 'string', 'boolean']: convert_method = '' else: convert_method = '.toObject()' self.output_func_text[namespace].append(' %scopy%s(src.value("%s")%s, dest.%s);' % ( - extend_ns, self.to_struct_style(ref_type_name), property_name, convert_method, property_name,)) + extend_ns, self.to_struct_style(ref_type_name), property_name, convert_method, self.to_property_style(property_name),)) elif p_type == 'union': value_key: str = '$type' @@ -742,16 +747,16 @@ def output_function(self, namespace: str, type_name: str, obj: dict): self.output_func_text[namespace].append(' }') elif p_type == 'unknown': - self.output_func_text[namespace].append(' LexiconsTypeUnknown::copyUnknown(src.value("%s").toObject(), dest.%s);' % (property_name, property_name, )) + self.output_func_text[namespace].append(' LexiconsTypeUnknown::copyUnknown(src.value("%s").toObject(), dest.%s);' % (property_name, self.to_property_style(property_name), )) elif p_type == 'integer': - self.output_func_text[namespace].append(' dest.%s = src.value("%s").toInt();' % (property_name, property_name, )) + self.output_func_text[namespace].append(' dest.%s = src.value("%s").toInt();' % (self.to_property_style(property_name), property_name, )) elif p_type == 'boolean': - self.output_func_text[namespace].append(' dest.%s = src.value("%s").toBool();' % (property_name, property_name, )) + self.output_func_text[namespace].append(' dest.%s = src.value("%s").toBool();' % (self.to_property_style(property_name), property_name, )) elif p_type == 'string': - self.output_func_text[namespace].append(' dest.%s = src.value("%s").toString();' % (property_name, property_name, )) + self.output_func_text[namespace].append(' dest.%s = src.value("%s").toString();' % (self.to_property_style(property_name), property_name, )) elif p_type == 'array': items_type = properties[property_name].get('items', {}).get('type', '') @@ -768,14 +773,14 @@ def output_function(self, namespace: str, type_name: str, obj: dict): if self.check_pointer(namespace, type_name, property_name, ref_namespace, ref_type_name): self.output_func_text[namespace].append(' QSharedPointer<%s%s> child = QSharedPointer<%s%s>(new %s%s());' % (extend_ns, self.to_struct_style(ref_type_name), extend_ns, self.to_struct_style(ref_type_name), extend_ns, self.to_struct_style(ref_type_name), )) self.output_func_text[namespace].append(' %s(s.toObject(), *child);' % (func_name, )) - self.output_func_text[namespace].append(' dest.%s.append(child);' % (property_name, )) + self.output_func_text[namespace].append(' dest.%s.append(child);' % (self.to_property_style(property_name), )) else: self.output_func_text[namespace].append(' %s%s child;' % (extend_ns, self.to_struct_style(ref_type_name), )) if self.check_object(ref_namespace, 'copy%s' % (self.to_struct_style(ref_type_name), )): self.output_func_text[namespace].append(' %s(s.toObject(), child);' % (func_name, )) else: self.output_func_text[namespace].append(' %s(s, child);' % (func_name, )) - self.output_func_text[namespace].append(' dest.%s.append(child);' % (property_name, )) + self.output_func_text[namespace].append(' dest.%s.append(child);' % (self.to_property_style(property_name), )) self.output_func_text[namespace].append(' }') elif items_type == 'union': @@ -815,20 +820,20 @@ def output_function(self, namespace: str, type_name: str, obj: dict): elif items_type == 'integer': self.output_func_text[namespace].append(' for (const auto &value : src.value("%s").toArray()) {' % (property_name, )) - self.output_func_text[namespace].append(' dest.%s.append(value.toInt());' % (property_name, )) + self.output_func_text[namespace].append(' dest.%s.append(value.toInt());' % (self.to_property_style(property_name), )) self.output_func_text[namespace].append(' }') elif items_type == 'boolean': self.output_func_text[namespace].append(' for (const auto &value : src.value("%s").toArray()) {' % (property_name, )) - self.output_func_text[namespace].append(' dest.%s.append(value.toBool());' % (property_name, )) + self.output_func_text[namespace].append(' dest.%s.append(value.toBool());' % (self.to_property_style(property_name), )) self.output_func_text[namespace].append(' }') elif items_type == 'string': self.output_func_text[namespace].append(' for (const auto &value : src.value("%s").toArray()) {' % (property_name, )) - self.output_func_text[namespace].append(' dest.%s.append(value.toString());' % (property_name, )) + self.output_func_text[namespace].append(' dest.%s.append(value.toString());' % (self.to_property_style(property_name), )) self.output_func_text[namespace].append(' }') elif p_type == 'blob': - self.output_func_text[namespace].append(' LexiconsTypeUnknown::copyBlob(src.value("%s").toObject(), dest.%s);' % (property_name, property_name, )) + self.output_func_text[namespace].append(' LexiconsTypeUnknown::copyBlob(src.value("%s").toObject(), dest.%s);' % (property_name, self.to_property_style(property_name), )) # self.output_text[namespace].append(' Blob %s;' % (property_name, )) self.output_func_text[namespace].append(' }') @@ -935,7 +940,7 @@ def output_function(self, namespace: str, type_name: str, obj: dict): if self.check_pointer(namespace, type_name, '', ref_namespace, ref_type_name): self.output_func_text[namespace].append(' QSharedPointer<%s%s> child = QSharedPointer<%s%s>(new %s%s());' % (extend_ns, self.to_struct_style(ref_type_name), extend_ns, self.to_struct_style(ref_type_name), extend_ns, self.to_struct_style(ref_type_name), )) self.output_func_text[namespace].append(' %s(s.toObject(), *child);' % (func_name, )) - self.output_func_text[namespace].append(' dest.%s.append(child);' % (property_name, )) + self.output_func_text[namespace].append(' dest.%s.append(child);' % (self.to_property_style(property_name), )) else: self.output_func_text[namespace].append(' %s%s child;' % (extend_ns, self.to_struct_style(ref_type_name), )) if self.check_object(ref_namespace, 'copy%s' % (self.to_struct_style(ref_type_name), )): diff --git a/scripts/deploy/linux_lib.txt b/scripts/deploy/linux_lib.txt index d42c0435..bd57c6ad 100644 --- a/scripts/deploy/linux_lib.txt +++ b/scripts/deploy/linux_lib.txt @@ -8,6 +8,8 @@ libQt5Gui.so.5 libQt5Gui.so.5.15.2 libQt5Network.so.5 libQt5Network.so.5.15.2 +libQt5HttpServer.so.5 +libQt5HttpServer.so.5.12.0 libQt5Qml.so.5 libQt5Qml.so.5.15.2 libQt5QmlModels.so.5 diff --git a/scripts/lexicons/app.bsky.feed.post.json b/scripts/lexicons/app.bsky.feed.post.json index 6416c5f6..c1c4c808 100644 --- a/scripts/lexicons/app.bsky.feed.post.json +++ b/scripts/lexicons/app.bsky.feed.post.json @@ -4,6 +4,10 @@ "record": { "properties": { "via": { + "type": "string", + "format": "client name(Unofficial field) old" + }, + "space.aoisora.post.via": { "type": "string", "format": "client name(Unofficial field)" } diff --git a/scripts/lexicons/blue.linkat.board.json b/scripts/lexicons/blue.linkat.board.json new file mode 100644 index 00000000..39e6372d --- /dev/null +++ b/scripts/lexicons/blue.linkat.board.json @@ -0,0 +1,27 @@ +{ + "lexicon": 1, + "id": "blue.linkat.board", + "defs": { + "main": { + "type": "record", + "description": "Record containing a cards of your profile.", + "key": "literal:self", + "record": { + "type": "object", + "required": [ + "cards" + ], + "properties": { + "cards": { + "type": "array", + "description": "List of cards in the board.", + "items": { + "type": "ref", + "ref": "blue.linkat.defs#card" + } + } + } + } + } + } +} diff --git a/scripts/lexicons/blue.linkat.defs.json b/scripts/lexicons/blue.linkat.defs.json new file mode 100644 index 00000000..1ce6714b --- /dev/null +++ b/scripts/lexicons/blue.linkat.defs.json @@ -0,0 +1,23 @@ +{ + "lexicon": 1, + "id": "blue.linkat.defs", + "defs": { + "card": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "URL of the link" + }, + "text": { + "type": "string", + "description": "Text of the card" + }, + "emoji": { + "type": "string", + "description": "Emoji of the card" + } + } + } + } +} diff --git a/scripts/lexicons/directory.plc.log.audit.json.temple b/scripts/lexicons/directory.plc.log.audit.json.template similarity index 100% rename from scripts/lexicons/directory.plc.log.audit.json.temple rename to scripts/lexicons/directory.plc.log.audit.json.template diff --git a/scripts/lexicons/oauth.defs.json b/scripts/lexicons/oauth.defs.json new file mode 100644 index 00000000..158b4f3c --- /dev/null +++ b/scripts/lexicons/oauth.defs.json @@ -0,0 +1,40 @@ +{ + "lexicon": 1, + "id": "oauth.defs", + "defs": { + "pushedAuthorizationResponse": { + "type": "object", + "properties": { + "request_uri": { + "type": "string" + }, + "expires_in": { + "type": "integer" + } + } + }, + "tokenResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "token_type": { + "type": "string" + }, + "refresh_token": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "sub": { + "type": "string" + }, + "expires_in": { + "type": "integer" + } + } + } + } +} diff --git a/scripts/lexicons/oauth.pushedAuthorizationRequest.json.template b/scripts/lexicons/oauth.pushedAuthorizationRequest.json.template new file mode 100644 index 00000000..4b428b4e --- /dev/null +++ b/scripts/lexicons/oauth.pushedAuthorizationRequest.json.template @@ -0,0 +1,16 @@ +{ + "lexicon": 1, + "id": "oauth.pushedAuthorizationRequest", + "defs": { + "main": { + "type": "procedure", + "output": { + "encoding": "application/json", + "schema": { + "type": "ref", + "ref": "oauth.defs#pushedAuthorizationResponse" + } + } + } + } +} diff --git a/scripts/lexicons/oauth.requestToken.json.template b/scripts/lexicons/oauth.requestToken.json.template new file mode 100644 index 00000000..babf70de --- /dev/null +++ b/scripts/lexicons/oauth.requestToken.json.template @@ -0,0 +1,16 @@ +{ + "lexicon": 1, + "id": "oauth.requestToken", + "defs": { + "main": { + "type": "procedure", + "output": { + "encoding": "application/json", + "schema": { + "type": "ref", + "ref": "oauth.defs#tokenResponse" + } + } + } + } +} diff --git a/scripts/lexicons/wellKnown.defs.json b/scripts/lexicons/wellKnown.defs.json new file mode 100644 index 00000000..a0d8f875 --- /dev/null +++ b/scripts/lexicons/wellKnown.defs.json @@ -0,0 +1,112 @@ +{ + "lexicon": 1, + "id": "wellKnown.defs", + "defs": { + "resourceMetadata": { + "type": "object", + "properties": { + "resource": { + "type": "string" + }, + "authorization_servers": { + "type": "array", + "items": { + "type": "string" + } + }, + "scopes_supported": { + "type": "array", + "items": { + "type": "string" + } + }, + "bearer_methods_supported": { + "type": "array", + "items": { + "type": "string" + } + }, + "resource_documentation": { + "type": "string" + } + } + }, + "serverMetadata": { + "type": "object", + "properties": { + "issuer": { + "type": "string" + }, + "response_types_supported": { + "type": "array", + "items": { + "type": "string" + } + }, + "grant_types_supported": { + "type": "array", + "items": { + "type": "string" + } + }, + "code_challenge_methods_supported": { + "type": "array", + "items": { + "type": "string" + } + }, + "token_endpoint_auth_methods_supported": { + "type": "array", + "items": { + "type": "string" + } + }, + "token_endpoint_auth_signing_alg_values_supported": { + "type": "array", + "items": { + "type": "string" + } + }, + "scopes_supported": { + "type": "array", + "items": { + "type": "string" + } + }, + "subject_types_supported": { + "type": "array", + "items": { + "type": "string" + } + }, + "authorization_response_iss_parameter_supported": { + "type": "boolean" + }, + "pushed_authorization_request_endpoint": { + "type": "string" + }, + "token_endpoint": { + "type": "string" + }, + "require_pushed_authorization_requests": { + "type": "boolean" + }, + "dpop_signing_alg_values_supported": { + "type": "array", + "items": { + "type": "string" + } + }, + "require_request_uri_registration": { + "type": "boolean" + }, + "client_id_metadata_document_supported": { + "type": "boolean" + }, + "authorization_endpoint": { + "type": "string" + } + } + } + } +} diff --git a/scripts/lexicons/wellKnown.oauthAuthorizationServer.json.template b/scripts/lexicons/wellKnown.oauthAuthorizationServer.json.template new file mode 100644 index 00000000..acf6d38b --- /dev/null +++ b/scripts/lexicons/wellKnown.oauthAuthorizationServer.json.template @@ -0,0 +1,23 @@ +{ + "lexicon": 1, + "id": "wellKnown.oauthAuthorizationServer", + "defs": { + "main": { + "type": "query", + "parameters": { + "type": "params", + "required": [ + ], + "properties": { + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "ref", + "ref": "wellKnown.defs#serverMetadata" + } + } + } + } +} diff --git a/scripts/lexicons/wellKnown.oauthProtectedResource.json.template b/scripts/lexicons/wellKnown.oauthProtectedResource.json.template new file mode 100644 index 00000000..743af930 --- /dev/null +++ b/scripts/lexicons/wellKnown.oauthProtectedResource.json.template @@ -0,0 +1,23 @@ +{ + "lexicon": 1, + "id": "wellKnown.oauthProtectedResource", + "defs": { + "main": { + "type": "query", + "parameters": { + "type": "params", + "required": [ + ], + "properties": { + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "ref", + "ref": "wellKnown.defs#resourceMetadata" + } + } + } + } +} diff --git a/scripts/unittest.bat b/scripts/unittest.bat index 86cbaac7..21e2a362 100644 --- a/scripts/unittest.bat +++ b/scripts/unittest.bat @@ -63,10 +63,14 @@ if ERRORLEVEL 1 goto TEST_FAIL if ERRORLEVEL 1 goto TEST_FAIL %BUILD_FOLDER%\tests\http_test\debug\http_test.exe if ERRORLEVEL 1 goto TEST_FAIL -%BUILD_FOLDER%\tests\search_test\debug\search_test.exe -if ERRORLEVEL 1 goto TEST_FAIL %BUILD_FOLDER%\tests\log_test\debug\log_test.exe if ERRORLEVEL 1 goto TEST_FAIL +%BUILD_FOLDER%\tests\oauth_test\debug\oauth_test.exe +if ERRORLEVEL 1 goto TEST_FAIL +%BUILD_FOLDER%\tests\realtime_test\debug\realtime_test.exe +if ERRORLEVEL 1 goto TEST_FAIL +%BUILD_FOLDER%\tests\search_test\debug\search_test.exe +if ERRORLEVEL 1 goto TEST_FAIL %BUILD_FOLDER%\tests\tools_test\debug\tools_test.exe if ERRORLEVEL 1 goto TEST_FAIL diff --git a/scripts/unittest.sh b/scripts/unittest.sh index c4207d89..38b0dc76 100755 --- a/scripts/unittest.sh +++ b/scripts/unittest.sh @@ -59,6 +59,8 @@ do_test ${work_dir}/chat_test/chat_test do_test ${work_dir}/hagoromo_test/hagoromo_test do_test ${work_dir}/hagoromo_test2/hagoromo_test2 do_test ${work_dir}/http_test/http_test -do_test ${work_dir}/search_test/search_test do_test ${work_dir}/log_test/log_test +do_test ${work_dir}/oauth_test/oauth_test +do_test ${work_dir}/realtime_test/realtime_test +do_test ${work_dir}/search_test/search_test do_test ${work_dir}/tools_test/tools_test diff --git a/tests/atprotocol_test/tst_atprotocol_test.cpp b/tests/atprotocol_test/tst_atprotocol_test.cpp index ea60273d..9035b05d 100644 --- a/tests/atprotocol_test/tst_atprotocol_test.cpp +++ b/tests/atprotocol_test/tst_atprotocol_test.cpp @@ -102,7 +102,7 @@ private slots: atprotocol_test::atprotocol_test() { QCoreApplication::setOrganizationName(QStringLiteral("relog")); - QCoreApplication::setApplicationName(QStringLiteral("Hagoromo")); + QCoreApplication::setApplicationName(QStringLiteral("Hagoromo_unittest")); m_listenPort = m_mockServer.listen(QHostAddress::LocalHost, 0); m_service = QString("http://localhost:%1/response").arg(m_listenPort); diff --git a/tests/chat_test/tst_chat_test.cpp b/tests/chat_test/tst_chat_test.cpp index be220f27..b5bc48f6 100644 --- a/tests/chat_test/tst_chat_test.cpp +++ b/tests/chat_test/tst_chat_test.cpp @@ -5,6 +5,7 @@ #include "chat/chatlistmodel.h" #include "chat/chatmessagelistmodel.h" #include "tools/chatlogsubscriber.h" +#include "tools/accountmanager.h" class chat_test : public QObject { @@ -32,7 +33,7 @@ private slots: chat_test::chat_test() { QCoreApplication::setOrganizationName(QStringLiteral("relog")); - QCoreApplication::setApplicationName(QStringLiteral("Hagoromo")); + QCoreApplication::setApplicationName(QStringLiteral("Hagoromo_unittest")); m_listenPort = m_mockServer.listen(QHostAddress::LocalHost, 0); m_service = QString("http://localhost:%1/response").arg(m_listenPort); @@ -75,9 +76,12 @@ void chat_test::test_ChatListModel() { int i = 0; ChatListModel model; - model.setAccount(m_service + "/list", "did:plc:ipj5qejfoqu6eukvt72uhyit", "handle", "email", - "accessJwt", "refreshJwt"); - model.setServiceEndpoint(m_service + "/list"); + + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/list", "id", "pass", "did:plc:ipj5qejfoqu6eukvt72uhyit", + "handle", "email", "accessJwt", "refreshJwt", true); + AccountManager::getInstance()->updateServiceEndpoint(uuid, m_service + "/list"); + model.setAccount(uuid); { QSignalSpy spy(&model, SIGNAL(runningChanged())); @@ -136,9 +140,11 @@ void chat_test::test_ChatMessageListModel() ChatMessageListModel model; int i = 0; - model.setAccount(m_service + "/message/1", "did:plc:ipj5qejfoqu6eukvt72uhyit", "handle", - "email", "accessJwt", "refreshJwt"); - model.setServiceEndpoint(m_service + "/message/1"); + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/message/1", "id", "pass", "did:plc:ipj5qejfoqu6eukvt72uhyit", + "handle", "email", "accessJwt", "refreshJwt", true); + AccountManager::getInstance()->updateServiceEndpoint(uuid, m_service + "/message/1"); + model.setAccount(uuid); model.setConvoId("3ksrqt7eebs2b"); model.setAutoLoading(false); @@ -261,9 +267,11 @@ void chat_test::test_ChatMessageListModelByMembers() ChatMessageListModel model; int i = 0; - model.setAccount(m_service + "/message/2", "did:plc:ipj5qejfoqu6eukvt72uhyit", "handle", - "email", "accessJwt", "refreshJwt"); - model.setServiceEndpoint(m_service + "/message/2"); + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/message/2", "id", "pass", "did:plc:ipj5qejfoqu6eukvt72uhyit", + "handle", "email", "accessJwt", "refreshJwt", true); + AccountManager::getInstance()->updateServiceEndpoint(uuid, m_service + "/message/2"); + model.setAccount(uuid); model.setMemberDids(QStringList() << "did:plc:mqxsuw5b5rhpwo4lw6iwlid5"); diff --git a/tests/common/webserver.cpp b/tests/common/webserver.cpp index f18ca3ad..b0fab792 100644 --- a/tests/common/webserver.cpp +++ b/tests/common/webserver.cpp @@ -2,12 +2,18 @@ WebServer::WebServer(QObject *parent) : QAbstractHttpServer(parent) { } +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) bool WebServer::handleRequest(const QHttpServerRequest &request, QTcpSocket *socket) +# define MAKE_RESPONDER makeResponder(request, socket) +#else +# define MAKE_RESPONDER responder +bool WebServer::handleRequest(const QHttpServerRequest &request, QHttpServerResponder &responder) +#endif { if (!verifyHttpHeader(request)) { - makeResponder(request, socket).write(QHttpServerResponder::StatusCode::BadRequest); + MAKE_RESPONDER.write(QHttpServerResponder::StatusCode::BadRequest); } else if (request.url().path().contains("//")) { - makeResponder(request, socket).write(QHttpServerResponder::StatusCode::NotFound); + MAKE_RESPONDER.write(QHttpServerResponder::StatusCode::NotFound); } else if (request.method() == QHttpServerRequest::Method::Post) { bool result = false; QString json; @@ -23,22 +29,19 @@ bool WebServer::handleRequest(const QHttpServerRequest &request, QTcpSocket *soc std::make_pair(QByteArray("ratelimit-policy"), QByteArray("30;w=300")) }; if (request.url().path().endsWith("/limit/xrpc/com.atproto.server.createSession")) { - makeResponder(request, socket) - .write(json.toUtf8(), headers, - QHttpServerResponder::StatusCode::Unauthorized); + MAKE_RESPONDER.write(json.toUtf8(), headers, + QHttpServerResponder::StatusCode::Unauthorized); } else { - makeResponder(request, socket) - .write(json.toUtf8(), headers, QHttpServerResponder::StatusCode::Ok); + MAKE_RESPONDER.write(json.toUtf8(), headers, + QHttpServerResponder::StatusCode::Ok); } } else { - makeResponder(request, socket) - .write(json.toUtf8(), - m_MimeDb.mimeTypeForFile("result.json").name().toUtf8(), - QHttpServerResponder::StatusCode::Ok); + MAKE_RESPONDER.write(json.toUtf8(), + m_MimeDb.mimeTypeForFile("result.json").name().toUtf8(), + QHttpServerResponder::StatusCode::Ok); } } else { - makeResponder(request, socket) - .write(QHttpServerResponder::StatusCode::InternalServerError); + MAKE_RESPONDER.write(QHttpServerResponder::StatusCode::InternalServerError); } } else { QString path = WebServer::convertResoucePath(request.url()); @@ -56,7 +59,7 @@ bool WebServer::handleRequest(const QHttpServerRequest &request, QTcpSocket *soc } qDebug().noquote() << "SERVER PATH=" << path; if (!QFile::exists(path)) { - makeResponder(request, socket).write(QHttpServerResponder::StatusCode::NotFound); + MAKE_RESPONDER.write(QHttpServerResponder::StatusCode::NotFound); } else { QFileInfo file_info(request.url().path()); QByteArray data; @@ -65,17 +68,25 @@ bool WebServer::handleRequest(const QHttpServerRequest &request, QTcpSocket *soc mime_type = "application/vnd.ipld.car"; } if (WebServer::readFile(path, data)) { - makeResponder(request, socket) - .write(data, mime_type.toUtf8(), QHttpServerResponder::StatusCode::Ok); + MAKE_RESPONDER.write(data, mime_type.toUtf8(), + QHttpServerResponder::StatusCode::Ok); } else { - makeResponder(request, socket) - .write(QHttpServerResponder::StatusCode::InternalServerError); + MAKE_RESPONDER.write(QHttpServerResponder::StatusCode::InternalServerError); } } } return true; } +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) +#else +void WebServer::missingHandler(const QHttpServerRequest &request, QHttpServerResponder &&responder) +{ + Q_UNUSED(request) + Q_UNUSED(responder) +} +#endif + QString WebServer::convertResoucePath(const QUrl &url) { QFileInfo file_info(url.path()); @@ -96,6 +107,7 @@ bool WebServer::readFile(const QString &path, QByteArray &data) bool WebServer::verifyHttpHeader(const QHttpServerRequest &request) const { +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) if (request.headers().contains("atproto-accept-labelers")) { QStringList dids = request.headers().value("atproto-accept-labelers").toString().split(","); for (const auto &did : dids) { @@ -104,5 +116,17 @@ bool WebServer::verifyHttpHeader(const QHttpServerRequest &request) const } } } +#else + for (const auto &header : request.headers()) { + if (header.first.contains("atproto-accept-labelers")) { + QList dids = header.second.split(','); + for (const auto &did : dids) { + if (!did.startsWith("did:")) { + return false; + } + } + } + } +#endif return true; } diff --git a/tests/common/webserver.h b/tests/common/webserver.h index 2cc2f866..af0c1cee 100644 --- a/tests/common/webserver.h +++ b/tests/common/webserver.h @@ -9,7 +9,13 @@ class WebServer : public QAbstractHttpServer public: explicit WebServer(QObject *parent = nullptr); +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) bool handleRequest(const QHttpServerRequest &request, QTcpSocket *socket) override; +#else + virtual bool handleRequest(const QHttpServerRequest &request, QHttpServerResponder &responder); + virtual void missingHandler(const QHttpServerRequest &request, + QHttpServerResponder &&responder); +#endif static QString convertResoucePath(const QUrl &url); static bool readFile(const QString &path, QByteArray &data); diff --git a/tests/deps.pri b/tests/deps.pri index 9ecc7532..5d9e8cda 100644 --- a/tests/deps.pri +++ b/tests/deps.pri @@ -1,4 +1,7 @@ QT += xml sql websockets +greaterThan(QT_MAJOR_VERSION, 5) { +QT += core5compat +} win32:CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../../lib/release/ -llib else:win32:CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../../lib/debug/ -llib @@ -6,6 +9,7 @@ else:unix: LIBS += -L$$OUT_PWD/../../lib/ -llib INCLUDEPATH += $$PWD/../lib DEPENDPATH += $$PWD/../lib +RESOURCES += $$PWD/../lib/lib.qrc win32-g++:CONFIG(release, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../../lib/release/liblib.a else:win32-g++:CONFIG(debug, debug|release): PRE_TARGETDEPS += $$OUT_PWD/../../lib/debug/liblib.a diff --git a/tests/hagoromo_test/response/timeline/next/xrpc/app.bsky.feed.getTimeline b/tests/hagoromo_test/response/timeline/next/xrpc/app.bsky.feed.getTimeline index ce13ba92..86c9ae7f 100644 --- a/tests/hagoromo_test/response/timeline/next/xrpc/app.bsky.feed.getTimeline +++ b/tests/hagoromo_test/response/timeline/next/xrpc/app.bsky.feed.getTimeline @@ -188,7 +188,9 @@ "repostCount": 0, "likeCount": 0, "indexedAt": "2023-05-28T11:30:02.749Z", - "viewer": {}, + "viewer": { + "repost": "at://did:plc:ipj5qejfoqu6eukvt72uhyit/app.bsky.feed.repost/3l5jty5ef3w" + }, "labels": [] }, "reason": { diff --git a/tests/hagoromo_test/tst_hagoromo_test.cpp b/tests/hagoromo_test/tst_hagoromo_test.cpp index 01c2602f..313cafb2 100644 --- a/tests/hagoromo_test/tst_hagoromo_test.cpp +++ b/tests/hagoromo_test/tst_hagoromo_test.cpp @@ -19,6 +19,7 @@ #include "moderation/contentfiltersettinglistmodel.h" #include "tools/labelerprovider.h" #include "controls/calendartablemodel.h" +#include "tools/accountmanager.h" class hagoromo_test : public QObject { @@ -77,7 +78,7 @@ private slots: hagoromo_test::hagoromo_test() { QCoreApplication::setOrganizationName(QStringLiteral("relog")); - QCoreApplication::setApplicationName(QStringLiteral("Hagoromo")); + QCoreApplication::setApplicationName(QStringLiteral("Hagoromo_unittest")); m_listenPort = m_mockServer.listen(QHostAddress::LocalHost, 0); m_service = QString("http://localhost:%1/response").arg(m_listenPort); @@ -128,8 +129,12 @@ void hagoromo_test::test_test_TimelineListModelError() void hagoromo_test::test_TimelineListModelFacet() { + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/facet", "id", "pass", "did:plc:ipj5qejfoqu6eukvt72uhyit", + "handle", "email", "accessJwt", "refreshJwt", true); + TimelineListModel model; - model.setAccount(m_service + "/facet", QString(), QString(), QString(), "dummy", QString()); + model.setAccount(uuid); model.setDisplayInterval(0); QSignalSpy spy(&model, SIGNAL(runningChanged())); @@ -452,9 +457,12 @@ void hagoromo_test::test_ColumnListModelSelected() void hagoromo_test::test_NotificationListModel() { + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/notifications/visible", "id", "pass", + "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", "handle", "email", "accessJwt", "refreshJwt", true); + NotificationListModel model; - model.setAccount(m_service + "/notifications/visible", QString(), QString(), QString(), "dummy", - QString()); + model.setAccount(uuid); { int i = 0; @@ -751,9 +759,12 @@ void hagoromo_test::test_NotificationListModel() void hagoromo_test::test_NotificationListModel2() { + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/notifications/visible", "id", "pass", + "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", "handle", "email", "accessJwt", "refreshJwt", true); + NotificationListModel model; - model.setAccount(m_service + "/notifications/visible", QString(), QString(), QString(), "dummy", - QString()); + model.setAccount(uuid); int i = 0; QSignalSpy spy(&model, SIGNAL(runningChanged())); @@ -976,10 +987,13 @@ void hagoromo_test::test_NotificationListModel2() void hagoromo_test::test_NotificationList_collecting() { + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/notifications/collecting/1", "id", "pass", + "did:plc:ipj5qejfoqu6eukvt72uhyit", "handle", "email", "accessJwt", "refreshJwt", true); + int i = 0; NotificationListModel model; - model.setAccount(m_service + "/notifications/collecting/1", "did:plc:ipj5qejfoqu6eukvt72uhyit", - QString(), QString(), "dummy", QString()); + model.setAccount(uuid); { QSignalSpy spy(&model, SIGNAL(runningChanged())); @@ -1032,9 +1046,12 @@ void hagoromo_test::test_NotificationList_collecting() void hagoromo_test::test_NotificationList_collecting_next() { + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/notifications/collecting/2", "id", "pass", + "did:plc:ipj5qejfoqu6eukvt72uhyit", "handle", "email", "accessJwt", "refreshJwt", true); + NotificationListModel model; - model.setAccount(m_service + "/notifications/collecting/2", "did:plc:ipj5qejfoqu6eukvt72uhyit", - QString(), QString(), "dummy", QString()); + model.setAccount(uuid); { QSignalSpy spy(&model, SIGNAL(runningChanged())); @@ -1146,9 +1163,11 @@ void hagoromo_test::test_NotificationList_collecting_next() void hagoromo_test::test_NotificationList_collecting_visibility() { + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/notifications/collecting/2", "id", "pass", + "did:plc:ipj5qejfoqu6eukvt72uhyit", "handle", "email", "accessJwt", "refreshJwt", true); NotificationListModel model; - model.setAccount(m_service + "/notifications/collecting/2", "did:plc:ipj5qejfoqu6eukvt72uhyit", - QString(), QString(), "dummy", QString()); + model.setAccount(uuid); model.setVisibleLike(false); { QSignalSpy spy(&model, SIGNAL(runningChanged())); @@ -1263,10 +1282,12 @@ void hagoromo_test::test_NotificationList_collecting_visibility() void hagoromo_test::test_NotificationList_no_collecting() { + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/notifications/collecting/2", "id", "pass", + "did:plc:ipj5qejfoqu6eukvt72uhyit", "handle", "email", "accessJwt", "refreshJwt", true); NotificationListModel model; - model.setAccount(m_service + "/notifications/collecting/2", "did:plc:ipj5qejfoqu6eukvt72uhyit", - QString(), QString(), "dummy", QString()); + model.setAccount(uuid); model.setAggregateReactions(false); { QSignalSpy spy(&model, SIGNAL(runningChanged())); @@ -1504,7 +1525,9 @@ void hagoromo_test::test_charCount() QVERIFY(file.open(QFile::ReadOnly)); QTextStream ts(&file); +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) ts.setCodec("utf-8"); +#endif QStringEx text = ts.readAll(); qDebug() << text << text.length() << text.count(); for (int i = 0; i < text.count(); i++) { @@ -1516,10 +1539,12 @@ void hagoromo_test::test_charCount() void hagoromo_test::test_TimelineListModel_quote_warn() { + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/timeline/warn", "id", "pass", "did:plc:test", "handle", + "email", "accessJwt", "refreshJwt", true); int row = 0; TimelineListModel model; - model.setAccount(m_service + "/timeline/warn", "did:plc:test", QString(), QString(), "dummy", - QString()); + model.setAccount(uuid); model.setDisplayInterval(0); LabelerProvider::getInstance()->clear(); @@ -1576,10 +1601,14 @@ void hagoromo_test::test_TimelineListModel_quote_warn() void hagoromo_test::test_TimelineListModel_quote_hide() { + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/timeline/hide", "id", "pass", + "did:plc:l4fsx4ujos7uw7n4ijq2ulgs", "ioriayane.bsky.social", "email", "accessJwt", + "refreshJwt", true); + int row = 0; TimelineListModel model; - model.setAccount(m_service + "/timeline/hide", "did:plc:l4fsx4ujos7uw7n4ijq2ulgs", - "ioriayane.bsky.social", QString(), "dummy", QString()); + model.setAccount(uuid); model.setDisplayInterval(0); QSignalSpy spy(&model, SIGNAL(runningChanged())); @@ -1634,12 +1663,16 @@ void hagoromo_test::test_TimelineListModel_quote_hide() void hagoromo_test::test_TimelineListModel_quote_hide2() { + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/timeline/hide", "id", "pass", + "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", "ioriayane2.bsky.social", "email", "accessJwt", + "refreshJwt", true); + // 自分のポストが引用されているのを見るイメージ // 自分のポストの引用はHide設定でも隠さない int row = 0; TimelineListModel model; - model.setAccount(m_service + "/timeline/hide", "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", - "ioriayane2.bsky.social", QString(), "dummy", QString()); + model.setAccount(uuid); model.setDisplayInterval(0); QSignalSpy spy(&model, SIGNAL(runningChanged())); @@ -1705,10 +1738,13 @@ void hagoromo_test::test_TimelineListModel_quote_hide2() void hagoromo_test::test_TimelineListModel_quote_label() { + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/timeline/labels", "id", "pass", + "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", "ioriayane2.bsky.social", "email", "accessJwt", + "refreshJwt", true); int row = 0; TimelineListModel model; - model.setAccount(m_service + "/timeline/labels", QString(), QString(), QString(), "dummy", - QString()); + model.setAccount(uuid); model.setDisplayInterval(0); QSignalSpy spy(&model, SIGNAL(runningChanged())); @@ -1757,10 +1793,14 @@ void hagoromo_test::test_TimelineListModel_quote_label() void hagoromo_test::test_TimelineListModel_animated_image() { + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/timeline/animated", "id", "pass", + "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", "ioriayane2.bsky.social", "email", "accessJwt", + "refreshJwt", true); + int row = 0; TimelineListModel model; - model.setAccount(m_service + "/timeline/animated", QString(), QString(), QString(), "dummy", - QString()); + model.setAccount(uuid); model.setDisplayInterval(0); QSignalSpy spy(&model, SIGNAL(runningChanged())); @@ -1898,10 +1938,14 @@ void hagoromo_test::test_TimelineListModel_animated_image() void hagoromo_test::test_TimelineListModel_threadgate() { + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/timeline/threadgate", "id", "pass", + "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", "ioriayane2.bsky.social", "email", "accessJwt", + "refreshJwt", true); + int row = 0; TimelineListModel model; - model.setAccount(m_service + "/timeline/threadgate", QString(), QString(), QString(), "dummy", - QString()); + model.setAccount(uuid); model.setDisplayInterval(0); QSignalSpy spy(&model, SIGNAL(runningChanged())); @@ -1974,10 +2018,14 @@ void hagoromo_test::test_TimelineListModel_threadgate() void hagoromo_test::test_TimelineListModel_hide_repost() { + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/timeline/hide2", "id", "pass", + "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", "ioriayane2.bsky.social", "email", "accessJwt", + "refreshJwt", true); + int row = 0; TimelineListModel model; - model.setAccount(m_service + "/timeline/hide2", "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", - "ioriayane2.bsky.social", QString(), "dummy", QString()); + model.setAccount(uuid); model.setDisplayInterval(0); QSignalSpy spy(&model, SIGNAL(runningChanged())); @@ -2300,10 +2348,14 @@ void hagoromo_test::test_TimelineListModel_hide_repost() void hagoromo_test::test_TimelineListModel_labelers() { + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/timeline/labelers/1", "id", "pass", + "did:plc:l4fsx4ujos7uw7n4ijq2ulgs", "ioriayane.bsky.social", "email", "accessJwt", + "refreshJwt", true); + int row = 0; TimelineListModel model; - model.setAccount(m_service + "/timeline/labelers/1", "did:plc:l4fsx4ujos7uw7n4ijq2ulgs", - "ioriayane.bsky.social", QString(), "dummy", QString()); + model.setAccount(uuid); model.setDisplayInterval(0); LabelerProvider::getInstance()->clear(); @@ -2476,10 +2528,14 @@ void hagoromo_test::test_TimelineListModel_labelers() void hagoromo_test::test_TimelineListModel_pinnded() { + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/timeline/pinned/1", "id", "pass", + "did:plc:l4fsx4ujos7uw7n4ijq2ulgs", "ioriayane.bsky.social", "email", "accessJwt", + "refreshJwt", true); + int row = 0; TimelineListModel model; - model.setAccount(m_service + "/timeline/pinned/1", "did:plc:l4fsx4ujos7uw7n4ijq2ulgs", - "ioriayane.bsky.social", QString(), "dummy", QString()); + model.setAccount(uuid); model.setDisplayInterval(0); model.setDisplayPinnedPost(true); model.setPinnedPost("at://did:plc:l4fsx4ujos7uw7n4ijq2ulgs/app.bsky.feed.post/3kgbutwycqd2g"); @@ -2508,8 +2564,11 @@ void hagoromo_test::test_TimelineListModel_pinnded() model.item(row, TimelineListModel::UriRole).toString().toLocal8Bit()); } - model.setAccount(m_service + "/timeline/pinned/2", "did:plc:l4fsx4ujos7uw7n4ijq2ulgs", - "ioriayane.bsky.social", QString(), "dummy", QString()); + uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/timeline/pinned/2", "id", "pass", + "did:plc:l4fsx4ujos7uw7n4ijq2ulgs", "ioriayane.bsky.social", "email", "accessJwt", + "refreshJwt", true); + model.setAccount(uuid); { QSignalSpy spy(&model, SIGNAL(runningChanged())); QVERIFY(model.getLatest()); @@ -2566,8 +2625,11 @@ void hagoromo_test::test_TimelineListModel_pinnded() } // re-set - model.setAccount(m_service + "/timeline/pinned/3", "did:plc:l4fsx4ujos7uw7n4ijq2ulgs", - "ioriayane.bsky.social", QString(), "dummy", QString()); + uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/timeline/pinned/3", "id", "pass", + "did:plc:l4fsx4ujos7uw7n4ijq2ulgs", "ioriayane.bsky.social", "email", "accessJwt", + "refreshJwt", true); + model.setAccount(uuid); model.setPinnedPost("at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.feed.post/3klrvkltf672n"); { QSignalSpy spy(&model, SIGNAL(runningChanged())); @@ -2599,8 +2661,11 @@ void hagoromo_test::test_TimelineListModel_pinnded() } // replace pin - model.setAccount(m_service + "/timeline/pinned/2", "did:plc:l4fsx4ujos7uw7n4ijq2ulgs", - "ioriayane.bsky.social", QString(), "dummy", QString()); + uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/timeline/pinned/2", "id", "pass", + "did:plc:l4fsx4ujos7uw7n4ijq2ulgs", "ioriayane.bsky.social", "email", "accessJwt", + "refreshJwt", true); + model.setAccount(uuid); model.setPinnedPost("at://did:plc:l4fsx4ujos7uw7n4ijq2ulgs/app.bsky.feed.post/3kgbutwycqd2g"); { QSignalSpy spy(&model, SIGNAL(runningChanged())); @@ -2634,10 +2699,12 @@ void hagoromo_test::test_TimelineListModel_pinnded() void hagoromo_test::test_NotificationListModel_warn() { + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/notifications/warn", "id", "pass", "did:plc:test", + "ioriayane.bsky.social", "email", "accessJwt", "refreshJwt", true); int row = 0; NotificationListModel model; - model.setAccount(m_service + "/notifications/warn", "did:plc:test", QString(), QString(), - "dummy", QString()); + model.setAccount(uuid); model.setDisplayInterval(0); LabelerProvider::getInstance()->clear(); @@ -2675,10 +2742,14 @@ void hagoromo_test::test_NotificationListModel_warn() void hagoromo_test::test_TimelineListModel_next() { + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/timeline/next", "id", "pass", + "did:plc:ipj5qejfoqu6eukvt72uhyit", "ioriayane.bsky.social", "email", "accessJwt", + "refreshJwt", true); + int row = 0; TimelineListModel model; - model.setAccount(m_service + "/timeline/next", QString(), QString(), QString(), "dummy", - QString()); + model.setAccount(uuid); model.setDisplayInterval(0); { QSignalSpy spy(&model, SIGNAL(runningChanged())); @@ -2744,9 +2815,12 @@ void hagoromo_test::test_TimelineListModel_next() void hagoromo_test::test_AnyProfileListModel() { + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/anyprofile", "id", "pass", "did:plc:test", + "ioriayane.bsky.social", "email", "accessJwt", "refreshJwt", true); + AnyProfileListModel model; - model.setAccount(m_service + "/anyprofile", QString(), QString(), QString(), "dummy", - QString()); + model.setAccount(uuid); model.setTargetUri("at://did:plc:ipj5qejfoqu6eukvt72uhyit/app.bsky.feed.post/3k6tpw4xr4d27"); model.setType(AnyProfileListModel::AnyProfileListModelType::Like); @@ -2772,10 +2846,13 @@ void hagoromo_test::test_AnyProfileListModel() void hagoromo_test::test_TimelineListModel_text() { + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/timeline/text", "id", "pass", "did:plc:test", + "ioriayane.bsky.social", "email", "accessJwt", "refreshJwt", true); + int row = 0; TimelineListModel model; - model.setAccount(m_service + "/timeline/text", QString(), QString(), QString(), "dummy", - QString()); + model.setAccount(uuid); model.setDisplayInterval(0); QSignalSpy spy(&model, SIGNAL(runningChanged())); @@ -2807,10 +2884,14 @@ void hagoromo_test::test_TimelineListModel_text() void hagoromo_test::test_TimelineListModel_reply() { + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/timeline/reply", "id", "pass", + "did:plc:ipj5qejfoqu6eukvt72uhyit", "ioriayane.bsky.social", "email", "accessJwt", + "refreshJwt", true); + int row = 0; TimelineListModel model; - model.setAccount(m_service + "/timeline/reply", "did:plc:ipj5qejfoqu6eukvt72uhyit", QString(), - QString(), "dummy", QString()); + model.setAccount(uuid); model.setDisplayInterval(0); model.setVisibleReplyToUnfollowedUsers(true); @@ -2849,8 +2930,11 @@ void hagoromo_test::test_TimelineListModel_reply() QVERIFY(model.item(2, TimelineListModel::CidRole) == "bafyreievv2yz3obnigwjix5kr2icycfkqdobrfufd3cm4wfavnjfeqhxbe_4"); - model.setAccount(m_service + "/timeline/reply", "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", QString(), - QString(), "dummy", QString()); + uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/timeline/reply", "id", "pass", + "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", "ioriayane.bsky.social", "email", "accessJwt", + "refreshJwt", true); + model.setAccount(uuid); model.setVisibleReplyToUnfollowedUsers(false); { QSignalSpy spy(&model, SIGNAL(runningChanged())); @@ -2872,9 +2956,12 @@ void hagoromo_test::test_TimelineListModel_reply() void hagoromo_test::test_PostThreadListModel() { + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/postthread/1", "id", "pass", + "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", "ioriayane.bsky.social", "email", "accessJwt", + "refreshJwt", true); PostThreadListModel model; - model.setAccount(m_service + "/postthread/1", QString(), QString(), QString(), "dummy", - QString()); + model.setAccount(uuid); model.setDisplayInterval(0); model.setPostThreadUri("at://uri"); @@ -2905,9 +2992,12 @@ void hagoromo_test::test_PostThreadListModel() QVERIFY(model.item(row, PostThreadListModel::ThreadConnectorTopRole).toBool() == true); QVERIFY(model.item(row, PostThreadListModel::ThreadConnectorBottomRole).toBool() == false); + uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/postthread/2", "id", "pass", + "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", "ioriayane.bsky.social", "email", "accessJwt", + "refreshJwt", true); model.clear(); - model.setAccount(m_service + "/postthread/2", QString(), QString(), QString(), "dummy", - QString()); + model.setAccount(uuid); { QSignalSpy spy(&model, SIGNAL(runningChanged())); model.getLatest(); @@ -2941,9 +3031,12 @@ void hagoromo_test::test_PostThreadListModel() QVERIFY(model.item(row, PostThreadListModel::ThreadConnectorTopRole).toBool() == true); QVERIFY(model.item(row, PostThreadListModel::ThreadConnectorBottomRole).toBool() == false); + uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/postthread/3", "id", "pass", + "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", "ioriayane.bsky.social", "email", "accessJwt", + "refreshJwt", true); model.clear(); - model.setAccount(m_service + "/postthread/3", QString(), QString(), QString(), "dummy", - QString()); + model.setAccount(uuid); { QSignalSpy spy(&model, SIGNAL(runningChanged())); model.getLatest(); @@ -2988,9 +3081,12 @@ void hagoromo_test::test_SystemTool_ImageClip() void hagoromo_test::test_SearchProfileListModel_suggestion() { + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/search_profile", "id", "pass", + "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", "ioriayane.bsky.social", "email", "accessJwt", + "refreshJwt", true); SearchProfileListModel model; - model.setAccount(m_service + "/search_profile", QString(), QString(), QString(), "dummy", - QString()); + model.setAccount(uuid); QString actual; @@ -3108,22 +3204,25 @@ void hagoromo_test::test_SearchProfileListModel_suggestion() void hagoromo_test::test_SearchPostListModel_text() { + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/search_profile", "id", "pass", "did:plc:hogehoge", + "hogehoge.bsky.social", "email", "accessJwt", "refreshJwt", true); SearchPostListModel model; - model.setAccount("", "did:plc:hogehoge", "hogehoge.bsky.sockal", "", "", ""); + model.setAccount(uuid); - QVERIFY2(model.replaceSearchCommand("from:me") == "from:hogehoge.bsky.sockal", + QVERIFY2(model.replaceSearchCommand("from:me") == "from:hogehoge.bsky.social", model.text().toLocal8Bit()); QVERIFY2(model.replaceSearchCommand("fuga from:me hoge") - == "fuga from:hogehoge.bsky.sockal hoge", + == "fuga from:hogehoge.bsky.social hoge", model.text().toLocal8Bit()); QVERIFY2(model.replaceSearchCommand("fuga\tfrom:me\thoge") - == "fuga from:hogehoge.bsky.sockal hoge", + == "fuga from:hogehoge.bsky.social hoge", model.text().toLocal8Bit()); QVERIFY2(model.replaceSearchCommand(QString("fuga%1from:me%1hoge").arg(QChar(0x3000))) - == "fuga from:hogehoge.bsky.sockal hoge", + == "fuga from:hogehoge.bsky.social hoge", model.text().toLocal8Bit()); } diff --git a/tests/hagoromo_test2/data/generator/remove/app.bsky.actor.putPreferences b/tests/hagoromo_test2/data/generator/remove/app.bsky.actor.putPreferences index b9e090bb..d5562db6 100644 --- a/tests/hagoromo_test2/data/generator/remove/app.bsky.actor.putPreferences +++ b/tests/hagoromo_test2/data/generator/remove/app.bsky.actor.putPreferences @@ -10,6 +10,35 @@ "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/bsky-team" ] }, + { + "$type": "app.bsky.actor.defs#savedFeedsPrefV2", + "items": [ + { + "id": "3ksooxaaknk27", + "pinned": true, + "type": "timeline", + "value": "following" + }, + { + "id": "3ksooxaaknm27", + "pinned": true, + "type": "feed", + "value": "at://did:plc:q6gjnaw2blty4crticxkmujt/app.bsky.feed.generator/cv:cat" + }, + { + "id": "3ksooxaaknn27", + "pinned": true, + "type": "list", + "value": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.graph.list/3kflf2r3lwg2x" + }, + { + "id": "3ksooaaaaaaaa", + "pinned": false, + "type": "feed", + "value": "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/hot-classic" + } + ] + }, { "$type": "app.bsky.actor.defs#savedFeedsPref", "pinned": [ @@ -20,7 +49,7 @@ ], "saved": [ "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/bsky-team", - "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot" + "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/with-friends" ] }, { diff --git a/tests/hagoromo_test2/data/generator/save/app.bsky.actor.putPreferences b/tests/hagoromo_test2/data/generator/save/app.bsky.actor.putPreferences index 449944b8..075e0bba 100644 --- a/tests/hagoromo_test2/data/generator/save/app.bsky.actor.putPreferences +++ b/tests/hagoromo_test2/data/generator/save/app.bsky.actor.putPreferences @@ -10,6 +10,41 @@ "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/bsky-team" ] }, + { + "$type": "app.bsky.actor.defs#savedFeedsPrefV2", + "items": [ + { + "id": "3ksooxaaknk27", + "pinned": true, + "type": "timeline", + "value": "following" + }, + { + "id": "3ksooxaaknm27", + "pinned": true, + "type": "feed", + "value": "at://did:plc:q6gjnaw2blty4crticxkmujt/app.bsky.feed.generator/cv:cat" + }, + { + "id": "3ksooxaaknu27", + "pinned": false, + "type": "feed", + "value": "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot" + }, + { + "id": "3ksooxaaknn27", + "pinned": true, + "type": "list", + "value": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.graph.list/3kflf2r3lwg2x" + }, + { + "id": "3ksooaaaaaaaa", + "pinned": false, + "type": "feed", + "value": "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/hot-classic" + } + ] + }, { "$type": "app.bsky.actor.defs#savedFeedsPref", "pinned": [ @@ -21,8 +56,7 @@ "saved": [ "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/bsky-team", "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/with-friends", - "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot", - "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/hot-classic" + "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot" ] }, { diff --git a/tests/hagoromo_test2/hagoromo_test2.qrc b/tests/hagoromo_test2/hagoromo_test2.qrc index c159b07c..215d3b2b 100644 --- a/tests/hagoromo_test2/hagoromo_test2.qrc +++ b/tests/hagoromo_test2/hagoromo_test2.qrc @@ -33,5 +33,7 @@ data/profile/3.1/com.atproto.repo.putRecord response/profile/3.2/xrpc/com.atproto.repo.getRecord data/profile/3.2/com.atproto.repo.putRecord + response/account/account1/xrpc/com.atproto.repo.describeRepo + response/account/account2/xrpc/com.atproto.repo.describeRepo diff --git a/tests/hagoromo_test2/response/account/account1/xrpc/com.atproto.repo.describeRepo b/tests/hagoromo_test2/response/account/account1/xrpc/com.atproto.repo.describeRepo new file mode 100644 index 00000000..a530cccc --- /dev/null +++ b/tests/hagoromo_test2/response/account/account1/xrpc/com.atproto.repo.describeRepo @@ -0,0 +1,48 @@ +{ + "handle": "account1.relog.tech", + "did": "did:plc:account1", + "didDoc": { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/multikey/v1", + "https://w3id.org/security/suites/secp256k1-2019/v1" + ], + "id": "did:plc:account1", + "alsoKnownAs": [ + "at://account1.relog.tech" + ], + "verificationMethod": [ + { + "id": "did:plc:account1#atproto", + "type": "Multikey", + "controller": "did:plc:account1", + "publicKeyMultibase": "zQ3shYNo9wSChjqSMPZU" + } + ], + "service": [ + { + "id": "#atproto_pds", + "type": "AtprotoPersonalDataServer", + "serviceEndpoint": "http://localhost:%1/response/account/account1" + } + ] + }, + "collections": [ + "app.bsky.actor.profile", + "app.bsky.feed.generator", + "app.bsky.feed.like", + "app.bsky.feed.post", + "app.bsky.feed.postgate", + "app.bsky.feed.repost", + "app.bsky.feed.threadgate", + "app.bsky.graph.block", + "app.bsky.graph.follow", + "app.bsky.graph.list", + "app.bsky.graph.listitem", + "app.bsky.graph.starterpack", + "blue.linkat.board", + "chat.bsky.actor.declaration", + "com.whtwnd.blog.entry" + ], + "handleIsCorrect": true +} diff --git a/tests/hagoromo_test2/response/account/account2/xrpc/com.atproto.repo.describeRepo b/tests/hagoromo_test2/response/account/account2/xrpc/com.atproto.repo.describeRepo new file mode 100644 index 00000000..aa7b7778 --- /dev/null +++ b/tests/hagoromo_test2/response/account/account2/xrpc/com.atproto.repo.describeRepo @@ -0,0 +1,48 @@ +{ + "handle": "account2.relog.tech", + "did": "did:plc:account2", + "didDoc": { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/multikey/v1", + "https://w3id.org/security/suites/secp256k1-2019/v1" + ], + "id": "did:plc:account2", + "alsoKnownAs": [ + "at://account2.relog.tech" + ], + "verificationMethod": [ + { + "id": "did:plc:account2#atproto", + "type": "Multikey", + "controller": "did:plc:account2", + "publicKeyMultibase": "zQ3shYNo9wSChj" + } + ], + "service": [ + { + "id": "#atproto_pds", + "type": "AtprotoPersonalDataServer", + "serviceEndpoint": "http://localhost:%1/response/account/account2" + } + ] + }, + "collections": [ + "app.bsky.actor.profile", + "app.bsky.feed.generator", + "app.bsky.feed.like", + "app.bsky.feed.post", + "app.bsky.feed.postgate", + "app.bsky.feed.repost", + "app.bsky.feed.threadgate", + "app.bsky.graph.block", + "app.bsky.graph.follow", + "app.bsky.graph.list", + "app.bsky.graph.listitem", + "app.bsky.graph.starterpack", + "blue.linkat.board", + "chat.bsky.actor.declaration", + "com.whtwnd.blog.entry" + ], + "handleIsCorrect": true +} diff --git a/tests/hagoromo_test2/response/generator/remove/xrpc/app.bsky.actor.getPreferences b/tests/hagoromo_test2/response/generator/remove/xrpc/app.bsky.actor.getPreferences index 6cb3efcb..ad5f3750 100644 --- a/tests/hagoromo_test2/response/generator/remove/xrpc/app.bsky.actor.getPreferences +++ b/tests/hagoromo_test2/response/generator/remove/xrpc/app.bsky.actor.getPreferences @@ -10,6 +10,41 @@ "at://did:plc:hiptcrt4k63szzz4ty3dhwcp/app.bsky.feed.generator/ja-images" ] }, + { + "$type": "app.bsky.actor.defs#savedFeedsPrefV2", + "items": [ + { + "id": "3ksooxaaknk27", + "pinned": true, + "type": "timeline", + "value": "following" + }, + { + "id": "3ksooxaaknm27", + "pinned": true, + "type": "feed", + "value": "at://did:plc:q6gjnaw2blty4crticxkmujt/app.bsky.feed.generator/cv:cat" + }, + { + "id": "3ksooxaaknu27", + "pinned": false, + "type": "feed", + "value": "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot" + }, + { + "id": "3ksooxaaknn27", + "pinned": true, + "type": "list", + "value": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.graph.list/3kflf2r3lwg2x" + }, + { + "id": "3ksooaaaaaaaa", + "pinned": false, + "type": "feed", + "value": "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/hot-classic" + } + ] + }, { "$type": "app.bsky.actor.defs#savedFeedsPref", "saved": [ diff --git a/tests/hagoromo_test2/response/generator/save/xrpc/app.bsky.actor.getPreferences b/tests/hagoromo_test2/response/generator/save/xrpc/app.bsky.actor.getPreferences index 6cb3efcb..48cf5064 100644 --- a/tests/hagoromo_test2/response/generator/save/xrpc/app.bsky.actor.getPreferences +++ b/tests/hagoromo_test2/response/generator/save/xrpc/app.bsky.actor.getPreferences @@ -10,6 +10,35 @@ "at://did:plc:hiptcrt4k63szzz4ty3dhwcp/app.bsky.feed.generator/ja-images" ] }, + { + "$type": "app.bsky.actor.defs#savedFeedsPrefV2", + "items": [ + { + "id": "3ksooxaaknk27", + "pinned": true, + "type": "timeline", + "value": "following" + }, + { + "id": "3ksooxaaknm27", + "pinned": true, + "type": "feed", + "value": "at://did:plc:q6gjnaw2blty4crticxkmujt/app.bsky.feed.generator/cv:cat" + }, + { + "id": "3ksooxaaknu27", + "pinned": false, + "type": "feed", + "value": "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot" + }, + { + "id": "3ksooxaaknn27", + "pinned": true, + "type": "list", + "value": "at://did:plc:mqxsuw5b5rhpwo4lw6iwlid5/app.bsky.graph.list/3kflf2r3lwg2x" + } + ] + }, { "$type": "app.bsky.actor.defs#savedFeedsPref", "saved": [ diff --git a/tests/hagoromo_test2/tst_hagoromo_test2.cpp b/tests/hagoromo_test2/tst_hagoromo_test2.cpp index 2d777f80..7f102d16 100644 --- a/tests/hagoromo_test2/tst_hagoromo_test2.cpp +++ b/tests/hagoromo_test2/tst_hagoromo_test2.cpp @@ -15,6 +15,7 @@ #include "list/listslistmodel.h" #include "list/listitemlistmodel.h" #include "list/listfeedlistmodel.h" +#include "tools/accountmanager.h" class hagoromo_test : public QObject { @@ -32,6 +33,7 @@ private slots: void test_FeedGeneratorListModel(); void test_FeedGeneratorLink(); void test_AccountListModel(); + void test_AccountManager(); void test_ListsListModel(); void test_ListsListModel_search(); void test_ListsListModel_error(); @@ -48,12 +50,13 @@ private slots: void test_putPreferences(const QString &path, const QByteArray &body); void test_putRecord(const QString &path, const QByteArray &body); void verifyStr(const QString &expect, const QString &actual); + QJsonObject copyObject(const QJsonObject &object, const QStringList &excludes); }; hagoromo_test::hagoromo_test() { QCoreApplication::setOrganizationName(QStringLiteral("relog")); - QCoreApplication::setApplicationName(QStringLiteral("Hagoromo")); + QCoreApplication::setApplicationName(QStringLiteral("Hagoromo_unittest")); m_listenPort = m_mockServer.listen(QHostAddress::LocalHost, 0); m_service = QString("http://localhost:%1/response").arg(m_listenPort); @@ -108,8 +111,12 @@ void hagoromo_test::cleanupTestCase() { } void hagoromo_test::test_RecordOperator() { + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/facet", "id", "pass", "did:plc:hogehoge", + "hogehoge.bsky.social", "email", "accessJwt", "refreshJwt", true); + RecordOperator ope; - ope.setAccount(m_service + "/facet", QString(), QString(), QString(), "dummy", QString()); + ope.setAccount(uuid); QHash hash = UnitTestCommon::loadPostHash(":/data/com.atproto.repo.createRecord_post.expect"); @@ -117,8 +124,12 @@ void hagoromo_test::test_RecordOperator() while (i.hasNext()) { i.next(); + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/facet", "id", "pass", i.key(), "handle", "email", + "accessJwt", "refreshJwt", true); + ope.clear(); - ope.setAccount(m_service + "/facet", i.key(), "handle", "email", "accessJwt", "refreshJwt"); + ope.setAccount(uuid); ope.setText(i.value()); QSignalSpy spy(&ope, SIGNAL(finished(bool, const QString &, const QString &))); @@ -134,9 +145,12 @@ void hagoromo_test::test_RecordOperator() void hagoromo_test::test_RecordOperator_profile() { + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/profile/1", "id", "pass", "did:plc:ipj5qejfoqu6eukvt72uhyit", + "hogehoge.bsky.social", "email", "accessJwt", "refreshJwt", true); + RecordOperator ope; - ope.setAccount(m_service + "/profile/1", "did:plc:ipj5qejfoqu6eukvt72uhyit", QString(), - QString(), "dummy", QString()); + ope.setAccount(uuid); { QSignalSpy spy(&ope, SIGNAL(finished(bool, const QString &, const QString &))); ope.updateProfile("", "", "description", "display_name"); @@ -148,8 +162,10 @@ void hagoromo_test::test_RecordOperator_profile() QVERIFY(arguments.at(0).toBool() == true); } - ope.setAccount(m_service + "/profile/3.1", "did:plc:ipj5qejfoqu6eukvt72uhyit", QString(), - QString(), "dummy", QString()); + uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/profile/3.1", "id", "pass", "did:plc:ipj5qejfoqu6eukvt72uhyit", + "hogehoge.bsky.social", "email", "accessJwt", "refreshJwt", true); + ope.setAccount(uuid); { QSignalSpy spy(&ope, SIGNAL(finished(bool, const QString &, const QString &))); ope.updatePostPinning( @@ -163,8 +179,10 @@ void hagoromo_test::test_RecordOperator_profile() QVERIFY(arguments.at(0).toBool() == true); } - ope.setAccount(m_service + "/profile/3.2", "did:plc:ipj5qejfoqu6eukvt72uhyit", QString(), - QString(), "dummy", QString()); + uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/profile/3.2", "id", "pass", "did:plc:ipj5qejfoqu6eukvt72uhyit", + "hogehoge.bsky.social", "email", "accessJwt", "refreshJwt", true); + ope.setAccount(uuid); { QSignalSpy spy(&ope, SIGNAL(finished(bool, const QString &, const QString &))); ope.updatePostPinning(QString(), QString()); @@ -180,9 +198,13 @@ void hagoromo_test::test_RecordOperator_profile() void hagoromo_test::test_FeedGeneratorListModel() { FeedGeneratorListModel model; - model.setAccount(m_service + "/generator", QString(), QString(), QString(), "dummy", QString()); model.setDisplayInterval(0); { + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/generator", "id", "pass", + "did:plc:ipj5qejfoqu6eukvt72uhyit", "hogehoge.bsky.social", "email", "accessJwt", + "refreshJwt", true); + model.setAccount(uuid); QSignalSpy spy(&model, SIGNAL(runningChanged())); model.getLatest(); spy.wait(); @@ -204,8 +226,11 @@ void hagoromo_test::test_FeedGeneratorListModel() } { // save - model.setAccount(m_service + "/generator/save", QString(), QString(), QString(), "dummy", - QString()); + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/generator/save", "id", "pass", + "did:plc:ipj5qejfoqu6eukvt72uhyit", "hogehoge.bsky.social", "email", "accessJwt", + "refreshJwt", true); + model.setAccount(uuid); QSignalSpy spy(&model, SIGNAL(runningChanged())); model.saveGenerator( "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/hot-classic"); @@ -214,11 +239,14 @@ void hagoromo_test::test_FeedGeneratorListModel() } { // remove - model.setAccount(m_service + "/generator/remove", QString(), QString(), QString(), "dummy", - QString()); + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/generator/remove", "id", "pass", + "did:plc:ipj5qejfoqu6eukvt72uhyit", "hogehoge.bsky.social", "email", "accessJwt", + "refreshJwt", true); + model.setAccount(uuid); QSignalSpy spy(&model, SIGNAL(runningChanged())); model.removeGenerator( - "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/with-friends"); + "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot"); spy.wait(10 * 1000); QVERIFY2(spy.count() == 2, QString("spy.count()=%1").arg(spy.count()).toUtf8()); } @@ -226,6 +254,10 @@ void hagoromo_test::test_FeedGeneratorListModel() void hagoromo_test::test_FeedGeneratorLink() { + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/generator", "id", "pass", "did:plc:ipj5qejfoqu6eukvt72uhyit", + "hogehoge.bsky.social", "email", "accessJwt", "refreshJwt", true); + FeedGeneratorLink link; QVERIFY(link.checkUri("https://bsky.app/profile/did:plc:hoge/feed/aaaaaaaa", "feed") == true); @@ -238,7 +270,7 @@ void hagoromo_test::test_FeedGeneratorLink() QVERIFY(link.checkUri("https://bsky.app/profile/handle.com/feed/aaaaaaaa", "feed") == true); QVERIFY(link.checkUri("https://bsky.app/profile/@handle.com/feed/aaaaaaaa", "feed") == false); - link.setAccount(m_service + "/generator", QString(), QString(), QString(), "dummy", QString()); + link.setAccount(uuid); { QSignalSpy spy(&link, SIGNAL(runningChanged())); link.getFeedGenerator("https://bsky.app/profile/did:plc:hoge/feed/aaaaaaaa"); @@ -254,6 +286,8 @@ void hagoromo_test::test_FeedGeneratorLink() void hagoromo_test::test_AccountListModel() { + AccountManager::getInstance()->clear(); + QString temp_path = Common::appDataFolder() + "/account.json"; if (QFile::exists(temp_path)) { QFile::remove(temp_path); @@ -270,11 +304,10 @@ void hagoromo_test::test_AccountListModel() AccountListModel model2; { - QSignalSpy spy(&model2, SIGNAL(updatedAccount(int, const QString &))); + QSignalSpy spy(&model2, SIGNAL(finished())); model2.load(); - spy.wait(10 * 1000); - spy.wait(10 * 1000); - QVERIFY2(spy.count() == 2, QString("spy.count()=%1").arg(spy.count()).toUtf8()); + spy.wait(15 * 1000); + QVERIFY2(spy.count() == 1, QString("spy.count()=%1").arg(spy.count()).toUtf8()); } QVERIFY2(model2.rowCount() == 2, QString::number(model2.rowCount()).toLocal8Bit()); int row = 0; @@ -289,14 +322,164 @@ void hagoromo_test::test_AccountListModel() QVERIFY2(model2.item(row, AccountListModel::RefreshJwtRole).toString() == "refreshJwt_account2_refresh", model2.item(row, AccountListModel::RefreshJwtRole).toString().toLocal8Bit()); + + // model.update(0, AccountListModel::UuidRole, "UuidRole"); + // model.update(0, AccountListModel::IsMainRole, "IsMainRole"); + model.update(0, AccountListModel::ServiceRole, "ServiceRole"); + model.update(0, AccountListModel::ServiceEndpointRole, "ServiceEndpointRole"); + model.update(0, AccountListModel::IdentifierRole, "IdentifierRole"); + model.update(0, AccountListModel::PasswordRole, "PasswordRole"); + model.update(0, AccountListModel::DidRole, "DidRole"); + model.update(0, AccountListModel::HandleRole, "HandleRole"); + model.update(0, AccountListModel::EmailRole, "EmailRole"); + model.update(0, AccountListModel::AccessJwtRole, "AccessJwtRole"); + model.update(0, AccountListModel::RefreshJwtRole, "RefreshJwtRole"); + model.update(0, AccountListModel::DisplayNameRole, "DisplayNameRole"); + model.update(0, AccountListModel::DescriptionRole, "DescriptionRole"); + model.update(0, AccountListModel::AvatarRole, "AvatarRole"); + model.update(0, AccountListModel::PostLanguagesRole, + QStringList() << "PostLanguagesRole1" + << "PostLanguagesRole2"); + model.update(0, AccountListModel::ThreadGateTypeRole, "ThreadGateTypeRole"); + model.update(0, AccountListModel::ThreadGateOptionsRole, + QStringList() << "ThreadGateOptionsRole1" + << "ThreadGateOptionsRole2"); + model.update(0, AccountListModel::PostGateQuoteEnabledRole, true); + + // QVERIFY2(model.item(0, AccountListModel::UuidRole).toString() == "UuidRole", + // model.item(0, AccountListModel::UuidRole).toString().toLocal8Bit()); + // QVERIFY2(model.item(0, AccountListModel::IsMainRole).toString() == "IsMainRole", + // model.item(0, AccountListModel::IsMainRole).toString().toLocal8Bit()); + QVERIFY2(model.item(0, AccountListModel::ServiceRole).toString() == "ServiceRole", + model.item(0, AccountListModel::ServiceRole).toString().toLocal8Bit()); + QVERIFY2(model.item(0, AccountListModel::ServiceEndpointRole).toString() + == "ServiceEndpointRole", + model.item(0, AccountListModel::ServiceEndpointRole).toString().toLocal8Bit()); + QVERIFY2(model.item(0, AccountListModel::IdentifierRole).toString() == "IdentifierRole", + model.item(0, AccountListModel::IdentifierRole).toString().toLocal8Bit()); + QVERIFY2(model.item(0, AccountListModel::PasswordRole).toString() == "PasswordRole", + model.item(0, AccountListModel::PasswordRole).toString().toLocal8Bit()); + QVERIFY2(model.item(0, AccountListModel::DidRole).toString() == "DidRole", + model.item(0, AccountListModel::DidRole).toString().toLocal8Bit()); + QVERIFY2(model.item(0, AccountListModel::HandleRole).toString() == "HandleRole", + model.item(0, AccountListModel::HandleRole).toString().toLocal8Bit()); + QVERIFY2(model.item(0, AccountListModel::EmailRole).toString() == "EmailRole", + model.item(0, AccountListModel::EmailRole).toString().toLocal8Bit()); + QVERIFY2(model.item(0, AccountListModel::AccessJwtRole).toString() == "AccessJwtRole", + model.item(0, AccountListModel::AccessJwtRole).toString().toLocal8Bit()); + QVERIFY2(model.item(0, AccountListModel::RefreshJwtRole).toString() == "RefreshJwtRole", + model.item(0, AccountListModel::RefreshJwtRole).toString().toLocal8Bit()); + QVERIFY2(model.item(0, AccountListModel::DisplayNameRole).toString() == "DisplayNameRole", + model.item(0, AccountListModel::DisplayNameRole).toString().toLocal8Bit()); + QVERIFY2(model.item(0, AccountListModel::DescriptionRole).toString() == "DescriptionRole", + model.item(0, AccountListModel::DescriptionRole).toString().toLocal8Bit()); + QVERIFY2(model.item(0, AccountListModel::AvatarRole).toString() == "AvatarRole", + model.item(0, AccountListModel::AvatarRole).toString().toLocal8Bit()); + QVERIFY2(model.item(0, AccountListModel::PostLanguagesRole).toStringList() + == QStringList() << "PostLanguagesRole1" + << "PostLanguagesRole2", + model.item(0, AccountListModel::PostLanguagesRole) + .toStringList() + .join(",") + .toLocal8Bit()); + QVERIFY2(model.item(0, AccountListModel::ThreadGateTypeRole).toString() == "ThreadGateTypeRole", + model.item(0, AccountListModel::ThreadGateTypeRole).toString().toLocal8Bit()); + QVERIFY2(model.item(0, AccountListModel::ThreadGateOptionsRole).toStringList() + == QStringList() << "ThreadGateOptionsRole1" + << "ThreadGateOptionsRole2", + model.item(0, AccountListModel::ThreadGateOptionsRole) + .toStringList() + .join(",") + .toLocal8Bit()); + QVERIFY2(model.item(0, AccountListModel::PostGateQuoteEnabledRole).toBool() == true, + QString::number(model.item(0, AccountListModel::PostGateQuoteEnabledRole).toBool()) + .toLocal8Bit()); +} + +void hagoromo_test::test_AccountManager() +{ + + QString temp_path = Common::appDataFolder() + "/account.json"; + if (QFile::exists(temp_path)) { + QFile::remove(temp_path); + } + + AccountManager *manager = AccountManager::getInstance(); + AtProtocolInterface::AccountData account; + + manager->clear(); + manager->updateAccount(QString(), m_service + "/account/account1", "id1", "password1", + "did:plc:account1", "account1.relog.tech", "account1@relog.tech", + "accessJwt_account1", "refreshJwt_account1", false); + manager->updateAccount(QString(), m_service + "/account/account2", "id2", "password2", + "did:plc:account2", "account2.relog.tech", "account2@relog.tech", + "accessJwt_account2", "refreshJwt_account2", false); + + QStringList uuids = manager->getUuids(); + + QVERIFY(uuids.count() == 2); + + account = manager->getAccount(uuids.at(0)); + QVERIFY(account.service == m_service + "/account/account1"); + QVERIFY(account.password == "password1"); + QVERIFY(account.did == "did:plc:account1"); + QVERIFY(account.handle == "account1.relog.tech"); + QVERIFY(account.email == "account1@relog.tech"); + QVERIFY(account.accessJwt == "accessJwt_account1"); + QVERIFY(account.refreshJwt == "refreshJwt_account1"); + + account = manager->getAccount(uuids.at(1)); + QVERIFY(account.service == m_service + "/account/account2"); + QVERIFY(account.password == "password2"); + QVERIFY(account.did == "did:plc:account2"); + QVERIFY(account.handle == "account2.relog.tech"); + QVERIFY(account.email == "account2@relog.tech"); + QVERIFY(account.accessJwt == "accessJwt_account2"); + QVERIFY(account.refreshJwt == "refreshJwt_account2"); + + QVERIFY(manager->checkAllAccountsReady() == false); + + manager->save(); + + manager->removeAccount(manager->getUuids().at(0)); + QVERIFY(manager->getUuids().count() == 1); + + account = manager->getAccount(manager->getUuids().at(0)); + QVERIFY(account.service == m_service + "/account/account2"); + QVERIFY(account.password == "password2"); + QVERIFY(account.did == "did:plc:account2"); + QVERIFY(account.handle == "account2.relog.tech"); + QVERIFY(account.email == "account2@relog.tech"); + QVERIFY(account.accessJwt == "accessJwt_account2"); + QVERIFY(account.refreshJwt == "refreshJwt_account2"); + + manager->clear(); + QVERIFY(manager->getUuids().isEmpty()); + + { + QSignalSpy spy(manager, SIGNAL(finished())); + manager->load(); + spy.wait(10 * 1000); + QVERIFY2(spy.count() == 1, QString("spy.count()=%1").arg(spy.count()).toUtf8()); + } + + uuids = manager->getUuids(); + QVERIFY(manager->count() == 1); + + account = manager->getAccount(uuids.at(0)); + QVERIFY2(account.service == m_service + "/account/account2", account.service.toLocal8Bit()); + QVERIFY(account.service_endpoint == "http://localhost:%1/response/account/account2"); + QVERIFY(account.did == "did:plc:account2_refresh"); } void hagoromo_test::test_ListsListModel() { - ListsListModel model; + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/lists/lists", "id", "pass", "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", + "hogehoge.bsky.social", "email", "accessJwt", "refreshJwt", true); - model.setAccount(m_service + "/lists/lists", "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", QString(), - QString(), "dummy", QString()); + ListsListModel model; + model.setAccount(uuid); model.setVisibilityType(ListsListModel::VisibilityTypeAll); { @@ -386,10 +569,13 @@ void hagoromo_test::test_ListsListModel() void hagoromo_test::test_ListsListModel_search() { - ListsListModel model; + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/lists/search", "id", "pass", + "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", "hogehoge.bsky.social", "email", "accessJwt", + "refreshJwt", true); - model.setAccount(m_service + "/lists/search", QString(), QString(), QString(), "dummy", - QString()); + ListsListModel model; + model.setAccount(uuid); model.setSearchTarget("did:plc:user_42"); { QSignalSpy spy(&model, SIGNAL(runningChanged())); @@ -444,10 +630,12 @@ void hagoromo_test::test_ListsListModel_error() void hagoromo_test::test_ListItemListModel() { - ListItemListModel model; + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/lists/list", "id", "pass", "did:plc:ipj5qejfoqu6eukvt72uhyit", + "hogehoge.bsky.social", "email", "accessJwt", "refreshJwt", true); - model.setAccount(m_service + "/lists/list", "did:plc:ipj5qejfoqu6eukvt72uhyit", QString(), - QString(), "dummy", QString()); + ListItemListModel model; + model.setAccount(uuid); model.setUri("at://did:plc:ipj5qejfoqu6eukvt72uhyit/app.bsky.graph.list/3k7igyxfizg27"); { @@ -515,10 +703,13 @@ void hagoromo_test::test_ListItemListModel_error() void hagoromo_test::test_ListFeedListModel() { - ListFeedListModel model; + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/lists/feed/0", "id", "pass", + "did:plc:l4fsx4ujos7uw7n4ijq2ulgs", "hogehoge.bsky.social", "email", "accessJwt", + "refreshJwt", true); - model.setAccount(m_service + "/lists/feed/0", "did:plc:l4fsx4ujos7uw7n4ijq2ulgs", QString(), - QString(), "dummy", QString()); + ListFeedListModel model; + model.setAccount(uuid); model.setUri("at://did:plc:ipj5qejfoqu6eukvt72uhyit/app.bsky.graph.list/3k7igyxfizg27"); { QSignalSpy spy(&model, SIGNAL(runningChanged())); @@ -563,14 +754,17 @@ void hagoromo_test::test_putPreferences(const QString &path, const QByteArray &b json_doc_expect = UnitTestCommon::loadJson(":/data/generator/remove/app.bsky.actor.putPreferences"); } + QJsonDocument json_doc_expect2 = + QJsonDocument(copyObject(json_doc_expect.object(), QStringList() << "id")); QJsonDocument json_doc = QJsonDocument::fromJson(body); + QJsonDocument json_doc2 = QJsonDocument(copyObject(json_doc.object(), QStringList() << "id")); - if (json_doc_expect.object() != json_doc.object()) { + if (json_doc_expect2.object() != json_doc2.object()) { qDebug().noquote().nospace() << QString("\nexpect:%1\nactual:%2\n") - .arg(json_doc_expect.toJson(), json_doc.toJson()); + .arg(json_doc_expect2.toJson(), json_doc2.toJson()); } - QVERIFY(json_doc_expect.object() == json_doc.object()); + QVERIFY(json_doc_expect2.object() == json_doc2.object()); } void hagoromo_test::test_putRecord(const QString &path, const QByteArray &body) @@ -601,6 +795,32 @@ void hagoromo_test::verifyStr(const QString &expect, const QString &actual) QString("\nexpect:%1\nactual:%2\n").arg(expect, actual).toLocal8Bit()); } +QJsonObject hagoromo_test::copyObject(const QJsonObject &src, const QStringList &excludes) +{ + + QJsonObject dest; + for (const auto &key : src.keys()) { + if (excludes.contains(key)) + continue; + if (src.value(key).isObject()) { + dest.insert(key, copyObject(src.value(key).toObject(), excludes)); + } else if (src.value(key).isArray()) { + QJsonArray dest_array; + for (const auto value : src.value(key).toArray()) { + if (value.isObject()) { + dest_array.append(copyObject(value.toObject(), excludes)); + } else { + dest_array.append(value); + } + } + dest.insert(key, dest_array); + } else { + dest.insert(key, src.value(key)); + } + } + return dest; +} + QTEST_MAIN(hagoromo_test) #include "tst_hagoromo_test2.moc" diff --git a/tests/http_test/tst_http_test.cpp b/tests/http_test/tst_http_test.cpp index d3075cbe..b273409d 100644 --- a/tests/http_test/tst_http_test.cpp +++ b/tests/http_test/tst_http_test.cpp @@ -29,7 +29,7 @@ private slots: http_test::http_test() { QCoreApplication::setOrganizationName(QStringLiteral("relog")); - QCoreApplication::setApplicationName(QStringLiteral("Hagoromo")); + QCoreApplication::setApplicationName(QStringLiteral("Hagoromo_unittest")); m_listenPort = m_mockServer.listen(QHostAddress::LocalHost, 0); m_service = QString("http://localhost:%1/response").arg(m_listenPort); @@ -39,8 +39,14 @@ http_test::http_test() qDebug() << "receive POST" << request.url().path(); QByteArray data; if (request.url().path() == "/response/xrpc/com.atproto.repo.uploadBlob") { +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) for (const auto key : request.headers().keys()) { QString value = request.headers().value(key).toString(); +#else + for(const auto &header: request.headers()){ + QString value = header.second; + QString key = header.first; +#endif qDebug().noquote() << " header:" << key << value; if (key == "PostFile") { QVERIFY(QFile::exists(value)); diff --git a/tests/log_test/tst_log_test.cpp b/tests/log_test/tst_log_test.cpp index 2ffb0ee9..6bb1a94b 100644 --- a/tests/log_test/tst_log_test.cpp +++ b/tests/log_test/tst_log_test.cpp @@ -4,6 +4,7 @@ #include #include +#include "tools/accountmanager.h" #include "webserver.h" #include "log/logmanager.h" #include "log/logoperator.h" @@ -42,7 +43,7 @@ log_test::log_test() qRegisterMetaType>("QList"); QCoreApplication::setOrganizationName(QStringLiteral("relog")); - QCoreApplication::setApplicationName(QStringLiteral("Hagoromo")); + QCoreApplication::setApplicationName(QStringLiteral("Hagoromo_unittest")); m_listenPort = m_mockServer.listen(QHostAddress::LocalHost, 0); m_service = QString("http://localhost:%1/response").arg(m_listenPort); @@ -604,9 +605,12 @@ void log_test::test_LogFeedListModel() account.handle = "log.manager.test"; account.accessJwt = "access jwt"; + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), account.service + "/posts/10", "id", "pass", account.did, account.handle, + account.email, account.accessJwt, account.refreshJwt, true); + LogFeedListModel model; - model.setAccount(account.service + "/posts/10", account.did, account.handle, account.email, - account.accessJwt, account.refreshJwt); + model.setAccount(uuid); model.setTargetDid(account.did); model.setTargetHandle(account.handle); model.setTargetAvatar("test_avatar.jpg"); diff --git a/tests/oauth_test/oauth_test.pro b/tests/oauth_test/oauth_test.pro new file mode 100644 index 00000000..eb337074 --- /dev/null +++ b/tests/oauth_test/oauth_test.pro @@ -0,0 +1,17 @@ +QT += testlib httpserver gui + +CONFIG += qt console warn_on depend_includepath testcase +CONFIG -= app_bundle + +TEMPLATE = app +DEFINES += HAGOROMO_UNIT_TEST + +SOURCES += tst_oauth_test.cpp + +include(../common/common.pri) +include(../deps.pri) +include(../../openssl/openssl.pri) +include(../../zlib/zlib.pri) + +RESOURCES += \ + oauth_test.qrc diff --git a/tests/oauth_test/oauth_test.qrc b/tests/oauth_test/oauth_test.qrc new file mode 100644 index 00000000..d5427b01 --- /dev/null +++ b/tests/oauth_test/oauth_test.qrc @@ -0,0 +1,10 @@ + + + response/1/.well-known/oauth-protected-resource + response/2/xrpc/com.atproto.repo.describeRepo + response/2/.well-known/oauth-authorization-server + response/2/oauth/par + response/2/oauth/authorize + response/2/oauth/token + + diff --git a/tests/oauth_test/response/1/.well-known/oauth-protected-resource b/tests/oauth_test/response/1/.well-known/oauth-protected-resource new file mode 100644 index 00000000..c16eb650 --- /dev/null +++ b/tests/oauth_test/response/1/.well-known/oauth-protected-resource @@ -0,0 +1,15 @@ +{ + "resource": "http://localhost:{{SERVER_PORT_NO}}/response/2", + "authorization_servers": [ + "http://localhost:{{SERVER_PORT_NO}}/response/2" + ], + "scopes_supported": [ + "profile", + "email", + "phone" + ], + "bearer_methods_supported": [ + "header" + ], + "resource_documentation": "https://atproto.com" +} diff --git a/tests/oauth_test/response/2/.well-known/oauth-authorization-server b/tests/oauth_test/response/2/.well-known/oauth-authorization-server new file mode 100644 index 00000000..1f106d8b --- /dev/null +++ b/tests/oauth_test/response/2/.well-known/oauth-authorization-server @@ -0,0 +1,90 @@ +{ + "issuer": "http://localhost:{{SERVER_PORT_NO}}/response/2/issuer", + "scopes_supported": [ + "atproto", + "transition:generic", + "transition:chat.bsky" + ], + "subject_types_supported": [ + "public" + ], + "response_types_supported": [ + "code" + ], + "response_modes_supported": [ + "query", + "fragment", + "form_post" + ], + "grant_types_supported": [ + "authorization_code", + "refresh_token" + ], + "code_challenge_methods_supported": [ + "S256", + "plain" + ], + "ui_locales_supported": [ + "en-US" + ], + "display_values_supported": [ + "page", + "popup", + "touch" + ], + "authorization_response_iss_parameter_supported": true, + "request_object_signing_alg_values_supported": [ + "RS256", + "RS384", + "RS512", + "PS256", + "PS384", + "PS512", + "ES256", + "ES256K", + "ES384", + "ES512", + "none" + ], + "request_object_encryption_alg_values_supported": [], + "request_object_encryption_enc_values_supported": [], + "request_parameter_supported": true, + "request_uri_parameter_supported": true, + "require_request_uri_registration": true, + "jwks_uri": "http://localhost:{{SERVER_PORT_NO}}/response/2/oauth/jwks", + "authorization_endpoint": "http://localhost:{{SERVER_PORT_NO}}/response/2/oauth/authorize", + "token_endpoint": "http://localhost:{{SERVER_PORT_NO}}/response/2/oauth/token", + "token_endpoint_auth_methods_supported": [ + "none", + "private_key_jwt" + ], + "token_endpoint_auth_signing_alg_values_supported": [ + "RS256", + "RS384", + "RS512", + "PS256", + "PS384", + "PS512", + "ES256", + "ES256K", + "ES384", + "ES512" + ], + "revocation_endpoint": "http://localhost:{{SERVER_PORT_NO}}/response/2/oauth/revoke", + "introspection_endpoint": "http://localhost:{{SERVER_PORT_NO}}/response/2/oauth/introspect", + "pushed_authorization_request_endpoint": "http://localhost:{{SERVER_PORT_NO}}/response/2/oauth/par", + "require_pushed_authorization_requests": true, + "dpop_signing_alg_values_supported": [ + "RS256", + "RS384", + "RS512", + "PS256", + "PS384", + "PS512", + "ES256", + "ES256K", + "ES384", + "ES512" + ], + "client_id_metadata_document_supported": true +} diff --git a/tests/oauth_test/response/2/oauth/authorize b/tests/oauth_test/response/2/oauth/authorize new file mode 100644 index 00000000..e69de29b diff --git a/tests/oauth_test/response/2/oauth/par b/tests/oauth_test/response/2/oauth/par new file mode 100644 index 00000000..4ac5606e --- /dev/null +++ b/tests/oauth_test/response/2/oauth/par @@ -0,0 +1,4 @@ +{ + "request_uri": "urn:ietf:params:oauth:request_uri:req-05650c01604941dc674f0af9cb032aca", + "expires_in": 299 +} diff --git a/tests/oauth_test/response/2/oauth/token b/tests/oauth_test/response/2/oauth/token new file mode 100644 index 00000000..a84748fd --- /dev/null +++ b/tests/oauth_test/response/2/oauth/token @@ -0,0 +1,6 @@ +{ + "access_token": "access token", + "token_type": "DPoP", + "expires_in": 2677, + "refresh_token": "refresh token" +} diff --git a/tests/oauth_test/response/2/xrpc/com.atproto.repo.describeRepo b/tests/oauth_test/response/2/xrpc/com.atproto.repo.describeRepo new file mode 100644 index 00000000..5cfb1896 --- /dev/null +++ b/tests/oauth_test/response/2/xrpc/com.atproto.repo.describeRepo @@ -0,0 +1,46 @@ +{ + "handle": "ioriayane.relog.tech", + "did": "did:plc:ipj5qejfoqu6eukvt72uhyit", + "didDoc": { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/multikey/v1", + "https://w3id.org/security/suites/secp256k1-2019/v1" + ], + "id": "did:plc:ipj5qejfoqu6eukvt72uhyit", + "alsoKnownAs": [ + "at://ioriayane.relog.tech" + ], + "verificationMethod": [ + { + "id": "did:plc:ipj5qejfoqu6eukvt72uhyit#atproto", + "type": "Multikey", + "controller": "did:plc:ipj5qejfoqu6eukvt72uhyit", + "publicKeyMultibase": "zQ3shYNo9wSChjqSMPZUmgwjEFMgsk5efZX5UYm8SFrXR5YUd" + } + ], + "service": [ + { + "id": "#atproto_pds", + "type": "AtprotoPersonalDataServer", + "serviceEndpoint": "http://localhost:{{SERVER_PORT_NO}}/response/1" + } + ] + }, + "collections": [ + "app.bsky.actor.profile", + "app.bsky.feed.generator", + "app.bsky.feed.like", + "app.bsky.feed.post", + "app.bsky.feed.repost", + "app.bsky.feed.threadgate", + "app.bsky.graph.block", + "app.bsky.graph.follow", + "app.bsky.graph.list", + "app.bsky.graph.listitem", + "app.bsky.graph.starterpack", + "chat.bsky.actor.declaration", + "com.whtwnd.blog.entry" + ], + "handleIsCorrect": true +} diff --git a/tests/oauth_test/tst_oauth_test.cpp b/tests/oauth_test/tst_oauth_test.cpp new file mode 100644 index 00000000..864a2811 --- /dev/null +++ b/tests/oauth_test/tst_oauth_test.cpp @@ -0,0 +1,424 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "tools/authorization.h" +#include "tools/jsonwebtoken.h" +#include "tools/es256.h" +#include "http/simplehttpserver.h" + +class oauth_test : public QObject +{ + Q_OBJECT + +public: + oauth_test(); + ~oauth_test(); + +private slots: + void initTestCase(); + void cleanupTestCase(); + void test_oauth_process(); + void test_oauth_server(); + void test_oauth(); + void test_jwt(); + void test_es256(); + +private: + SimpleHttpServer m_server; + quint16 m_listenPort; + + void test_get(const QString &url, const QByteArray &except_data); + void verify_jwt(const QByteArray &jwt, EVP_PKEY *pkey); +}; + +oauth_test::oauth_test() +{ + QCoreApplication::setOrganizationName(QStringLiteral("relog")); + QCoreApplication::setApplicationName(QStringLiteral("Hagoromo_unittest")); + + m_listenPort = m_server.listen(QHostAddress::LocalHost, 0); + connect(&m_server, &SimpleHttpServer::received, this, + [=](const QHttpServerRequest &request, bool &result, QByteArray &data, + QByteArray &mime_type) { + // + qDebug().noquote() << request.url(); + QString path = SimpleHttpServer::convertResoucePath(request.url()); + qDebug().noquote() << " res path =" << path; + + if (path.endsWith("/oauth/token")) { + qDebug().noquote() << "Verify jwt"; +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) + QVERIFY(request.headers().contains("DPoP")); + verify_jwt(request.headers().value("DPoP").toByteArray(), nullptr); +#else + bool exist = false; + for(const auto &header: request.headers()){ + if(header.first == "DPoP"){ + verify_jwt(header.second, nullptr); + exist = true; + break; + } + } + QVERIFY(exist); +#endif + } + + if (!QFile::exists(path)) { + result = false; + } else { + mime_type = "application/json"; + result = SimpleHttpServer::readFile(path, data); + data.replace("{{SERVER_PORT_NO}}", QString::number(m_listenPort).toLocal8Bit()); + qDebug().noquote() << " result =" << result; + } + }); +} + +oauth_test::~oauth_test() { } + +void oauth_test::initTestCase() { } + +void oauth_test::cleanupTestCase() { } + +void oauth_test::test_oauth_process() +{ + QString code_challenge; + + const uint8_t base[] = { 116, 24, 223, 180, 151, 153, 224, 37, 79, 250, 96, + 125, 216, 173, 187, 186, 22, 212, 37, 77, 105, 214, + 191, 240, 91, 88, 5, 88, 83, 132, 141, 121 }; + QByteArray base_ba; //(QByteArray::fromRawData(static_cast(base), sizeof(base))); + for (int i = 0; i < sizeof(base); i++) { + base_ba.append(base[i]); + } + qDebug() << "codeVerifier" << sizeof(base) << base_ba.size() + << base_ba.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); + + QString msg; + QByteArray sha256 = QCryptographicHash::hash( + base_ba.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals), + QCryptographicHash::Sha256); + for (const auto s : sha256) { + msg += QString::number(static_cast(s)) + ", "; + } + qDebug() << msg; + qDebug() << "codeChallenge" + << sha256.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); + + Authorization oauth; + oauth.makeCodeChallenge(); + qDebug() << "codeChallenge" << oauth.codeChallenge(); + qDebug() << "codeVerifier" << oauth.codeVerifier(); + + // QVERIFY(oauth.codeChallenge() + // == sha256.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)); + // QVERIFY(oauth.codeVerifier() + // == base_ba.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)); +} + +void oauth_test::test_oauth_server() +{ + Authorization oauth; +#if 0 + { + QSignalSpy spy(&oauth, SIGNAL(madeRequestUrl(const QString &))); + oauth.start("https://bsky.social", "ioriayane.bsky.social"); + spy.wait(); + QVERIFY2(spy.count() == 1, QString("spy.count()=%1").arg(spy.count()).toUtf8()); + QList arguments = spy.takeFirst(); + QString request_url = arguments.at(0).toString(); + qDebug().noquote() << "request url:" << request_url; + QDesktopServices::openUrl(request_url); + } + { + QSignalSpy spy(&oauth, SIGNAL(finished(bool))); + spy.wait(5 * 60 * 1000); + QVERIFY2(spy.count() == 1, QString("spy.count()=%1").arg(spy.count()).toUtf8()); + QList arguments = spy.takeFirst(); + QVERIFY(arguments.at(0).toBool()); + } + qDebug().noquote() << "DPoP for test"; + qDebug().noquote() << JsonWebToken::generate(oauth.tokenEndopoint(), oauth.clientId(), "GET", + oauth.dPopNonce()); +#elif 0 + AtProtocolType::OauthDefs::TokenResponse token; + token.refresh_token = "ref-121f89618c436"; + oauth.setToken(token); + oauth.setTokenEndopoint("https://bsky.social/oauth/token"); + oauth.setDPopNonce("8mo0kjo"); + oauth.setListenPort("65073"); + oauth.makeClientId(); + { + QSignalSpy spy(&oauth, SIGNAL(finished(bool))); + oauth.requestToken(true); + spy.wait(); + QVERIFY2(spy.count() == 1, QString("spy.count()=%1").arg(spy.count()).toUtf8()); + QList arguments = spy.takeFirst(); + QVERIFY(arguments.at(0).toBool()); + } +#endif +} + +void oauth_test::test_oauth() +{ + // response/1 : pds + // response/2 : entry-way + + Authorization oauth; + oauth.setRedirectTimeout(20); + + QString pds = QString("http://localhost:%1/response/2").arg(m_listenPort); + QString handle = "ioriayane.relog.tech"; + + oauth.reset(); + { + QSignalSpy spy(&oauth, SIGNAL(serviceEndpointChanged())); + oauth.start(pds, handle); + spy.wait(); + QVERIFY2(spy.count() == 1, QString("spy.count()=%1").arg(spy.count()).toUtf8()); + } + QVERIFY(oauth.serviceEndpoint() == QString("http://localhost:%1/response/1").arg(m_listenPort)); + + { + QSignalSpy spy(&oauth, SIGNAL(authorizationServerChanged())); + spy.wait(); + QVERIFY2(spy.count() == 1, QString("spy.count()=%1").arg(spy.count()).toUtf8()); + } + QVERIFY(oauth.authorizationServer() + == QString("http://localhost:%1/response/2").arg(m_listenPort)); + + { + QSignalSpy spy(&oauth, SIGNAL(pushedAuthorizationRequestEndpointChanged())); + spy.wait(); + QVERIFY2(spy.count() == 1, QString("spy.count()=%1").arg(spy.count()).toUtf8()); + } + QVERIFY(oauth.pushedAuthorizationRequestEndpoint() + == QString("http://localhost:%1/response/2/oauth/par").arg(m_listenPort)); + QVERIFY(oauth.authorizationEndpoint() + == QString("http://localhost:%1/response/2/oauth/authorize").arg(m_listenPort)); + + // + QString request_url; + { + QSignalSpy spy(&oauth, SIGNAL(madeRequestUrl(const QString &))); + spy.wait(); + QVERIFY2(spy.count() == 1, QString("spy.count()=%1").arg(spy.count()).toUtf8()); + QList arguments = spy.takeFirst(); + request_url = arguments.at(0).toString(); + QVERIFY2(request_url + == (QStringLiteral("http://localhost:") + QString::number(m_listenPort) + + QStringLiteral( + "/response/2/oauth/" + "authorize?client_id=https%3A%2F%2Foauth.hagoromo.relog.tech%" + "2Fclient-metadata.json&request_uri=urn%3Aietf%" + "3Aparams%3Aoauth%3Arequest_uri%3Areq-" + "05650c01604941dc674f0af9cb032aca")), + request_url.toLocal8Bit()); + } + + { + // ブラウザで認証ができないのでタイムアウトしてくるのを確認 + QSignalSpy spy(&oauth, SIGNAL(finished(bool))); + spy.wait(20 * 1000); + QVERIFY2(spy.count() == 1, QString("spy.count()=%1").arg(spy.count()).toUtf8()); + QList arguments = spy.takeFirst(); + QVERIFY(!arguments.at(0).toBool()); + } + + // ブラウザに認証しにいくURLからリダイレクトURLを取り出す + QUrl redirect_url; + { + QUrl request(request_url); + QUrlQuery request_query(request.query()); + QUrl client_id(request_query.queryItemValue("client_id", QUrl::FullyDecoded)); + QUrlQuery client_query(client_id.query()); + redirect_url = "http://127.0.0.1/tech/relog/hagoromo/oauth-callback"; + QUrlQuery redirect_query; + redirect_query.addQueryItem("iss", "iss-hogehoge"); + redirect_query.addQueryItem("state", oauth.state()); + redirect_query.addQueryItem("code", "code-hogehoge"); + redirect_url.setQuery(redirect_query); + qDebug().noquote() << "extract to " << redirect_url; + } + // 認証終了したていで続き + { + QSignalSpy spy(&oauth, SIGNAL(tokenChanged())); + oauth.startRedirectServer(); + redirect_url.setPort(oauth.listenPort().toInt()); + qDebug().noquote() << "port updated " << redirect_url; + test_get(redirect_url.toString(), QByteArray()); // ブラウザへのアクセスを模擬 + spy.wait(); + QVERIFY2(spy.count() == 1, QString("spy.count()=%1").arg(spy.count()).toUtf8()); + } + + QVERIFY2(oauth.token().access_token == "access token", + oauth.token().access_token.toLocal8Bit()); + QVERIFY2(oauth.token().token_type == "DPoP", oauth.token().token_type.toLocal8Bit()); + QVERIFY2(oauth.token().refresh_token == "refresh token", + oauth.token().refresh_token.toLocal8Bit()); + QVERIFY2(oauth.token().expires_in == 2677, + QString::number(oauth.token().expires_in).toLocal8Bit()); +} + +void oauth_test::test_jwt() +{ + QByteArray jwt = JsonWebToken::generate("https://hoge", "client_id", "GET", + "O_m5dyvKO7jNfnsfuYwB5GflhTuVaqCub4x3xVKqJ9Y"); + + qDebug().noquote() << jwt; + + verify_jwt(jwt, Es256::getInstance()->pKey()); + + QVERIFY(true); +} + +void oauth_test::test_es256() +{ + QString private_key_path = + QString("%1/%2/%3%4/private_key.pem") + .arg(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation)) + .arg(QCoreApplication::organizationName()) + .arg(QCoreApplication::applicationName()) + .arg(QStringLiteral("_debug")); + QFile::remove(private_key_path); + Es256::getInstance()->clear(); + + { + QString message = "header.payload"; + QByteArray sign = Es256::getInstance()->sign(message.toUtf8()); + QByteArray jwt = message.toUtf8() + '.' + + sign.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); + + QVERIFY(QFile::exists(private_key_path)); + verify_jwt(jwt, Es256::getInstance()->pKey()); + } + QFile::remove(private_key_path); + QVERIFY(!QFile::exists(private_key_path)); + { + QString message = "header2.payload2"; + QByteArray sign = Es256::getInstance()->sign(message.toUtf8()); + QByteArray jwt = message.toUtf8() + '.' + + sign.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); + + verify_jwt(jwt, Es256::getInstance()->pKey()); + } +} + +void oauth_test::test_get(const QString &url, const QByteArray &except_data) +{ + qDebug() << "test_get url" << url; + QNetworkRequest request((QUrl(url))); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + + QNetworkAccessManager *manager = new QNetworkAccessManager(this); + connect(manager, &QNetworkAccessManager::finished, [=](QNetworkReply *reply) { + qDebug() << "test_get reply" << reply->error() << reply->url(); + + QJsonDocument json_doc = QJsonDocument::fromJson(reply->readAll()); + + QVERIFY(reply->error() == QNetworkReply::NoError); + QVERIFY2(reply->readAll() == except_data, json_doc.toJson()); + + reply->deleteLater(); + manager->deleteLater(); + }); + manager->get(request); +} + +void oauth_test::verify_jwt(const QByteArray &jwt, EVP_PKEY *pkey) +{ + EC_KEY *ec_key = nullptr; + + const QByteArrayList jwt_parts = jwt.split('.'); + QVERIFY2(jwt_parts.length() == 3, jwt); + + QByteArray message = jwt_parts[0] + '.' + jwt_parts[1]; + QByteArray sig = QByteArray::fromBase64( + jwt_parts.last(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); + QVERIFY2(sig.length() == 64, QString::number(sig.length()).toLocal8Bit()); + QByteArray header = QByteArray::fromBase64( + jwt_parts.first(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals); + + bool use_jwk = false; + QJsonDocument json_doc = QJsonDocument::fromJson(header); + if (json_doc.object().contains("jwk")) { + QJsonObject jwk_obj = json_doc.object().value("jwk").toObject(); + QByteArray x_coord = QByteArray::fromBase64(jwk_obj.value("x").toString().toUtf8(), + QByteArray::Base64UrlEncoding + | QByteArray::OmitTrailingEquals); + QByteArray y_coord = QByteArray::fromBase64(jwk_obj.value("y").toString().toUtf8(), + QByteArray::Base64UrlEncoding + | QByteArray::OmitTrailingEquals); + QVERIFY(!x_coord.isEmpty()); + QVERIFY(!y_coord.isEmpty()); + + BIGNUM *x = BN_bin2bn(reinterpret_cast(x_coord.constData()), + x_coord.length(), nullptr); + BIGNUM *y = BN_bin2bn(reinterpret_cast(y_coord.constData()), + y_coord.length(), nullptr); + QVERIFY(x); + QVERIFY(y); + + ec_key = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1); + QVERIFY(ec_key); + QVERIFY(EC_KEY_set_public_key_affine_coordinates(ec_key, x, y)); + + pkey = EVP_PKEY_new(); + QVERIFY(EVP_PKEY_assign_EC_KEY(pkey, ec_key)); + + BN_free(y); + BN_free(x); + + use_jwk = true; + } + + // convert IEEE P1363 to DER + QByteArray sig_rr = sig.left(32); + QByteArray sig_ss = sig.right(32); + BIGNUM *ec_sig_r = NULL; + BIGNUM *ec_sig_s = NULL; + ec_sig_r = BN_bin2bn(reinterpret_cast(sig_rr.constData()), + sig_rr.length(), NULL); + ec_sig_s = BN_bin2bn(reinterpret_cast(sig_ss.constData()), + sig_ss.length(), NULL); + QVERIFY(ec_sig_r != NULL); + QVERIFY(ec_sig_s != NULL); + + ECDSA_SIG *ec_sig = ECDSA_SIG_new(); + QVERIFY(ECDSA_SIG_set0(ec_sig, ec_sig_r, ec_sig_s) == 1); + + unsigned char *der_sig = NULL; + int der_sig_len = i2d_ECDSA_SIG(ec_sig, &der_sig); + QVERIFY(der_sig_len > 0); + + // ECDSA署名の検証 + EVP_MD_CTX *mdctx = EVP_MD_CTX_new(); + QVERIFY(pkey != nullptr); + QVERIFY(EVP_DigestVerifyInit(mdctx, nullptr, EVP_sha256(), nullptr, pkey) > 0); + QVERIFY(EVP_DigestVerifyUpdate(mdctx, message.constData(), message.length()) > 0); + QVERIFY(EVP_DigestVerifyFinal(mdctx, der_sig, der_sig_len) > 0); + EVP_MD_CTX_free(mdctx); + + OPENSSL_free(der_sig); + ECDSA_SIG_free(ec_sig); + // BN_free(ec_sig_s); // ECDSA_SIG_freeで解放される + // BN_free(ec_sig_r); // ECDSA_SIG_freeで解放される + + if (use_jwk) { + EVP_PKEY_free(pkey); + // EC_KEY_free(ec_key); EVP_PKEY_freeで解放される + } +} + +QTEST_MAIN(oauth_test) + +#include "tst_oauth_test.moc" diff --git a/tests/realtime_test/tst_realtime_test.cpp b/tests/realtime_test/tst_realtime_test.cpp index c9e7645d..d9ad6f2e 100644 --- a/tests/realtime_test/tst_realtime_test.cpp +++ b/tests/realtime_test/tst_realtime_test.cpp @@ -10,6 +10,7 @@ #include "realtime/abstractpostselector.h" #include "realtime/firehosereceiver.h" #include "realtime/realtimefeedlistmodel.h" +#include "tools/accountmanager.h" using namespace RealtimeFeed; @@ -45,7 +46,7 @@ realtime_test::realtime_test() // : m_server(QStringLiteral("name"), QWebSocketServer::SecureMode, this) { QCoreApplication::setOrganizationName(QStringLiteral("relog")); - QCoreApplication::setApplicationName(QStringLiteral("Hagoromo")); + QCoreApplication::setApplicationName(QStringLiteral("Hagoromo_unittest")); m_listenPort = m_mockServer.listen(QHostAddress::LocalHost, 0); m_service = QString("http://localhost:%1/response").arg(m_listenPort); @@ -231,9 +232,12 @@ void realtime_test::test_Websock() void realtime_test::test_RealtimeFeedListModel() { + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/realtime/1", "id", "pass", "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", + "handle", "email", "accessJwt", "refreshJwt", true); + RealtimeFeedListModel model; - model.setAccount(m_service + "/realtime/1", "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", QString(), - QString(), "dummy", QString()); + model.setAccount(uuid); // model.setSelectorJson("{\"not\":{\"me\":{}}}"); // model.setSelectorJson("{\"not\":{\"following\":{}}}"); @@ -284,8 +288,10 @@ void realtime_test::test_RealtimeFeedListModel() QVERIFY(model.item(0, TimelineListModel::RecordTextPlainRole).toString() == "reply3"); qDebug().noquote() << "---------------------------"; - model.setAccount(m_service + "/realtime/2", "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", QString(), - QString(), "dummy", QString()); + uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/realtime/2", "id", "pass", "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", + "handle", "email", "accessJwt", "refreshJwt", true); + model.setAccount(uuid); json_doc = loadJson(":/data/realtimemodel/recv_data_2.json"); QVERIFY(json_doc.isObject()); { @@ -310,8 +316,10 @@ void realtime_test::test_RealtimeFeedListModel() QVERIFY(model.item(1, TimelineListModel::IsRepostedByRole).toBool() == false); qDebug().noquote() << "---------------------------"; - model.setAccount(m_service + "/realtime/3", "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", QString(), - QString(), "dummy", QString()); + uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service + "/realtime/3", "id", "pass", "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", + "handle", "email", "accessJwt", "refreshJwt", true); + model.setAccount(uuid); json_doc = loadJson(":/data/realtimemodel/recv_data_3.json"); recv->testReceived(json_doc.object()); QVERIFY2(model.rowCount() == 2, QString::number(model.rowCount()).toLocal8Bit()); diff --git a/tests/search_test/tst_search_test.cpp b/tests/search_test/tst_search_test.cpp index b3442919..2fc7349e 100644 --- a/tests/search_test/tst_search_test.cpp +++ b/tests/search_test/tst_search_test.cpp @@ -3,9 +3,9 @@ #include #include "webserver.h" - #include "timeline/searchpostlistmodel.h" #include "profile/searchprofilelistmodel.h" +#include "tools/accountmanager.h" class search_test : public QObject { @@ -30,7 +30,7 @@ private slots: search_test::search_test() { QCoreApplication::setOrganizationName(QStringLiteral("relog")); - QCoreApplication::setApplicationName(QStringLiteral("Hagoromo")); + QCoreApplication::setApplicationName(QStringLiteral("Hagoromo_unittest")); m_listenPort = m_mockServer.listen(QHostAddress::LocalHost, 0); m_service = QString("http://localhost:%1/response").arg(m_listenPort); @@ -47,8 +47,12 @@ void search_test::cleanupTestCase() { } void search_test::test_SearchPostListModel() { + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service, "id", "pass", "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", "handle", + "email", "accessJwt", "refreshJwt", true); + SearchPostListModel model; - model.setAccount(m_service, QString(), QString(), QString(), "dummy", QString()); + model.setAccount(uuid); model.setText("epub"); QSignalSpy spy(&model, SIGNAL(runningChanged())); @@ -61,8 +65,12 @@ void search_test::test_SearchPostListModel() void search_test::test_SearchProfileListModel() { + QString uuid = AccountManager::getInstance()->updateAccount( + QString(), m_service, "id", "pass", "did:plc:mqxsuw5b5rhpwo4lw6iwlid5", "handle", + "email", "accessJwt", "refreshJwt", true); + SearchProfileListModel model; - model.setAccount(m_service, QString(), QString(), QString(), "dummy", QString()); + model.setAccount(uuid); model.setText("epub"); QSignalSpy spy(&model, SIGNAL(runningChanged())); diff --git a/tests/tests.pro b/tests/tests.pro index f2d16827..cc215ebb 100644 --- a/tests/tests.pro +++ b/tests/tests.pro @@ -7,6 +7,7 @@ SUBDIRS += \ hagoromo_test2 \ http_test \ log_test \ + oauth_test \ realtime_test \ search_test \ tools_test diff --git a/tests/tools_test/tst_tools_test.cpp b/tests/tools_test/tst_tools_test.cpp index 16cabacd..8d34b067 100644 --- a/tests/tools_test/tst_tools_test.cpp +++ b/tests/tools_test/tst_tools_test.cpp @@ -7,6 +7,7 @@ #include "tools/base32.h" #include "tools/leb128.h" #include "tools/cardecoder.h" +#include "tools/tid.h" class tools_test : public QObject { @@ -21,11 +22,17 @@ private slots: void cleanupTestCase(); void test_base32(); + void test_base32_s(); + void test_tid(); void test_Leb128(); void test_CarDecoder(); }; -tools_test::tools_test() { } +tools_test::tools_test() +{ + QCoreApplication::setOrganizationName(QStringLiteral("relog")); + QCoreApplication::setApplicationName(QStringLiteral("Hagoromo_unittest")); +} tools_test::~tools_test() { } @@ -79,6 +86,24 @@ void tools_test::test_base32() } } +void tools_test::test_base32_s() +{ + QVERIFY2(Base32::encode_s(0) == "2222222222222", Base32::encode_s(0).toLocal8Bit()); +} + +void tools_test::test_tid() +{ + QString prev = ""; + QString current; + + for (int i = 0; i < 1000; i++) { + current = Tid::next(); + QVERIFY2(prev < current, + QString("prev, current = %1, %2").arg(prev, current).toLocal8Bit()); + prev = current; + } +} + void tools_test::test_Leb128() { quint8 buf[] = { 0x00, 0x00, 0x00, 0x00, 0x00 }; diff --git a/web/content/docs/release-note.en.md b/web/content/docs/release-note.en.md index fcf8d593..76d902d9 100644 --- a/web/content/docs/release-note.en.md +++ b/web/content/docs/release-note.en.md @@ -8,6 +8,16 @@ description: This is a multi-column Bluesky client. ## 2024 +### v0.40.0 - 2024/11/1 + +- Add + - Add a link to user's profile if user is registered with Linkat + - Support for accepting attached images by drag and drop +- Update + - Change the format of the embedded via in the post + - Change the internal format of feed storage to V2 + - Keeping up with jetstream changes + ### v0.39.0 - 2024/10/12 - Add diff --git a/web/content/docs/release-note.ja.md b/web/content/docs/release-note.ja.md index 378f76a9..9c0bbb65 100644 --- a/web/content/docs/release-note.ja.md +++ b/web/content/docs/release-note.ja.md @@ -8,6 +8,16 @@ description: マルチカラム対応Blueskyクライアント ## 2024 +### v0.40.0 - 2024/11/1 + +- 追加 + - Linkatに登録があるときリンクをプロフィールに追加 + - 添付画像をドラッグアンドドロップでの受付に対応 +- 変更 + - ポストに埋め込むviaの形式を変更 + - フィードの保存の内部形式をV2に変更 + - jetstreamの変更に追従する + ### v0.39.0 - 2024/10/12 - 追加 diff --git a/web/layouts/shortcodes/download_link.html b/web/layouts/shortcodes/download_link.html index 61421fac..b923c363 100644 --- a/web/layouts/shortcodes/download_link.html +++ b/web/layouts/shortcodes/download_link.html @@ -1,9 +1,9 @@