diff --git a/main.js b/main.js index 234539d..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') // アプリ保持用設定データの管理 @@ -22,8 +23,11 @@ var pref_temptl_fav = null var cache_history = null var cache_draft = null 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' @@ -48,11 +52,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用) @@ -83,7 +92,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) } @@ -124,12 +133,51 @@ 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': json_data.app_pass, + 'access_token': null, + 'avatar_url': json_data.avatar_url, + 'post_maxlength': json_data.post_maxlength, + 'acc_color': getRandomColor() + } + + // ユーザー情報をファイルに書き込み + 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 + } + + // セッションキャッシュを更新 + cache_bsky_session.set(json_data.user_id, write_session) +} + /** * #IPC * アカウント認証情報に色情報を書き込む. @@ -159,7 +207,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) } /** @@ -291,7 +339,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) } @@ -337,7 +385,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) } @@ -347,6 +395,90 @@ async function authorizeMisskey(session) { } } +async function refreshBlueskySession(event, 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}` } + }) + + // 有効なセッションの場合はキャッシュされたアクセストークンを返す + console.log("#BSKY-SESSION: Access token returned.") + 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}` } + }) + + // セッション情報の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) + + 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 + } + } + } + + 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 + }) + }) + + // セッション情報の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 + }) +} + /** * #IPC * 保存してあるカラム設定情報を読み込む @@ -576,6 +708,24 @@ function getAPIParams(event, arg) { } socket_url = `wss://${arg.host}/streaming` break + case 'Bluesky': // Bluesky + // タイムラインタイプによって設定値を変える + switch (arg.timeline.timeline_type) { + case 'home': // ホームタイムライン + rest_url = `https://${arg.host}/xrpc/app.bsky.feed.getTimeline` + query_param = {} + socket_param = {} + break + case 'notification': // 通知 + rest_url = `https://${arg.host}/xrpc/app.bsky.notification.listNotifications` + query_param = {} + socket_param = {} + break + default: + break + } + socket_url = null + break default: break } @@ -977,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 + } } } @@ -988,7 +1141,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 { @@ -1183,6 +1336,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) @@ -1197,6 +1351,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/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/preload.js b/preload.js index c738e5f..1f81a94 100644 --- a/preload.js +++ b/preload.js @@ -11,6 +11,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), @@ -23,5 +24,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 8eddb58..8eb9668 100644 --- a/src/auth.html +++ b/src/auth.html @@ -119,22 +119,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 5472a4b..1b5a411 100644 --- a/src/js/mist_auth.js +++ b/src/js/mist_auth.js @@ -149,6 +149,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 } @@ -216,4 +232,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/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_account.js b/src/js/module/class_account.js index d6584a0..92998a0 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 @@ -182,8 +188,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() @@ -367,12 +374,37 @@ 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', + "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 ${jwt}` + }, + data: JSON.stringify(request_param) + }) + + console.log(response) + break 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 8353fcc..34c355f 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..aa7bcc6 100644 --- a/src/js/module/class_instance.js +++ b/src/js/module/class_instance.js @@ -438,6 +438,57 @@ 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, + 'app_pass': app_pass, + '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_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 ababd10..502238f 100644 --- a/src/js/module/class_status.js +++ b/src/js/module/class_status.js @@ -246,6 +246,59 @@ class Status extends StatusLayout { this.count_fav = 0 } this.reaction_self = data.myReaction + break + case 'Bluesky': // Bluesky + //this.notif_type = this.type == 'notification' ? json.reason : null + this.medias = [] + + 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.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.replace(new RegExp('\n', 'g'), '
') // 改行文字をタグに置換 + this.content_length = this.content.length + + // 添付メディア + this.sensitive = data.labels.length > 0 // 閲覧注意設定 + 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.count_reply = data.replyCount + this.count_reblog = data.repostCount + this.count_fav = data.likeCount + } else original_date = json.indexedAt + + this.emojis = Emojis.THRU // カスタム絵文字はないのでスルー + break default: break @@ -505,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 a665006..20c2f7a 100644 --- a/src/js/module/class_timeline.js +++ b/src/js/module/class_timeline.js @@ -90,6 +90,21 @@ class Timeline { data: JSON.stringify(query_param) }) 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 ${jwt}` }, + data: query_param + }) + + // Feedの配列をステータスに渡す + response = response.feed ?? response.notifications + break default: break }