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 aca9dc3..8a15191 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/help/help_main.html b/src/help/help_main.html index 2e7f0b3..7e55783 100644 --- a/src/help/help_main.html +++ b/src/help/help_main.html @@ -1757,7 +1757,7 @@

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

Mistdonについて

-
Current Version: 1.6.1 (2025/01/11)
+
Current Version: 1.6.2a (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)