From 1abcbc6735664770115930f5058b8f7257f476bc Mon Sep 17 00:00:00 2001 From: tizerm Date: Sun, 20 Oct 2024 19:32:39 +0900 Subject: [PATCH 1/9] =?UTF-8?q?Bluesky=E5=AF=BE=E5=BF=9C=E3=81=AE=E5=9F=BA?= =?UTF-8?q?=E5=B9=B9=E9=83=A8=E5=88=86=E3=81=8C=E3=81=A7=E3=81=8D=E3=81=BE?= =?UTF-8?q?=E3=81=97=E3=81=9F.=20SessionRefresh=E3=81=82=E3=81=9F=E3=82=8A?= =?UTF-8?q?=E3=81=8C=E3=81=BE=E3=81=A0=E5=AE=9F=E8=A3=85=E3=81=AE=E8=80=83?= =?UTF-8?q?=E3=81=88=E4=B8=AD.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.js | 121 ++++++++++++++++++++++++++++++-- preload.js | 5 +- src/auth.html | 59 +++++++++++----- src/css/mist_auth.css | 66 ++++++++++------- src/js/mist_auth.js | 22 ++++++ src/js/module/class_account.js | 39 ++++++++-- src/js/module/class_emojis.js | 7 +- src/js/module/class_instance.js | 50 +++++++++++++ src/js/module/class_status.js | 45 ++++++++++++ src/js/module/class_timeline.js | 14 ++++ 10 files changed, 374 insertions(+), 54 deletions(-) diff --git a/main.js b/main.js index 37b2851..b114869 100644 --- a/main.js +++ b/main.js @@ -22,6 +22,7 @@ var pref_emojis = new Map() var cache_history = null var cache_draft = null var cache_emoji_history = null +var cache_bsky_session = null var oauth_session = null const is_windows = process.platform === 'win32' @@ -40,6 +41,15 @@ app.disableHardwareAcceleration() * @return アカウント認証情報(マップで返却) */ async function readPrefAccs() { + // Blueskyのセッション情報を先に読み込む + if (!cache_bsky_session) { + const content = readFile('app_prefs/bsky_session.json') + if (content) { + cache_bsky_session = jsonToMap(JSON.parse(content), elm => elm.handle) + console.log('@INF: read app_prefs/bsky_session.json.') + } else cache_bsky_session = new Map() + } + // 変数キャッシュがある場合はキャッシュを使用 if (pref_accounts) { console.log('@INF: use app_prefs/auth.json cache.') @@ -48,11 +58,16 @@ async function readPrefAccs() { const content = readFile('app_prefs/auth.json') if (!content) return null // ファイルが見つからなかったらnullを返却 - pref_accounts = jsonToMap(JSON.parse(content), (elm) => `@${elm.user_id}@${elm.domain}`) + pref_accounts = jsonToMap(JSON.parse(content), getAccountKey) console.log('@INF: read app_prefs/auth.json.') return pref_accounts } +function getAccountKey(json) { + if (json.platform == 'Bluesky') return `@${json.user_id}` + else return `@${json.user_id}@${json.domain}` +} + /** * #IPC * アカウント認証情報を設定ファイルに書き込む(Mastodon用) @@ -84,7 +99,7 @@ async function writePrefMstdAccs(event, json_data) { // キャッシュを更新 if (!pref_accounts) { // キャッシュがない場合はファイルを読み込んでキャッシュを生成 - pref_accounts = jsonToMap(JSON.parse(content), (elm) => `@${elm.user_id}@${elm.domain}`) + pref_accounts = jsonToMap(JSON.parse(content), getAccountKey) } else { pref_accounts.set(`@${json_data.user_id}@${json_data.domain}`, write_json) } @@ -126,12 +141,60 @@ async function writePrefMskyAccs(event, json_data) { // キャッシュを更新 if (!pref_accounts) { // キャッシュがない場合はファイルを読み込んでキャッシュを生成 - pref_accounts = jsonToMap(JSON.parse(content), (elm) => `@${elm.user_id}@${elm.domain}`) + pref_accounts = jsonToMap(JSON.parse(content), getAccountKey) } else { pref_accounts.set(`@${write_json.user_id}@${write_json.domain}`, write_json) } } +async function writePrefBskyAccs(event, json_data) { + // JSONを生成(あとでキャッシュに入れるので) + const write_json = { + 'domain': json_data.domain, + 'platform': 'Bluesky', + 'user_id': json_data.user_id, + 'username': json_data.username, + 'socket_url': null, + 'client_id': json_data.did, + 'client_secret': null, + 'access_token': null, + 'avatar_url': json_data.avatar_url, + 'post_maxlength': json_data.post_maxlength, + // アカウントカラーは初期値グレー + 'acc_color': '808080' + } + + // ユーザー情報をファイルに書き込み + const content = await writeFileArrayJson('app_prefs/auth.json', write_json) + + // ユーザー情報のキャッシュを更新 + if (!pref_accounts) { + // キャッシュがない場合はファイルを読み込んでキャッシュを生成 + pref_accounts = jsonToMap(JSON.parse(content), getAccountKey) + } else { + pref_accounts.set(`@${json_data.user_id}`, write_json) + } + + // セッション情報のJSONを生成 + const write_session = { + 'handle': json_data.user_id, + 'pds': json_data.domain, + 'refresh_token': json_data.refresh_token, + 'access_token': json_data.access_token + } + + // セッション情報をファイルに書き込み + const session = await writeFileArrayJson('app_prefs/bsky_session.json', write_session) + + // Blueskyのキャッシュを更新 + if (!cache_bsky_session) { + // キャッシュがない場合はファイルを読み込んでキャッシュを生成 + cache_bsky_session = jsonToMap(JSON.parse(session), elm => elm.handle) + } else { + cache_bsky_session.set(write_session.handle, write_session) + } +} + /** * #IPC * アカウント認証情報に色情報を書き込む. @@ -161,7 +224,7 @@ async function writePrefAccColor(event, json_data) { const content = await overwriteFile('app_prefs/auth.json', write_json) // キャッシュを更新 - pref_accounts = jsonToMap(JSON.parse(content), (elm) => `@${elm.user_id}@${elm.domain}`) + pref_accounts = jsonToMap(JSON.parse(content), getAccountKey) } /** @@ -294,7 +357,7 @@ async function authorizeMastodon(auth_code) { // キャッシュを更新 if (!pref_accounts) { // キャッシュがない場合はファイルを読み込んでキャッシュを生成 - pref_accounts = jsonToMap(JSON.parse(content), (elm) => `@${elm.user_id}@${elm.domain}`) + pref_accounts = jsonToMap(JSON.parse(content), getAccountKey) } else { pref_accounts.set(`@${write_json.user_id}@${write_json.domain}`, write_json) } @@ -341,7 +404,7 @@ async function authorizeMisskey(session) { // キャッシュを更新 if (!pref_accounts) { // キャッシュがない場合はファイルを読み込んでキャッシュを生成 - pref_accounts = jsonToMap(JSON.parse(content), (elm) => `@${elm.user_id}@${elm.domain}`) + pref_accounts = jsonToMap(JSON.parse(content), getAccountKey) } else { pref_accounts.set(`@${write_json.user_id}@${write_json.domain}`, write_json) } @@ -351,6 +414,31 @@ async function authorizeMisskey(session) { } } +async function refreshBlueskySession(event, handle) { + + console.log("$$$ call refreshBlueskySession") + console.log(handle) + const session = cache_bsky_session.get(handle) + console.log(session) + + try { + const session_info = await ajax({ // セッションが有効か確認 + method: "GET", + url: `https://${session.pds}/xrpc/com.atproto.server.getSession`, + headers: { 'Authorization': `Bearer ${session.access_token}` } + }) + + console.log("$$$ Print session info") + console.log(session_info) + + return session.access_token + } catch (err) { + console.log("$$$ Session error") + console.log(err) + } + +} + /** * #IPC * 保存してあるカラム設定情報を読み込む @@ -507,6 +595,24 @@ async function writePrefCols(event, json_data) { } socket_url = `wss://${host}/streaming` break + case 'Bluesky': // Bluesky + // タイムラインタイプによって設定値を変える + switch (tl.timeline_type) { + case 'home': // ホームタイムライン + rest_url = `https://${host}/xrpc/app.bsky.feed.getTimeline` + query_param = {} + socket_param = {} + break + case 'notification': // 通知 + rest_url = `https://${host}/xrpc/app.bsky.notification.listNotifications` + query_param = {} + socket_param = {} + break + default: + break + } + socket_url = null + break default: break } @@ -1106,6 +1212,7 @@ app.whenReady().then(() => { ipcMain.handle('read-emoji-history', readEmojiHistory) ipcMain.on('write-pref-mstd-accs', writePrefMstdAccs) ipcMain.on('write-pref-msky-accs', writePrefMskyAccs) + ipcMain.on('write-pref-bsky-accs', writePrefBskyAccs) ipcMain.on('write-pref-acc-color', writePrefAccColor) ipcMain.on('write-pref-cols', writePrefCols) ipcMain.on('write-general-pref', writeGeneralPref) @@ -1118,6 +1225,8 @@ app.whenReady().then(() => { ipcMain.on('open-external-browser', openExternalBrowser) ipcMain.on('notification', notification) + ipcMain.handle('refresh-bsky-session', refreshBlueskySession) + // ウィンドウ生成 createWindow() app.on('activate', () => { diff --git a/preload.js b/preload.js index 9c51110..0ed3f6f 100644 --- a/preload.js +++ b/preload.js @@ -10,6 +10,7 @@ contextBridge.exposeInMainWorld('accessApi', { readEmojiHistory: () => ipcRenderer.invoke('read-emoji-history'), writePrefMstdAccs: (json_data) => ipcRenderer.send('write-pref-mstd-accs', json_data), writePrefMskyAccs: (json_data) => ipcRenderer.send('write-pref-msky-accs', json_data), + writePrefBskyAccs: (json_data) => ipcRenderer.send('write-pref-bsky-accs', json_data), writePrefAccColor: (json_data) => ipcRenderer.send('write-pref-acc-color', json_data), writePrefCols: (json_data) => ipcRenderer.send('write-pref-cols', json_data), writeGeneralPref: (json_data) => ipcRenderer.send('write-general-pref', json_data), @@ -20,5 +21,7 @@ contextBridge.exposeInMainWorld('accessApi', { fetchVersion: () => ipcRenderer.invoke('fetch-version'), openOAuthSession: (json_data) => ipcRenderer.send('open-oauth', json_data), openExternalBrowser: (url) => ipcRenderer.send('open-external-browser', url), - notification: (arg) => ipcRenderer.send('notification', arg) + notification: (arg) => ipcRenderer.send('notification', arg), + + refreshBlueskySession: (handle) => ipcRenderer.invoke('refresh-bsky-session', handle) }) diff --git a/src/auth.html b/src/auth.html index a6a7dfd..ce02ff0 100644 --- a/src/auth.html +++ b/src/auth.html @@ -118,22 +118,49 @@

アカウント一覧

アカウントの追加

-
- - 追加したいアカウントのインスタンス(サーバー)のURLを入力してください。
- MastodonとMisskeyどちらのインスタンスでもOKです(自動判別します)。
-
https:///
- -
(URLを入力してください)
- - - - ※OAuth,MiAuth認証をオススメしますがエラーが出る場合は右の旧方式認証をお使いください。 - +
+
+ + 追加したいアカウントのインスタンス(サーバー)のURLを入力してください。
+ MastodonとMisskeyどちらのインスタンスでもOKです(自動判別します)。
+
https:///
+ +
(URLを入力してください)
+ + + + ※OAuth,MiAuth認証をオススメしますがエラーが出る場合は右の旧方式認証をお使いください。 + +
+
+ + Blueskyのアカウントを認証する場合は以下のフォームから認証してください。 + + + + + + + + + + + + + +
PDS
ハンドル
アプリパス
+ + + ※独自にPDSサーバーを立てていない場合はPDSはbsky.socialのままにしてください。 + +

Mastodonアカウントの追加

diff --git a/src/css/mist_auth.css b/src/css/mist_auth.css index dddbe42..be80516 100644 --- a/src/css/mist_auth.css +++ b/src/css/mist_auth.css @@ -179,17 +179,38 @@ input[type="text"] { font-family: Arial, "メイリオ", sans-selif; } margin: 6px 12px; padding: 4px 32px; } + > #pre_section { + position: absolute; + top: 50%; + transform: translate(0%, -50%); + right: 0px; + left: 0px; + > div { + background-color: var(--tl-color-bg); + border-radius: 16px; + margin: 18px; + padding: 24px 12px; + > button.icon_label_set { + border-radius: 18px; + border-style: none; + margin: 8px; + padding: 7px 24px; + padding-left: 48px; + font-size: 16px; + font-weight: bold; + > img { + height: 36px; + width: 36px; + } + } + > .bottom_auth_warn { + display: block; + color: var(--help-color-text-strong); + } + } + } } #select_platform { - position: absolute; - background-color: var(--tl-color-bg); - border-radius: 16px; - margin: 18px; - padding: 32px 12px; - top: 50%; - transform: translate(0%, -50%); - right: 0px; - left: 0px; > .instance_box { border-radius: 12px; background-color: var(--tl-color-quote-bg); @@ -217,23 +238,18 @@ input[type="text"] { font-family: Arial, "メイリオ", sans-selif; } vertical-align: text-bottom; } } - > button.icon_label_set { - border-radius: 18px; - border-style: none; - margin: 8px; - padding: 7px 24px; - padding-left: 48px; - font-size: 16px; - font-weight: bold; - > img { - height: 36px; - width: 36px; - } - } - > .bottom_auth_warn { - display: block; - color: var(--help-color-text-strong); + } + #bluesky_login>table { + & td, & th { + background-color: var(--tl-color-quote-bg); + border-radius: 8px; } + & th { width: 140px; } + & input[type="text"] { width: calc(100% - 48px); } + } + #select_platform>.instance_box, #select_platform>.instance_info, #bluesky_login>table { + width: min(640px, calc(100% - 48px)); + margin: 8px auto; } .platform_section { position: absolute; diff --git a/src/js/mist_auth.js b/src/js/mist_auth.js index 0101cb4..bc728ff 100644 --- a/src/js/mist_auth.js +++ b/src/js/mist_auth.js @@ -145,6 +145,22 @@ } }) break + case 'Bluesky': // Bluesky + dialog({ // MisskeyはDOM上から消すだけ + type: 'confirm', + title: "アカウント認証情報削除", + text: ` + このアカウントの認証情報をMistdonから削除します。
+ よろしいですか?

+ ※使用していたアプリパスワードはBlueskyの設定で削除しておいてください。 + `, + // OKボタン押下時の処理 + accept: () => { + target_li.remove() + $("#on_save_account_info").click() + } + }) + break default: break } @@ -212,4 +228,10 @@ $("#select_platform").show("fade", 120) }) + $("#on_auth_bluesky").on("click", e => Instance.authorizeBluesky( + $("#__txt_bsky_pds").val(), + $("#__txt_bsky_handle").val(), + $("#__txt_bsky_pass").val() + )) + }) diff --git a/src/js/module/class_account.js b/src/js/module/class_account.js index add5c4b..154e33b 100644 --- a/src/js/module/class_account.js +++ b/src/js/module/class_account.js @@ -9,7 +9,13 @@ class Account { constructor(pref) { this.pref = pref this.index = pref.index - this.full_address = `@${pref.user_id}@${pref.domain}` + this.full_address = this.pref.platform == 'Bluesky' + ? `@${pref.user_id}` : `@${pref.user_id}@${pref.domain}` + + // Blueskyの場合は__access_jwtにトークンを一時保存 + if (this.pref.platform == 'Bluesky') (async () => this.__access_jwt = + await window.accessApi.refreshBlueskySession(pref.user_id))() + this.socket_prefs = [] this.socket = null this.reconnect = false @@ -181,8 +187,9 @@ class Account { // 背景アイコンとアカウントカラーを設定 if (this.platform == 'Misskey') $("#header>h1>.head_user") .css('background-image', 'url("resources/ic_misskey.png")') - else $("#header>h1>.head_user").css('background-image', - `url("resources/${this.is_skybridge ? 'ic_bluesky' : 'ic_mastodon'}.png")`) + else if (this.platform == 'Bluesky' || this.is_skybridge) $("#header>h1>.head_user") + .css('background-image', 'url("resources/ic_bluesky.png")') + else $("#header>h1>.head_user").css('background-image', `url("resources/ic_mastodon.png")`) $("#header>h1>.head_user>.username").html(this.replaceEmojis(this.pref.username)) $("#header>h1>.head_user>.channelname").empty() @@ -366,12 +373,34 @@ class Account { }) response = response.createdNote break + case 'Bluesky': // Bluesky(alpha) + request_param = { + "repo": this.pref.user_id, + "collection": 'app.bsky.feed.post', + "record": { + "text": arg.content, + "createdAt": new Date() + } + } + + response = await $.ajax({ // API呼び出し + type: "POST", + url: `https://${this.pref.domain}/xrpc/com.atproto.repo.createRecord`, + dataType: "json", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${this.__access_jwt}` + }, + data: JSON.stringify(request_param) + }) + + console.log(response) default: break } - // 投稿を履歴にスタックする - new Status(response, History.HISTORY_PREF_TIMELINE, this).pushStack(arg.content) + if (this.pref.platform != 'Bluesky') // 投稿を履歴にスタックする + new Status(response, History.HISTORY_PREF_TIMELINE, this).pushStack(arg.content) // 投稿成功時(コールバック関数実行) arg.success() notification.done(`${this.full_address}から投稿しました.`) diff --git a/src/js/module/class_emojis.js b/src/js/module/class_emojis.js index 5ee8aa5..6b38961 100644 --- a/src/js/module/class_emojis.js +++ b/src/js/module/class_emojis.js @@ -8,7 +8,9 @@ class Emojis { // コンストラクタ: パラメータを使って初期化(ファイルとJSON両対応) constructor(arg) { this.host = arg.host ?? null - if (arg.cache_flg) { + // カスタム絵文字を使用しない場合はスルーオブジェクトを参照する + if (arg.thru) this.thru = true + else if (arg.cache_flg) { // キャッシュフラグがON: アカウントから参照するサーバーのカスタム絵文字(リストではなくMap) const emoji_map = new Map() const category_map = new Map() @@ -44,6 +46,7 @@ class Emojis { // スタティックブロック(カスタム絵文字キャッシュを取りに行く) static { + Emojis.THRU = new Emojis({ 'thru': true }) Emojis.readCache() } @@ -122,6 +125,8 @@ class Emojis { * @param text 置換対象のテキスト */ replace(text) { + if (this.thru) return text // スルーする場合はなにもしない + if (this.cache_flg) { // アプリケーションキャッシュの絵文字データの場合 if (!text) return "" return text.replace(new RegExp('(? { diff --git a/src/js/module/class_instance.js b/src/js/module/class_instance.js index 36f1169..0f3a864 100644 --- a/src/js/module/class_instance.js +++ b/src/js/module/class_instance.js @@ -438,6 +438,56 @@ class Instance { }) } + static async authorizeBluesky(pds, handle, app_pass) { + try { // アクセストークンの取得 + const token = await $.ajax({ + type: "POST", + url: `https://${pds}/xrpc/com.atproto.server.createSession`, + dataType: "json", + headers: { "Content-Type": "application/json" }, + data: JSON.stringify({ + 'identifier': handle, + 'password': app_pass + }) + }) + + const user_data = await $.ajax({ // ユーザーデータの取得 + type: "GET", + url: `https://${pds}/xrpc/app.bsky.actor.getProfile`, + dataType: "json", + headers: { "Authorization": `Bearer ${token.accessJwt}` }, + data: { "actor": token.did } + }) + + // responseが返ってきたらアクセストークンをメインプロセスに渡す + await window.accessApi.writePrefBskyAccs({ + 'domain': pds, + 'user_id': token.handle, + 'username': user_data.displayName, + 'avatar_url': user_data.avatar, + 'refresh_token': token.refreshJwt, + 'did': token.did, + 'access_token': token.accessJwt, + 'post_maxlength': 300 + }) + + dialog({ + type: 'alert', + title: "アカウント設定", + text: "アカウントの認証に成功しました!", + // OKボタンを押してから画面をリロード + accept: () => location.reload() + }) + } catch (err) { // 認証失敗時 + dialog({ + type: 'alert', + title: "アカウント設定", + text: "認証リクエスト実行中に問題が発生しました。" + }) + return Promise.reject(err) + } + } + // Getter: インスタンスヘッダのDOMを返却 get header_element() { let target_emojis = null diff --git a/src/js/module/class_status.js b/src/js/module/class_status.js index bff74b2..e8af742 100644 --- a/src/js/module/class_status.js +++ b/src/js/module/class_status.js @@ -264,6 +264,51 @@ class Status { this.count_fav = reactions.reduce((sum, react) => sum + Number(react.count), 0) } this.reaction_self = data.myReaction + break + case 'Bluesky': // Bluesky + this.notif_type = this.type == 'notification' ? json.reason : null + + // リポスト判定とデータの参照 + this.reblog = !!json.reason + this.reblog_by = this.reblog ? json.reason.by.handle : null + this.reblog_by_icon = this.reblog ? json.reason.by.avatar : null + data = json.post + + original_date = this.reblog ? json.reason.indexedAt : data.indexedAt + this.reblog_origin_time = this.reblog ? new RelativeTime(new Date(data.indexedAt)) : null + this.uri = data.uri // 投稿URL(前はリプライ時のURL) + this.id = data.cid // 投稿ID + + // ユーザーに関するデータ + this.user = { + username: data.author.displayName, + id: data.author.handle, + full_address: data.author.handle, + avatar_url: data.author.avatar, + profile: null, + emojis: Emojis.THRU + } + // 投稿コンテンツに関するデータ + this.visibility = "public" + this.allow_reblog = true + this.reply_to = json.reply?.parent?.cid + + this.content = data.record.text + this.content_length = this.content.length + this.emojis = Emojis.THRU + + // 添付メディア + //this.sensitive = data.sensitive // 閲覧注意設定 + this.medias = [] + data.embed?.images?.forEach(media => this.medias.push({ + id: null, + type: data.embed?.$type, + url: media.fullsize, + thumbnail: media.thumb, + sensitive: false, + aspect: media.aspectRatio?.width / media.aspectRatio?.height ?? 1 + })) + break default: break diff --git a/src/js/module/class_timeline.js b/src/js/module/class_timeline.js index f2d8c58..8931b41 100644 --- a/src/js/module/class_timeline.js +++ b/src/js/module/class_timeline.js @@ -83,6 +83,20 @@ class Timeline { data: JSON.stringify(query_param) }) break + case 'Bluesky': // Bluesky + // REST APIで最新TLを30件取得、する処理をプロミスとして格納 + response = await $.ajax({ + type: "GET", + url: this.pref.rest_url, + dataType: "json", + headers: { "Authorization": `Bearer ${this.target_account.__access_jwt}` }, + data: query_param + }) + + console.log(response) + // Feedの配列をステータスに渡す + response = response.feed ?? response.notifications + break default: break } From 66ee8706e39afdf5cc2364d653bd23844e2acf94 Mon Sep 17 00:00:00 2001 From: tizerm Date: Sun, 20 Oct 2024 23:26:07 +0900 Subject: [PATCH 2/9] =?UTF-8?q?=E3=82=BB=E3=83=83=E3=82=B7=E3=83=A7?= =?UTF-8?q?=E3=83=B3=E5=86=8D=E7=94=9F=E6=88=90=E5=87=A6=E7=90=86=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0.=20=E3=81=9F=E3=81=A0=E3=83=95=E3=82=A1?= =?UTF-8?q?=E3=82=A4=E3=83=AB=E6=A7=8B=E9=80=A0=E3=81=AE=E8=A6=81=E8=A6=8B?= =?UTF-8?q?=E7=9B=B4=E3=81=97.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.js | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/main.js b/main.js index b114869..24fd94a 100644 --- a/main.js +++ b/main.js @@ -415,11 +415,7 @@ async function authorizeMisskey(session) { } async function refreshBlueskySession(event, handle) { - - console.log("$$$ call refreshBlueskySession") - console.log(handle) const session = cache_bsky_session.get(handle) - console.log(session) try { const session_info = await ajax({ // セッションが有効か確認 @@ -428,15 +424,38 @@ async function refreshBlueskySession(event, handle) { headers: { 'Authorization': `Bearer ${session.access_token}` } }) - console.log("$$$ Print session info") - console.log(session_info) - return session.access_token } catch (err) { - console.log("$$$ Session error") console.log(err) + // Bad Request以外はトークンの取得に失敗 + if (err.message != '400') return null } + try { + const session_info = await ajax({ // セッションを再取得する + method: "GET", + url: `https://${session.pds}/xrpc/com.atproto.server.refreshSession`, + headers: { 'Authorization': `Bearer ${session.refresh_token}` } + }) + + // セッション情報のJSONを生成 + const write_session = { + 'handle': session.handle, + 'pds': session.handle, + 'refresh_token': session_info.refreshJwt, + 'access_token': session_info.accessJwt + } + + // セッション情報をファイルに書き込み + const writer = await writeFileArrayJson('app_prefs/bsky_session.json', write_session) + + cache_bsky_session.set(write_session.handle, write_session) + + return session_info.accessJwt + } catch (err) { + console.log(err) + } + return null } /** @@ -1033,7 +1052,7 @@ async function ajax(arg) { response = await fetch(url, param) // ステータスコードがエラーの場合はエラーを投げる - if (!response.ok) throw new Error(`HTTP Status: ${response.status}`) + if (!response.ok) throw new Error(response.status) // responseをjsonとheaderとHTTP Statusに分けて返却 return { From b1665a7ce541121ad0a1f47c1c50175918cf9b7a Mon Sep 17 00:00:00 2001 From: tizerm Date: Mon, 11 Nov 2024 00:55:30 +0900 Subject: [PATCH 3/9] =?UTF-8?q?refreshSession=E3=81=BE=E3=82=8F=E3=82=8A?= =?UTF-8?q?=E3=81=AE=E5=AE=9F=E8=A3=85=E3=82=92=E6=A9=9F=E8=83=BD=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E4=BF=AE=E6=AD=A3.=20?= =?UTF-8?q?=E9=80=9A=E7=9F=A5=E3=81=8C=E8=A1=A8=E7=A4=BA=E3=81=95=E3=82=8C?= =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E6=9A=AB=E5=AE=9A=E8=A8=AD?= =?UTF-8?q?=E5=AE=9A.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.js | 49 +++++++++++------------ src/js/module/class_account.js | 5 ++- src/js/module/class_status.js | 70 +++++++++++++++++++-------------- src/js/module/class_timeline.js | 4 +- 4 files changed, 70 insertions(+), 58 deletions(-) diff --git a/main.js b/main.js index 29a8c72..a660ddd 100644 --- a/main.js +++ b/main.js @@ -22,7 +22,7 @@ var pref_temptl_fav = null var cache_history = null var cache_draft = null var cache_emoji_history = null -var cache_bsky_session = null +var cache_bsky_session = new Map() var oauth_session = null const is_windows = process.platform === 'win32' @@ -42,12 +42,14 @@ app.disableHardwareAcceleration() */ async function readPrefAccs() { // Blueskyのセッション情報を先に読み込む - if (!cache_bsky_session) { - const content = readFile('app_prefs/bsky_session.json') - if (content) { - cache_bsky_session = jsonToMap(JSON.parse(content), elm => elm.handle) - console.log('@INF: read app_prefs/bsky_session.json.') - } else cache_bsky_session = new Map() + if (cache_bsky_session.size == 0) { + const content_map = readDirFile('app_prefs/bsky_sessions') + + if (content_map.size > 0) // ハッシュキーから拡張子を抜いてドメイン名にする + content_map.forEach((v, k) => cache_bsky_session.set(k.substring(0, k.lastIndexOf('.')), JSON.parse(v))) + else cache_bsky_session.set('0', {}) // 空のキャッシュエントリを作成(ロードしないため) + + console.log('@INF: read app_prefs/emojis/.') } // 変数キャッシュがある場合はキャッシュを使用 @@ -158,8 +160,7 @@ async function writePrefBskyAccs(event, json_data) { 'access_token': null, 'avatar_url': json_data.avatar_url, 'post_maxlength': json_data.post_maxlength, - // アカウントカラーは初期値グレー - 'acc_color': '808080' + 'acc_color': getRandomColor() } // ユーザー情報をファイルに書き込み @@ -182,15 +183,10 @@ async function writePrefBskyAccs(event, json_data) { } // セッション情報をファイルに書き込み - const session = await writeFileArrayJson('app_prefs/bsky_session.json', write_session) + const session = await overwriteFile(`app_prefs/bsky_sessions/${json_data.user_id}.json`, write_session) - // Blueskyのキャッシュを更新 - if (!cache_bsky_session) { - // キャッシュがない場合はファイルを読み込んでキャッシュを生成 - cache_bsky_session = jsonToMap(JSON.parse(session), elm => elm.handle) - } else { - cache_bsky_session.set(write_session.handle, write_session) - } + // セッションキャッシュを更新 + cache_bsky_session.set(json_data.user_id, write_session) } /** @@ -420,11 +416,13 @@ async function refreshBlueskySession(event, handle) { headers: { 'Authorization': `Bearer ${session.access_token}` } }) + // 有効なセッションの場合はキャッシュされたアクセストークンを返す return session.access_token } catch (err) { - console.log(err) - // Bad Request以外はトークンの取得に失敗 - if (err.message != '400') return null + if (err.message != '400') { // Bad Request以外はトークンの取得に失敗 + console.log(err) + return null + } } try { @@ -437,14 +435,13 @@ async function refreshBlueskySession(event, handle) { // セッション情報のJSONを生成 const write_session = { 'handle': session.handle, - 'pds': session.handle, + 'pds': session.pds, 'refresh_token': session_info.refreshJwt, 'access_token': session_info.accessJwt } // セッション情報をファイルに書き込み - const writer = await writeFileArrayJson('app_prefs/bsky_session.json', write_session) - + const writer = await overwriteFile(`app_prefs/bsky_sessions/${write_session.handle}.json`, write_session) cache_bsky_session.set(write_session.handle, write_session) return session_info.accessJwt @@ -680,14 +677,14 @@ function getAPIParams(event, arg) { break case 'Bluesky': // Bluesky // タイムラインタイプによって設定値を変える - switch (tl.timeline_type) { + switch (arg.timeline.timeline_type) { case 'home': // ホームタイムライン - rest_url = `https://${host}/xrpc/app.bsky.feed.getTimeline` + rest_url = `https://${arg.host}/xrpc/app.bsky.feed.getTimeline` query_param = {} socket_param = {} break case 'notification': // 通知 - rest_url = `https://${host}/xrpc/app.bsky.notification.listNotifications` + rest_url = `https://${arg.host}/xrpc/app.bsky.notification.listNotifications` query_param = {} socket_param = {} break diff --git a/src/js/module/class_account.js b/src/js/module/class_account.js index 154e33b..6de16c8 100644 --- a/src/js/module/class_account.js +++ b/src/js/module/class_account.js @@ -374,6 +374,8 @@ class Account { response = response.createdNote break case 'Bluesky': // Bluesky(alpha) + // アクセストークンのセッション取得 + const jwt = await window.accessApi.refreshBlueskySession(this.pref.user_id) request_param = { "repo": this.pref.user_id, "collection": 'app.bsky.feed.post', @@ -389,12 +391,13 @@ class Account { dataType: "json", headers: { "Content-Type": "application/json", - "Authorization": `Bearer ${this.__access_jwt}` + "Authorization": `Bearer ${jwt}` }, data: JSON.stringify(request_param) }) console.log(response) + break default: break } diff --git a/src/js/module/class_status.js b/src/js/module/class_status.js index 1df2088..e0900e1 100644 --- a/src/js/module/class_status.js +++ b/src/js/module/class_status.js @@ -268,17 +268,46 @@ class Status { break case 'Bluesky': // Bluesky this.notif_type = this.type == 'notification' ? json.reason : null + this.medias = [] - // リポスト判定とデータの参照 - this.reblog = !!json.reason - this.reblog_by = this.reblog ? json.reason.by.handle : null - this.reblog_by_icon = this.reblog ? json.reason.by.avatar : null - data = json.post + if (json.post) { // 投稿データ + // リポスト判定とデータの参照 + this.reblog = !!json.reason + this.reblog_by = this.reblog ? json.reason.by.handle : null + this.reblog_by_icon = this.reblog ? json.reason.by.avatar : null + data = json.post + + original_date = this.reblog ? json.reason.indexedAt : data.indexedAt + this.reblog_origin_time = this.reblog ? new RelativeTime(new Date(data.indexedAt)) : null + this.uri = data.uri // 投稿URL(前はリプライ時のURL) + this.id = data.cid // 投稿ID + + // 投稿コンテンツに関するデータ + this.visibility = "public" + this.allow_reblog = true + this.reply_to = json.reply?.parent?.cid + + this.content = data.record.text + this.content_length = this.content.length + + // 添付メディア + //this.sensitive = data.sensitive // 閲覧注意設定 + data.embed?.images?.forEach(media => this.medias.push({ + id: null, + type: data.embed?.$type, + url: media.fullsize, + thumbnail: media.thumb, + sensitive: false, + aspect: media.aspectRatio?.width / media.aspectRatio?.height ?? 1 + })) + } else { // 通知データ + data = json + original_date = data.indexedAt + this.id = data.cid // 投稿ID + this.notif_id = json.record?.subject?.cid // 通知のID - original_date = this.reblog ? json.reason.indexedAt : data.indexedAt - this.reblog_origin_time = this.reblog ? new RelativeTime(new Date(data.indexedAt)) : null - this.uri = data.uri // 投稿URL(前はリプライ時のURL) - this.id = data.cid // 投稿ID + this.content = json.record?.subject?.uri + } // ユーザーに関するデータ this.user = { @@ -286,29 +315,10 @@ class Status { id: data.author.handle, full_address: data.author.handle, avatar_url: data.author.avatar, - profile: null, + profile: data.author.description, emojis: Emojis.THRU } - // 投稿コンテンツに関するデータ - this.visibility = "public" - this.allow_reblog = true - this.reply_to = json.reply?.parent?.cid - - this.content = data.record.text - this.content_length = this.content.length - this.emojis = Emojis.THRU - - // 添付メディア - //this.sensitive = data.sensitive // 閲覧注意設定 - this.medias = [] - data.embed?.images?.forEach(media => this.medias.push({ - id: null, - type: data.embed?.$type, - url: media.fullsize, - thumbnail: media.thumb, - sensitive: false, - aspect: media.aspectRatio?.width / media.aspectRatio?.height ?? 1 - })) + this.emojis = Emojis.THRU // カスタム絵文字はないのでスルー break default: diff --git a/src/js/module/class_timeline.js b/src/js/module/class_timeline.js index fae1a1a..4cbd76c 100644 --- a/src/js/module/class_timeline.js +++ b/src/js/module/class_timeline.js @@ -88,12 +88,14 @@ class Timeline { }) break case 'Bluesky': // Bluesky + // アクセストークンのセッション取得 + const jwt = await window.accessApi.refreshBlueskySession(this.target_account.pref.user_id) // REST APIで最新TLを30件取得、する処理をプロミスとして格納 response = await $.ajax({ type: "GET", url: this.pref.rest_url, dataType: "json", - headers: { "Authorization": `Bearer ${this.target_account.__access_jwt}` }, + headers: { "Authorization": `Bearer ${jwt}` }, data: query_param }) From 074adc6482bd8d805ddb2a6f3dacb67302eaae9a Mon Sep 17 00:00:00 2001 From: tizerm Date: Sat, 4 Jan 2025 15:00:26 +0900 Subject: [PATCH 4/9] =?UTF-8?q?createSession=E3=81=AE=E3=82=BB=E3=83=83?= =?UTF-8?q?=E3=82=B7=E3=83=A7=E3=83=B3=E7=94=9F=E6=88=90=E5=87=A6=E7=90=86?= =?UTF-8?q?=E3=82=92=E3=82=A2=E3=83=97=E3=83=AA=E3=83=91=E3=82=B9=E3=83=AF?= =?UTF-8?q?=E3=83=BC=E3=83=89=E3=82=92=E4=BF=9D=E5=AD=98=E3=81=99=E3=82=8B?= =?UTF-8?q?=E6=96=B9=E5=BC=8F=E3=81=AB=E5=A4=89=E6=9B=B4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.js | 139 +++++++++++++++++++------------- package-lock.json | 12 ++- package.json | 1 + src/js/module/class_instance.js | 1 + 4 files changed, 96 insertions(+), 57 deletions(-) diff --git a/main.js b/main.js index 128a745..acf2290 100644 --- a/main.js +++ b/main.js @@ -11,6 +11,7 @@ const fs = require('fs') const http = require('http') const stateKeeper = require('electron-window-state') const FeedParser = require('feedparser') +const AsyncLock = require('async-lock') const fetch = require('node-fetch') // アプリ保持用設定データの管理 @@ -25,6 +26,8 @@ var cache_emoji_history = null var cache_bsky_session = new Map() var oauth_session = null +var lock = new AsyncLock() + const is_windows = process.platform === 'win32' const is_mac = process.platform === 'darwin' @@ -41,17 +44,6 @@ app.disableHardwareAcceleration() * @return アカウント認証情報(マップで返却) */ async function readPrefAccs() { - // Blueskyのセッション情報を先に読み込む - if (cache_bsky_session.size == 0) { - const content_map = readDirFile('app_prefs/bsky_sessions') - - if (content_map.size > 0) // ハッシュキーから拡張子を抜いてドメイン名にする - content_map.forEach((v, k) => cache_bsky_session.set(k.substring(0, k.lastIndexOf('.')), JSON.parse(v))) - else cache_bsky_session.set('0', {}) // 空のキャッシュエントリを作成(ロードしないため) - - console.log('@INF: read app_prefs/emojis/.') - } - // 変数キャッシュがある場合はキャッシュを使用 if (pref_accounts) { console.log('@INF: use app_prefs/auth.json cache.') @@ -156,7 +148,7 @@ async function writePrefBskyAccs(event, json_data) { 'username': json_data.username, 'socket_url': null, 'client_id': json_data.did, - 'client_secret': null, + 'client_secret': json_data.app_pass, 'access_token': null, 'avatar_url': json_data.avatar_url, 'post_maxlength': json_data.post_maxlength, @@ -182,9 +174,6 @@ async function writePrefBskyAccs(event, json_data) { 'access_token': json_data.access_token } - // セッション情報をファイルに書き込み - const session = await overwriteFile(`app_prefs/bsky_sessions/${json_data.user_id}.json`, write_session) - // セッションキャッシュを更新 cache_bsky_session.set(json_data.user_id, write_session) } @@ -407,48 +396,87 @@ async function authorizeMisskey(session) { } async function refreshBlueskySession(event, handle) { - const session = cache_bsky_session.get(handle) + console.log("#BSKY-SESSION: Exclusive Bluesky Session getted...") + return await lock.acquire('bluesky-session', async () => { // 同時実行しないよう排他 + const session = cache_bsky_session.get(handle) + + if (session) { // セッションキャッシュが残っている場合はセッションが生きてるかの確認から + try { + const session_info = await ajax({ // セッションが有効か確認 + method: "GET", + url: `https://${session.pds}/xrpc/com.atproto.server.getSession`, + headers: { 'Authorization': `Bearer ${session.access_token}` } + }) - try { - const session_info = await ajax({ // セッションが有効か確認 - method: "GET", - url: `https://${session.pds}/xrpc/com.atproto.server.getSession`, - headers: { 'Authorization': `Bearer ${session.access_token}` } - }) + // 有効なセッションの場合はキャッシュされたアクセストークンを返す + console.log("#BSKY-SESSION: Access token returned.") + return session.access_token + } catch (err) { + if (err.message != '400') { // Bad Request以外はトークンの取得に失敗 + console.log(err) + return null + } + } - // 有効なセッションの場合はキャッシュされたアクセストークンを返す - return session.access_token - } catch (err) { - if (err.message != '400') { // Bad Request以外はトークンの取得に失敗 - console.log(err) - return null - } - } + try { + const session_info = await ajax({ // セッションを更新する + method: "POST", + url: `https://${session.pds}/xrpc/com.atproto.server.refreshSession`, + headers: { 'Authorization': `Bearer ${session.refresh_token}` } + }) - try { - const session_info = await ajax({ // セッションを再取得する - method: "GET", - url: `https://${session.pds}/xrpc/com.atproto.server.refreshSession`, - headers: { 'Authorization': `Bearer ${session.refresh_token}` } - }) + // セッション情報のJSONを生成 + const write_session = { + 'handle': session.handle, + 'pds': session.pds, + 'refresh_token': session_info.body.refreshJwt, + 'access_token': session_info.body.accessJwt + } + + // セッション情報のキャッシュを更新 + cache_bsky_session.set(write_session.handle, write_session) - // セッション情報のJSONを生成 - const write_session = { - 'handle': session.handle, - 'pds': session.pds, - 'refresh_token': session_info.refreshJwt, - 'access_token': session_info.accessJwt + console.log("#BSKY-SESSION: Refresh token created.") + return session_info.body.accessJwt + } catch (err) { + if (err.message != '400') { // Bad Request以外はトークンの取得に失敗 + console.log(err) + return null + } + } } - // セッション情報をファイルに書き込み - const writer = await overwriteFile(`app_prefs/bsky_sessions/${write_session.handle}.json`, write_session) - cache_bsky_session.set(write_session.handle, write_session) + try { // セッションを再取得する + const account = pref_accounts.get(`@${handle}`) + const pds = account.domain + const session_info = await ajax({ + method: "POST", + url: `https://${pds}/xrpc/com.atproto.server.createSession`, + headers: { "Content-Type": "application/json" }, + data: JSON.stringify({ + 'identifier': handle, + 'password': account.client_secret + }) + }) - return session_info.accessJwt - } catch (err) { - console.log(err) - } - return null + // セッション情報のJSONを生成 + const write_session = { + 'handle': handle, + 'pds': pds, + 'refresh_token': session_info.body.refreshJwt, + 'access_token': session_info.body.accessJwt + } + + // セッション情報をアプリにキャッシュする + cache_bsky_session.set(write_session.handle, write_session) + + console.log("#BSKY-SESSION: Session Regenerated.") + return session_info.body.accessJwt + } catch (err) { + console.log(err) + } + return null + }) } /** @@ -1099,10 +1127,13 @@ async function ajax(arg) { if (arg.method == "GET") { // GETはパラメータをURLに埋め込む const query_param = Object.keys(arg.data).reduce((str, key) => `${str}&${key}=${arg.data[key]}`, '') url += `?${query_param.substring(1)}` - } else { // POSTはパラメータをURLSearchParamsにセットする - const post_params = new URLSearchParams() - Object.keys(arg.data).forEach(key => post_params.append(key, arg.data[key])) - param.body = post_params + } else { + if (arg.headers['Content-Type'] == 'application/json') param.body = arg.data + else { // POSTはパラメータをURLSearchParamsにセットする + const post_params = new URLSearchParams() + Object.keys(arg.data).forEach(key => post_params.append(key, arg.data[key])) + param.body = post_params + } } } diff --git a/package-lock.json b/package-lock.json index 61e195f..c079f3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,16 @@ { "name": "Mistdon", - "version": "0.9.9", + "version": "1.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "Mistdon", - "version": "0.9.9", - "license": "MIT", + "version": "1.6.0", + "license": "LGPL", "dependencies": { "@electron-forge/publisher-github": "^6.4.2", + "async-lock": "^1.4.1", "electron-squirrel-startup": "^1.0.0", "electron-window-state": "^5.0.3", "feedparser": "^2.2.10", @@ -1702,6 +1703,11 @@ "node": ">=8" } }, + "node_modules/async-lock": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==" + }, "node_modules/at-least-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", diff --git a/package.json b/package.json index 751c4da..28b1bba 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ }, "dependencies": { "@electron-forge/publisher-github": "^6.4.2", + "async-lock": "^1.4.1", "electron-squirrel-startup": "^1.0.0", "electron-window-state": "^5.0.3", "feedparser": "^2.2.10", diff --git a/src/js/module/class_instance.js b/src/js/module/class_instance.js index 0f3a864..aa7bcc6 100644 --- a/src/js/module/class_instance.js +++ b/src/js/module/class_instance.js @@ -464,6 +464,7 @@ class Instance { 'domain': pds, 'user_id': token.handle, 'username': user_data.displayName, + 'app_pass': app_pass, 'avatar_url': user_data.avatar, 'refresh_token': token.refreshJwt, 'did': token.did, From c0f67b9ceee84beeb5b4b83779f0ac63febbdf77 Mon Sep 17 00:00:00 2001 From: tizerm Date: Sun, 5 Jan 2025 13:02:41 +0900 Subject: [PATCH 5/9] =?UTF-8?q?Bluesky=E3=81=AE=E9=80=9A=E7=9F=A5=E3=81=AE?= =?UTF-8?q?=E6=8A=95=E7=A8=BF=E3=82=92=E9=9D=9E=E5=90=8C=E6=9C=9F=E3=81=A7?= =?UTF-8?q?=E5=8F=96=E5=BE=97=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/js/module/aclass_status_layout.js | 1 + src/js/module/class_notification_status.js | 68 +++++++++++++++++++++- src/js/module/class_status.js | 57 ++++++++++++------ src/js/module/class_timeline.js | 1 - 4 files changed, 105 insertions(+), 22 deletions(-) diff --git a/src/js/module/aclass_status_layout.js b/src/js/module/aclass_status_layout.js index 18a9796..a997533 100644 --- a/src/js/module/aclass_status_layout.js +++ b/src/js/module/aclass_status_layout.js @@ -594,6 +594,7 @@ let reaction_self = null switch (this.platform) { case 'Mastodon': // Mastodon + case 'Bluesky': // Bluesky // ふぁぼの表示だけする if (this.count_fav > 0) html += `${this.count_fav}` break diff --git a/src/js/module/class_notification_status.js b/src/js/module/class_notification_status.js index 96da820..c638491 100644 --- a/src/js/module/class_notification_status.js +++ b/src/js/module/class_notification_status.js @@ -16,6 +16,8 @@ class NotificationStatus extends Status { if (json.type == 'follow') super(json, timeline, account) else super(json.status, timeline, account) original_date = json.created_at + this.notification_id = json.id + this.notification_type = json.type // アクションを起こしたユーザーの情報 this.action_user = { @@ -41,6 +43,8 @@ class NotificationStatus extends Status { if (json.type == 'follow') super(json, timeline, account) else super(json.note, timeline, account) original_date = json.createdAt + this.notification_id = json.id + this.notification_type = json.type // アクションを起こしたユーザーの情報 this.action_user = { @@ -62,11 +66,38 @@ class NotificationStatus extends Status { this.reaction_summary.set(json.reaction, [this.action_user]) } break + case 'Bluesky': // Bluesky + super(json, timeline, account) + + original_date = json.indexedAt + this.notification_id = json.cid + this.notification_type = json.reason + this.id = json.record?.subject?.cid + this.uri = json.record?.subject?.uri + + // アクションを起こしたユーザーの情報 + this.action_user = { + username: json.author.displayName, + id: json.author.handle, + full_address: json.author.handle, + avatar_url: json.author.avatar, + profile: json.author.description, + emojis: Emojis.THRU + } + + // 通知元ユーザーの情報 + this.user = { + username: this.from_account?.pref.username, + id: this.from_account?.pref.user_id, + full_address: this.from_account?.full_address, + avatar_url: this.from_account?.pref.avatar_url, + profile: null, + emojis: Emojis.THRU + } + break default: break } - this.notification_id = json.id - this.notification_type = json.type this.sort_date = new Date(original_date) this.relative_time = new RelativeTime(this.sort_date) this.status_key = `${this.notification_id}@${this.id}@${this.user?.full_address}` @@ -76,10 +107,12 @@ class NotificationStatus extends Status { this.mergable = true switch (this.notification_type) { case 'favourite': // お気に入り + case 'like': // お気に入り case 'reaction': // 絵文字リアクション this.notification_key = `fav_${this.id}` break case 'reblog': // ブースト + case 'repost': // ブースト case 'renote': // リノート this.notification_key = `reb_${this.id}` break @@ -92,6 +125,9 @@ class NotificationStatus extends Status { this.mergable = false break } + + // 追加で取得する情報がある場合はHTTP Requestで取得 + this.fetchAdditionalNotifyInfoAsync() } // Getter: フォロー通知判定 @@ -135,6 +171,19 @@ class NotificationStatus extends Status { return [merge_at, merge_from] } + async fetchAdditionalNotifyInfoAsync() { + if (this.platform == 'Bluesky' && this.notification_type != 'follow') // 通知の投稿を取得 + this.__prm_notify_status = super.getPostBsky(this.uri).then(post => { // 内容をコピーする + this.content = post.content + this.content_length = post.content_length + this.sensitive = post.sensitive + this.medias = post.medias + this.count_reply = post.count_reply + this.count_reblog = post.count_reblog + this.count_fav = post.count_fav + }) + } + /** * #Method #Async * この通知をDOMに反映したあとにあとから非同期で取得して表示する情報を追加でバインドする. @@ -161,6 +210,9 @@ class NotificationStatus extends Status { }) observer.observe(target_li.get(0)) } + + if (this.platform == 'Bluesky') this.__prm_notify_status?.then(post => tgul.find(`li[id="${this.status_key}"]`) + .replaceWith(this.popout_flg ? this.element : this.timeline_element)) } // Getter: 投稿データからHTMLを生成して返却(ノーマルレイアウト) @@ -195,10 +247,12 @@ class NotificationStatus extends Status { switch (this.notification_type) { case 'favourite': // お気に入り case 'reaction': // 絵文字リアクション + case 'like': // お気に入り(Bluesky) jqelm.closest('li').addClass('favorited_post') break case 'reblog': // ブースト case 'renote': // リノート + case 'repost': // リポスト(Bluesky) jqelm.closest('li').addClass('rebloged_post') break case 'follow': // フォロー通知 @@ -243,10 +297,12 @@ class NotificationStatus extends Status { switch (this.notification_type) { case 'favourite': // お気に入り case 'reaction': // 絵文字リアクション + case 'like': // お気に入り(Bluesky) jqelm.closest('li').addClass('favorited_post') break case 'reblog': // ブースト case 'renote': // リノート + case 'repost': // リポスト(Bluesky) jqelm.closest('li').addClass('rebloged_post') break case 'follow': // フォロー通知 @@ -308,11 +364,13 @@ class NotificationStatus extends Status { switch (this.notification_type) { case 'favourite': // お気に入り + case 'like': // お気に入り(Bluesky) jqelm.closest('li').addClass('favorited_post') jqelm.find('.ic_notif_type').attr('src', 'resources/ic_favorite.png') break case 'reblog': // ブースト case 'renote': // リノート + case 'repost': // リポスト(Bluesky) jqelm.closest('li').addClass('rebloged_post') jqelm.find('.ic_notif_type').attr('src', 'resources/ic_reblog.png') break @@ -383,6 +441,12 @@ class NotificationStatus extends Status { case 'favourite': // お気に入り html += '
Favorited Users
' break + case 'like': // お気に入り(Bluesky) + html += '
Liked Users
' + break + case 'repost': // リポスト(Bluesky) + html += '
Reposted Users
' + break case 'reblog': // ブースト html += '
Boosted Users
' break diff --git a/src/js/module/class_status.js b/src/js/module/class_status.js index a4661bf..502238f 100644 --- a/src/js/module/class_status.js +++ b/src/js/module/class_status.js @@ -248,7 +248,7 @@ class Status extends StatusLayout { this.reaction_self = data.myReaction break case 'Bluesky': // Bluesky - this.notif_type = this.type == 'notification' ? json.reason : null + //this.notif_type = this.type == 'notification' ? json.reason : null this.medias = [] if (json.post) { // 投稿データ @@ -263,16 +263,26 @@ class Status extends StatusLayout { this.uri = data.uri // 投稿URL(前はリプライ時のURL) this.id = data.cid // 投稿ID + // ユーザーに関するデータ + this.user = { + username: data.author.displayName, + id: data.author.handle, + full_address: data.author.handle, + avatar_url: data.author.avatar, + profile: data.author.description, + emojis: Emojis.THRU + } + // 投稿コンテンツに関するデータ this.visibility = "public" this.allow_reblog = true this.reply_to = json.reply?.parent?.cid - this.content = data.record.text + this.content = data.record.text.replace(new RegExp('\n', 'g'), '
') // 改行文字をタグに置換 this.content_length = this.content.length // 添付メディア - //this.sensitive = data.sensitive // 閲覧注意設定 + this.sensitive = data.labels.length > 0 // 閲覧注意設定 data.embed?.images?.forEach(media => this.medias.push({ id: null, type: data.embed?.$type, @@ -281,24 +291,12 @@ class Status extends StatusLayout { sensitive: false, aspect: media.aspectRatio?.width / media.aspectRatio?.height ?? 1 })) - } else { // 通知データ - data = json - original_date = data.indexedAt - this.id = data.cid // 投稿ID - this.notif_id = json.record?.subject?.cid // 通知のID - this.content = json.record?.subject?.uri - } + this.count_reply = data.replyCount + this.count_reblog = data.repostCount + this.count_fav = data.likeCount + } else original_date = json.indexedAt - // ユーザーに関するデータ - this.user = { - username: data.author.displayName, - id: data.author.handle, - full_address: data.author.handle, - avatar_url: data.author.avatar, - profile: data.author.description, - emojis: Emojis.THRU - } this.emojis = Emojis.THRU // カスタム絵文字はないのでスルー break @@ -560,6 +558,27 @@ class Status extends StatusLayout { } } + async getPostBsky(uri) { + let response = null + try { + console.log(uri) + // アクセストークンのセッション取得 + const jwt = await window.accessApi.refreshBlueskySession(this.from_account.pref.user_id) + + response = await $.ajax({ + type: "GET", + url: `https://${this.from_account.pref.domain}/xrpc/app.bsky.feed.getPosts`, + dataType: "json", + headers: { "Authorization": `Bearer ${jwt}` }, + data: { "uris": [uri] } + }) + + return new Status({ 'post': response.posts[0] }, this.from_timeline, this.from_account) + } catch (err) { + Notification.error("投稿の取得に失敗しました.") + } + } + /** * #StaticMethod * IDから投稿データを対象サーバーを使って取得する diff --git a/src/js/module/class_timeline.js b/src/js/module/class_timeline.js index 9bd2e53..20c2f7a 100644 --- a/src/js/module/class_timeline.js +++ b/src/js/module/class_timeline.js @@ -102,7 +102,6 @@ class Timeline { data: query_param }) - console.log(response) // Feedの配列をステータスに渡す response = response.feed ?? response.notifications break From b5e0840bb48a06576b42071b0ce4418e33ce6ba6 Mon Sep 17 00:00:00 2001 From: tizerm Date: Sun, 5 Jan 2025 13:58:45 +0900 Subject: [PATCH 6/9] add meta info. --- src/help/help_main.html | 2 +- src/index.html | 2 +- src/js/module/class_status.js | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/help/help_main.html b/src/help/help_main.html index 5f3f78e..aec0aec 100644 --- a/src/help/help_main.html +++ b/src/help/help_main.html @@ -1750,7 +1750,7 @@

設定ファイルの場所について

Mistdonについて

-
Current Version: 1.6.0 (2025/01/01)
+
Current Version: 1.6.1a (Bluesky Edition)(2025/01/05)

Mistdonは、MastodonとMisskeyの統合Fediverseクライアントです。
OSSですが基本的にはぜるま(@tizerm@mofu.kemo.no)(@tizerm@misskey.dev)が主導になって開発を行っています(というかコントリビューターは今のところいない……)。
diff --git a/src/index.html b/src/index.html index 7329fb1..ce44723 100644 --- a/src/index.html +++ b/src/index.html @@ -50,7 +50,7 @@ - Mistdon - The Integrated Mastodon and Misskey Client + Mistdon - v1.6.1a (Bluesky Edition)