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 @@
アカウント一覧