From 4c81b5d95e0eca2d4c7c2c471886a104a2cd6056 Mon Sep 17 00:00:00 2001 From: AkiraFukushima Date: Sat, 31 Dec 2022 22:45:14 +0900 Subject: [PATCH 01/23] refs #2500 Change account database to sqlite3 --- package.json | 1 + src/main/account.ts | 493 +++------ src/main/auth.ts | 119 --- src/main/database.ts | 54 +- src/main/index.ts | 953 +++++++++--------- src/main/server.ts | 29 + src/main/timelines.ts | 62 -- src/main/websocket.ts | 4 +- src/renderer/components/Authorize.vue | 191 ---- src/renderer/components/GlobalHeader.vue | 33 +- src/renderer/components/Login.vue | 14 +- src/renderer/components/Login/Authorize.vue | 156 +++ src/renderer/components/Login/LoginForm.vue | 36 +- src/renderer/components/TimelineSpace.vue | 3 - .../TimelineSpace/Contents/Bookmarks.vue | 5 +- .../TimelineSpace/Contents/Favourites.vue | 5 +- .../components/TimelineSpace/SideMenu.vue | 12 +- src/renderer/components/utils/reloadable.ts | 5 - src/renderer/router/index.ts | 7 - src/renderer/store/Authorize.ts | 37 - src/renderer/store/GlobalHeader.ts | 67 +- src/renderer/store/Login.ts | 60 +- src/renderer/store/Preferences/Account.ts | 13 +- src/renderer/store/Settings/Filters.ts | 12 +- src/renderer/store/Settings/Filters/Edit.ts | 12 +- src/renderer/store/Settings/Filters/New.ts | 6 +- src/renderer/store/Settings/General.ts | 18 +- src/renderer/store/TimelineSpace.ts | 239 +---- .../store/TimelineSpace/Contents/Bookmarks.ts | 16 +- .../TimelineSpace/Contents/DirectMessages.ts | 12 +- .../TimelineSpace/Contents/Favourites.ts | 16 +- .../TimelineSpace/Contents/FollowRequests.ts | 18 +- .../TimelineSpace/Contents/Hashtag/Tag.ts | 14 +- .../store/TimelineSpace/Contents/Home.ts | 36 +- .../TimelineSpace/Contents/Lists/Edit.ts | 12 +- .../TimelineSpace/Contents/Lists/Index.ts | 18 +- .../TimelineSpace/Contents/Lists/Show.ts | 14 +- .../store/TimelineSpace/Contents/Local.ts | 12 +- .../store/TimelineSpace/Contents/Mentions.ts | 12 +- .../TimelineSpace/Contents/Notifications.ts | 38 +- .../store/TimelineSpace/Contents/Public.ts | 12 +- .../TimelineSpace/Contents/Search/Account.ts | 6 +- .../TimelineSpace/Contents/Search/Tag.ts | 6 +- .../TimelineSpace/Contents/Search/Toots.ts | 6 +- .../Contents/SideBar/AccountProfile.ts | 77 +- .../SideBar/AccountProfile/Followers.ts | 18 +- .../SideBar/AccountProfile/Follows.ts | 18 +- .../SideBar/AccountProfile/Timeline/Media.ts | 12 +- .../SideBar/AccountProfile/Timeline/Posts.ts | 12 +- .../Timeline/PostsAndReplies.ts | 12 +- .../Contents/SideBar/TootDetail.ts | 6 +- .../store/TimelineSpace/HeaderMenu.ts | 6 +- .../TimelineSpace/Modals/AddListMember.ts | 12 +- .../store/TimelineSpace/Modals/Jump.ts | 4 +- .../TimelineSpace/Modals/ListMembership.ts | 18 +- .../store/TimelineSpace/Modals/MuteConfirm.ts | 6 +- .../store/TimelineSpace/Modals/NewToot.ts | 40 +- .../TimelineSpace/Modals/NewToot/Status.ts | 18 +- .../store/TimelineSpace/Modals/Report.ts | 6 +- src/renderer/store/TimelineSpace/SideMenu.ts | 86 +- src/renderer/store/index.ts | 5 +- src/renderer/store/organisms/Toot.ts | 74 +- src/types/insertAccountCache.ts | 2 +- src/types/localAccount.ts | 14 +- src/types/localMarker.ts | 2 +- src/types/localServer.ts | 7 + yarn.lock | 92 +- 67 files changed, 1488 insertions(+), 1953 deletions(-) delete mode 100644 src/main/auth.ts create mode 100644 src/main/server.ts delete mode 100644 src/main/timelines.ts delete mode 100644 src/renderer/components/Authorize.vue create mode 100644 src/renderer/components/Login/Authorize.vue delete mode 100644 src/renderer/store/Authorize.ts create mode 100644 src/types/localServer.ts diff --git a/package.json b/package.json index 3ae6b4cd..f0448268 100644 --- a/package.json +++ b/package.json @@ -108,6 +108,7 @@ "rc": "^1.2.7", "sanitize-html": "^2.8.1", "simplayer": "0.0.8", + "sqlite3": "^5.1.4", "system-font-families": "^0.6.0", "tunnel-agent": "^0.6.0", "unicode-emoji-json": "^0.4.0", diff --git a/src/main/account.ts b/src/main/account.ts index ef7d636d..bba9fd34 100644 --- a/src/main/account.ts +++ b/src/main/account.ts @@ -1,354 +1,163 @@ -import { isEmpty } from 'lodash' -import generator, { detector, Entity, ProxyConfig } from 'megalodon' -import Datastore from 'nedb' -import log from 'electron-log' +import sqlite3 from 'sqlite3' import { LocalAccount } from '~/src/types/localAccount' +import { LocalServer } from '~src/types/localServer' -export default class Account { - private db: Datastore +export const insertAccount = ( + db: sqlite3.Database, + username: string, + accountId: string, + avatar: string, + clientId: string, + clientSecret: string, + accessToken: string, + refreshToken: string | null, + server: LocalServer +): Promise => { + return new Promise((resolve, reject) => { + db.serialize(() => { + db.run('BEGIN TRANSACTION') - constructor(db: Datastore) { - this.db = db - } - - async initialize() { - await this.cleanup() - await this.reorder() - await this.updateUnique() - } - - updateUnique(): Promise<{}> { - return new Promise((resolve, reject) => { - // At first, remove old index. - this.db.removeIndex('order', err => { - if (err) reject(err) - // Add unique index. - this.db.ensureIndex({ fieldName: 'order', unique: true, sparse: true }, err => { - if (err) reject(err) - resolve({}) - }) - }) - }) - } - - /** - * Reorder accounts, because sometimes the order of accounts is duplicated. - */ - async reorder() { - const accounts = await this.listAllAccounts() - await Promise.all( - accounts.map(async (account, index) => { - const update = await this.updateAccount(account._id!, Object.assign(account, { order: index + 1 })) - return update - }) - ) - const ordered = await this.listAllAccounts() - return ordered - } - - /** - * Check order of all accounts, and fix if order is negative value or over the length. - */ - async cleanup() { - const accounts = await this.listAccounts() - if (accounts.length < 1) { - return accounts.length - } - if (accounts[0].order < 1 || accounts[accounts.length - 1].order > accounts.length) { - await Promise.all( - accounts.map(async (element, index) => { - const update = await this.updateAccount(element._id!, Object.assign(element, { order: index + 1 })) - return update - }) - ) - } - return null - } - - insertAccount(localAccount: LocalAccount): Promise { - return new Promise((resolve, reject) => { - this.db.insert(localAccount, (err, doc) => { - if (err) return reject(err) - if (isEmpty(doc)) return reject(new EmptyRecordError('empty')) - resolve(doc) - }) - }) - } - - /** - * List up all accounts either authenticated or not authenticated. - */ - listAllAccounts(order = 1): Promise> { - return new Promise((resolve, reject) => { - this.db - .find({}) - .sort({ order: order }) - .exec((err, docs) => { - if (err) return reject(err) - if (isEmpty(docs)) return reject(new EmptyRecordError('empty')) - resolve(docs) - }) - }) - } - - /** - * List up authenticated accounts. - */ - listAccounts(): Promise> { - return new Promise((resolve, reject) => { - this.db - .find({ $and: [{ accessToken: { $ne: '' } }, { accessToken: { $ne: null } }] }) - .sort({ order: 1 }) - .exec((err, docs) => { - if (err) return reject(err) - if (isEmpty(docs)) return reject(new EmptyRecordError('empty')) - resolve(docs) - }) - }) - } - - // Get the last account. - async lastAccount(): Promise { - const accounts = await this.listAllAccounts(-1) - return accounts[0] - } - - getAccount(id: string): Promise { - return new Promise((resolve, reject) => { - this.db.findOne( - { - _id: id - }, - (err, doc) => { - if (err) return reject(err) - if (isEmpty(doc)) return reject(new EmptyRecordError('empty')) - resolve(doc) + db.get('SELECT * FROM accounts ORDER BY sort DESC', (err, row) => { + if (err) { + reject(err) } - ) - }) - } - - searchAccount(obj: any): Promise { - return new Promise((resolve, reject) => { - this.db.findOne(obj, (err, doc) => { - if (err) return reject(err) - if (isEmpty(doc)) return reject(new EmptyRecordError('empty')) - resolve(doc) - }) - }) - } - - searchAccounts(obj: any, order = 1): Promise> { - return new Promise((resolve, reject) => { - this.db - .find(obj) - .sort({ order: order }) - .exec((err, docs) => { - if (err) return reject(err) - resolve(docs) - }) - }) - } - - updateAccount(id: string, obj: any): Promise { - return new Promise((resolve, reject) => { - this.db.update( - { - _id: id - }, - { $set: Object.assign(obj, { _id: id }) }, - { multi: true }, - (err, _numReplaced) => { - if (err) return reject(err) - this.db.findOne( - { - _id: id - }, - (err, doc) => { - if (err) return reject(err) - if (isEmpty(doc)) return reject(new EmptyRecordError('empty')) - resolve(doc) + let order = 1 + if (row) { + order = row.order + } + db.run( + 'INSERT INTO accounts(username, account_id, avatar, client_id, client_secret, access_token, refresh_token, sort) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', + [username, accountId, avatar, clientId, clientSecret, accessToken, refreshToken, order], + function (err) { + if (err) { + reject(err) } - ) - } - ) - }) - } + const id = this.lastID - removeAccount(id: string): Promise { - return new Promise((resolve, reject) => { - this.db.remove( - { - _id: id - }, - { multi: true }, - (err, _numRemoved) => { - if (err) return reject(err) - resolve(id) - } - ) - }) - } + db.run('UPDATE servers SET account_id = ? WHERE id = ?', [id, server.id], err => { + if (err) { + reject(err) + } - removeAll(): Promise { - return new Promise((resolve, reject) => { - this.db.remove({}, { multi: true }, (err, numRemoved) => { - if (err) return reject(err) - resolve(numRemoved) + db.run('COMMIT') + + resolve({ + id, + username, + accountId, + avatar, + clientId, + clientSecret, + accessToken, + refreshToken, + order + }) + }) + } + ) }) }) - } - - async forwardAccount(ac: LocalAccount): Promise { - // Find account which is the previous of the target account. - const accounts = await this.searchAccounts({ order: { $lt: ac.order } }, -1).catch(err => { - console.log(err) - return [] - }) - if (accounts.length < 1) { - return Promise.resolve({}) - } - const previousAccount = accounts[0] - const targetOrder = ac.order - const previousOrder = previousAccount.order - - // At first, we need to update the previous account with dummy order. - // Because this column is uniqued, so can not update with same order. - await this.updateAccount( - previousAccount._id!, - Object.assign(previousAccount, { - order: -1 - }) - ) - // Change order of the target account. - const updated = await this.updateAccount( - ac._id!, - Object.assign(ac, { - order: previousOrder - }) - ) - // Update the previous account with right order. - await this.updateAccount( - previousAccount._id!, - Object.assign(previousAccount, { - order: targetOrder - }) - ) - return updated - } - - async backwardAccount(ac: LocalAccount): Promise { - // Find account which is the next of the target account. - const accounts = await this.searchAccounts({ order: { $gt: ac.order } }, 1).catch(err => { - console.log(err) - return [] - }) - if (accounts.length < 1) { - return Promise.resolve({}) - } - const nextAccount = accounts[0] - const targetOrder = ac.order - const nextOrder = nextAccount.order - - // At first, we need to update the next account with dummy order. - // Because this column is uniqued, so can not update with same order. - await this.updateAccount( - nextAccount._id!, - Object.assign(nextAccount, { - order: -1 - }) - ) - // Change order of the target account/ - const updated = await this.updateAccount( - ac._id!, - Object.assign(ac, { - order: nextOrder - }) - ) - // Update the next account with right order. - await this.updateAccount( - nextAccount._id!, - Object.assign(nextAccount, { - order: targetOrder - }) - ) - return updated - } - - async refreshAccounts(proxy: ProxyConfig | false): Promise> { - const accounts = await this.listAccounts() - if (accounts.length < 1) { - return accounts - } - const results = await Promise.all( - accounts.map(async account => { - const refresh = await this.refresh(account, proxy) - return refresh - }) - ) - return results - } - - /** - * refresh: Refresh an account which is already saved at local - * @param {LocalAccount} account is an local account - * @return {LocalAccount} updated account - */ - async refresh(account: LocalAccount, proxy: ProxyConfig | false): Promise { - const sns = await detector(account.baseURL, proxy) - let client = generator(sns, account.baseURL, account.accessToken, 'Whalebird', proxy) - let json = {} - try { - const res = await client.verifyAccountCredentials() - json = { - username: res.data.username, - accountId: res.data.id, - avatar: res.data.avatar - } - } catch (err) { - log.error(err) - log.info('Get new access token using refresh token...') - // If failed to fetch account, get new access token using refresh token. - if (!account.refreshToken) { - throw new RefreshTokenDoesNotExist() - } - const token = await client.refreshToken(account.clientId, account.clientSecret, account.refreshToken) - client = generator(sns, account.baseURL, token.access_token, 'Whalebird', proxy) - const res = await client.verifyAccountCredentials() - json = { - username: res.data.username, - accountId: res.data.id, - avatar: res.data.avatar, - accessToken: token.accessToken, - refreshToken: token.refreshToken - } - } - return this.updateAccount(account._id!, json) - } - - // Confirm the access token, and check duplicate - async fetchAccount( - sns: 'mastodon' | 'pleroma' | 'misskey', - account: LocalAccount, - accessToken: string, - proxy: ProxyConfig | false - ): Promise { - const client = generator(sns, account.baseURL, accessToken, 'Whalebird', proxy) - const res = await client.verifyAccountCredentials() - const query = { - baseURL: account.baseURL, - username: res.data.username - } - const duplicates = await this.searchAccounts(query) - if (duplicates.length > 0) { - throw new DuplicateRecordError(`${res.data.username}@${account.baseURL} is duplicated`) - } - return res.data - } + }) } -class EmptyRecordError extends Error {} +/** + * List up authenticated accounts. + */ +export const listAccounts = (db: sqlite3.Database): Promise> => { + return new Promise((resolve, reject) => { + db.all( + 'SELECT \ +accounts.id as id, \ +accounts.username as username, \ +accounts.account_id as remote_account_id, \ +accounts.avatar as avatar, \ +accounts.client_id as client_id, \ +accounts.client_secret as client_secret, \ +accounts.access_token as access_token, \ +accounts.refresh_token as refresh_token, \ +accounts.sort as sort, \ +servers.id as server_id, \ +servers.base_url as base_url, \ +servers.domain as domain, \ +servers.sns as sns, \ +servers.account_id as account_id \ +FROM accounts INNER JOIN servers ON servers.account_id = accounts.id ORDER BY accounts.sort', + (err, rows) => { + if (err) { + reject(err) + } + resolve( + rows.map(r => [ + { + id: r.id, + username: r.username, + accountId: r.remote_account_id, + avatar: r.avatar, + clientId: r.client_id, + clientSecret: r.client_secret, + accessToken: r.access_token, + refreshToken: r.refresh_token, + order: r.sort + } as LocalAccount, + { + id: r.server_id, + baseURL: r.base_url, + domain: r.domain, + sns: r.sns, + accountId: r.account_id + } as LocalServer + ]) + ) + } + ) + }) +} -class DuplicateRecordError extends Error {} - -class RefreshTokenDoesNotExist extends Error {} +export const getAccount = (db: sqlite3.Database, id: number): Promise<[LocalAccount, LocalServer]> => { + return new Promise((resolve, reject) => { + db.get( + 'SELECT \ +accounts.id as id, \ +accounts.username as username, \ +accounts.account_id as remote_account_id, \ +accounts.avatar as avatar, \ +accounts.client_id as client_id, \ +accounts.client_secret as client_secret, \ +accounts.access_token as access_token, \ +accounts.refresh_token as refresh_token, \ +accounts.sort as sort, \ +servers.id as server_id, \ +servers.base_url as base_url, \ +servers.domain as domain, \ +servers.sns as sns, \ +servers.account_id as account_id \ +FROM accounts INNER JOIN servers ON servers.account_id = accounts.id WHERE accounts.id = ?', + id, + (err, r) => { + if (err) { + reject(err) + } + resolve([ + { + id: r.id, + username: r.username, + accountId: r.remote_account_id, + avatar: r.avatar, + clientId: r.client_id, + clientSecret: r.client_secret, + accessToken: r.access_token, + refreshToken: r.refresh_token, + order: r.sort + } as LocalAccount, + { + id: r.server_id, + baseURL: r.base_url, + domain: r.domain, + sns: r.sns, + accountId: r.account_id + } as LocalServer + ]) + } + ) + }) +} diff --git a/src/main/auth.ts b/src/main/auth.ts deleted file mode 100644 index 2d988e21..00000000 --- a/src/main/auth.ts +++ /dev/null @@ -1,119 +0,0 @@ -import generator, { ProxyConfig } from 'megalodon' -import crypto from 'crypto' -import Account from './account' -import { LocalAccount } from '~/src/types/localAccount' - -const appName = 'Whalebird' -const appURL = 'https://whalebird.social' - -export default class Authentication { - private db: Account - private baseURL: string | null = null - private domain: string | null = null - private clientId: string | null = null - private clientSecret: string | null = null - private sessionToken: string | null = null - private protocol: 'http' | 'https' - - constructor(accountDB: Account) { - this.db = accountDB - this.protocol = 'https' - } - - setOtherInstance(domain: string) { - this.baseURL = `${this.protocol}://${domain}` - this.domain = domain - this.clientId = null - this.clientSecret = null - } - - async getAuthorizationUrl( - sns: 'mastodon' | 'pleroma' | 'misskey', - domain: string = 'mastodon.social', - proxy: ProxyConfig | false - ): Promise { - this.setOtherInstance(domain) - if (!this.baseURL || !this.domain) { - throw new Error('domain is required') - } - const client = generator(sns, this.baseURL, null, 'Whalebird', proxy) - const res = await client.registerApp(appName, { - website: appURL - }) - this.clientId = res.clientId - this.clientSecret = res.clientSecret - this.sessionToken = res.session_token - - const order = await this.db - .lastAccount() - .then(account => account.order + 1) - .catch(err => { - console.log(err) - return 1 - }) - const local: LocalAccount = { - baseURL: this.baseURL, - domain: this.domain, - clientId: this.clientId, - clientSecret: this.clientSecret, - accessToken: null, - refreshToken: null, - username: null, - accountId: null, - avatar: null, - order: order - } - await this.db.insertAccount(local) - if (res.url === null) { - throw new AuthenticationURLError('Can not get url') - } - return res.url - } - - async getAndUpdateAccessToken(sns: 'mastodon' | 'pleroma' | 'misskey', code: string | null, proxy: ProxyConfig | false): Promise { - if (!this.baseURL) { - throw new Error('domain is required') - } - if (!this.clientSecret) { - throw new Error('client secret is required') - } - const client = generator(sns, this.baseURL, null, 'Whalebird', proxy) - - // In Misskey session token is required instead of authentication code. - let authCode = code - if (!code) { - authCode = this.sessionToken - } - if (!authCode) { - throw new Error('auth code is required') - } - const tokenData = await client.fetchAccessToken(this.clientId, this.clientSecret, authCode, 'urn:ietf:wg:oauth:2.0:oob') - const search = { - baseURL: this.baseURL, - domain: this.domain, - clientId: this.clientId, - clientSecret: this.clientSecret - } - const rec = await this.db.searchAccount(search) - let accessToken = tokenData.accessToken - // In misskey, access token is sha256(userToken + clientSecret) - if (sns === 'misskey') { - accessToken = crypto - .createHash('sha256') - .update(tokenData.accessToken + this.clientSecret, 'utf8') - .digest('hex') - } - const refreshToken = tokenData.refreshToken - const data = await this.db.fetchAccount(sns, rec, accessToken, proxy) - await this.db.updateAccount(rec._id!, { - username: data.username, - accountId: data.id, - avatar: data.avatar, - accessToken: accessToken, - refreshToken: refreshToken - }) - return accessToken - } -} - -class AuthenticationURLError extends Error {} diff --git a/src/main/database.ts b/src/main/database.ts index 51558345..0d82b6d4 100644 --- a/src/main/database.ts +++ b/src/main/database.ts @@ -1,22 +1,44 @@ -import Loki from 'lokijs' +import sqlite3 from 'sqlite3' -const newDB = (file: string): Promise => { - return new Promise(resolve => { - const databaseInitializer = () => { - let markers = db.getCollection('markers') - if (markers === null) { - markers = db.addCollection('markers') +const newDB = (file: string): sqlite3.Database => { + let db = new sqlite3.Database(file, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE) + + // migration + db.serialize(() => { + db.run( + 'CREATE TABLE IF NOT EXISTS accounts(\ +id INTEGER PRIMARY KEY, \ +username TEXT NOT NULL, \ +account_id TEXT NOT NULL, \ +avatar TEXT NOT NULL, \ +client_id TEXT DEFAULT NULL, \ +client_secret TEXT NOT NULL, \ +access_token TEXT NOT NULL, \ +refresh_token TEXT DEFAULT NULL, \ +sort INTEGER UNIQUE NOT NULL)', + err => { + if (err) { + console.error('failed to create accounts: ', err) + } } - resolve(db) - } - - const db = new Loki(file, { - autoload: true, - autosave: true, - autosaveInterval: 4000, - autoloadCallback: databaseInitializer - }) + ) + db.run( + 'CREATE TABLE IF NOT EXISTS servers(\ +id INTEGER PRIMARY KEY, \ +domain TEXT NOT NULL, \ +base_url TEXT NOT NULL, \ +sns TEXT NOT NULL, \ +account_id INTEGER UNIQUE DEFAULT NULL, \ +FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE)', + err => { + if (err) { + console.error('failed to create servers: ', err) + } + } + ) }) + + return db } export default newDB diff --git a/src/main/index.ts b/src/main/index.ts index 8c311262..24bc0f06 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -16,8 +16,9 @@ import { nativeTheme, IpcMainInvokeEvent } from 'electron' + import Datastore from 'nedb' -import { isEmpty } from 'lodash' +import crypto from 'crypto' import log from 'electron-log' import windowStateKeeper from 'electron-window-state' import simplayer from 'simplayer' @@ -25,14 +26,13 @@ import path from 'path' import ContextMenu from 'electron-context-menu' import { initSplashScreen, Config } from '@trodi/electron-splashscreen' import openAboutWindow from 'about-window' -import generator, { Entity, detector, NotificationType, MegalodonInterface } from 'megalodon' +import generator, { Entity, detector, NotificationType, OAuth, MegalodonInterface } from 'megalodon' import sanitizeHtml from 'sanitize-html' import AutoLaunch from 'auto-launch' import minimist from 'minimist' -import Authentication from './auth' -import Account from './account' -import { StreamingURL, UserStreaming, DirectStreaming, LocalStreaming, PublicStreaming, ListStreaming, TagStreaming } from './websocket' +import { getAccount, insertAccount, listAccounts } from './account' +// import { StreamingURL, UserStreaming, DirectStreaming, LocalStreaming, PublicStreaming, ListStreaming, TagStreaming } from './websocket' import Preferences from './preferences' import Fonts from './fonts' import Hashtags from './hashtags' @@ -42,14 +42,12 @@ import Language, { LanguageType } from '../constants/language' import { LocalAccount } from '~/src/types/localAccount' import { LocalTag } from '~/src/types/localTag' import { Notify } from '~/src/types/notify' -import { StreamingError } from '~/src/errors/streamingError' +// import { StreamingError } from '~/src/errors/streamingError' import HashtagCache from './cache/hashtag' import AccountCache from './cache/account' import { InsertAccountCache } from '~/src/types/insertAccountCache' import { Proxy } from '~/src/types/proxy' import ProxyConfiguration from './proxy' -import confirm from './timelines' -import { EnabledTimelines } from '~/src/types/enabledTimelines' import { Menu as MenuPreferences } from '~/src/types/preference' import { General as GeneralPreferences } from '~/src/types/preference' import { LocalMarker } from '~/src/types/localMarker' @@ -57,6 +55,8 @@ import Marker from './marker' import newDB from './database' import Settings from './settings' import { BaseSettings, Setting } from '~/src/types/setting' +import { insertServer } from './server' +import { LocalServer } from '~src/types/localServer' /** * Context menu @@ -123,13 +123,12 @@ const splashURL = const userData = app.getPath('userData') const appPath = app.getPath('exe') -const accountDBPath = process.env.NODE_ENV === 'production' ? userData + '/db/account.db' : 'account.db' -const accountDB = new Datastore({ - filename: accountDBPath, - autoload: true -}) -const accountRepo = new Account(accountDB) -accountRepo.initialize().catch((err: Error) => log.error(err)) +const databasePath = process.env.NODE_ENV === 'production' ? userData + '/db/whalebird.db' : 'whalebird.db' +const db = newDB(databasePath) + +const preferencesDBPath = process.env.NODE_ENV === 'production' ? userData + './db/preferences.json' : 'preferences.json' + +let markerRepo: Marker | null = null const hashtagsDBPath = process.env.NODE_ENV === 'production' ? userData + '/db/hashtags.db' : 'hashtags.db' const hashtagsDB = new Datastore({ @@ -139,12 +138,6 @@ const hashtagsDB = new Datastore({ const settingsDBPath = process.env.NODE_ENV === 'production' ? userData + './db/settings.json' : 'settings.json' -const preferencesDBPath = process.env.NODE_ENV === 'production' ? userData + './db/preferences.json' : 'preferences.json' - -const lokiDatabasePath = process.env.NODE_ENV === 'production' ? userData + '/db/lokiDatabase.db' : 'lokiDatabase.db' - -let markerRepo: Marker | null = null - /** * Cache path */ @@ -175,16 +168,7 @@ if (process.platform !== 'darwin') { }) } -async function listAccounts(): Promise> { - try { - const accounts = await accountRepo.listAccounts() - return accounts - } catch (err) { - return [] - } -} - -async function changeAccount(account: LocalAccount, index: number) { +async function changeAccount([account, _server]: [LocalAccount, LocalServer], index: number) { // Sometimes application is closed to tray. // In this time, mainWindow in not exist, so we have to create window. if (mainWindow === null) { @@ -260,20 +244,15 @@ const updateDockMenu = async (accountsChange: Array) } async function createWindow() { - /** - DB - */ - const lokiDB = await newDB(lokiDatabasePath) - markerRepo = new Marker(lokiDB) /** * List accounts */ - const accounts = await listAccounts() - const accountsChange: Array = accounts.map((a, index) => { + const accounts = await listAccounts(db) + const accountsChange: Array = accounts.map(([a, s], index) => { return { - label: a.domain, + label: s.domain, accelerator: `CmdOrCtrl+${index + 1}`, - click: () => changeAccount(a, index) + click: () => changeAccount([a, s], index) } }) @@ -474,132 +453,134 @@ app.on('activate', () => { } }) -const auth = new Authentication(accountRepo) - -type AuthRequest = { - instance: string - sns: 'mastodon' | 'pleroma' | 'misskey' -} - -ipcMain.handle('get-auth-url', async (_: IpcMainInvokeEvent, request: AuthRequest) => { +ipcMain.handle('add-server', async (_: IpcMainInvokeEvent, domain: string) => { const proxy = await proxyConfiguration.forMastodon() - const url = await auth.getAuthorizationUrl(request.sns, request.instance, proxy) - log.debug(url) - // Open authorize url in default browser. - shell.openExternal(url) - return url + const sns = await detector(`https://${domain}`, proxy) + const server = await insertServer(db, `https://${domain}`, domain, sns, null) + return server }) -type TokenRequest = { - code: string | null - sns: 'mastodon' | 'pleroma' | 'misskey' +ipcMain.handle('add-app', async (_: IpcMainInvokeEvent, url: string) => { + const proxy = await proxyConfiguration.forMastodon() + const sns = await detector(url, proxy) + const client = generator(sns, url, null, 'Whalebird', proxy) + const appData = await client.registerApp('Whalebird', { + website: 'https://whalebird.social' + }) + if (appData.url) { + shell.openExternal(appData.url) + } + return appData +}) + +type AuthorizeRequest = { + server: LocalServer + appData: OAuth.AppData + code: string } -ipcMain.handle('get-and-update-access-token', async (_: IpcMainInvokeEvent, request: TokenRequest) => { +ipcMain.handle('authorize', async (_: IpcMainInvokeEvent, req: AuthorizeRequest) => { const proxy = await proxyConfiguration.forMastodon() - const token = await auth.getAndUpdateAccessToken(request.sns, request.code, proxy) - // Update instance menu - const accounts = await listAccounts() - const accountsChange: Array = accounts.map((a, index) => { - return { - label: a.domain, - accelerator: `CmdOrCtrl+${index + 1}`, - click: () => changeAccount(a, index) - } - }) - - await updateApplicationMenu(accountsChange) - await updateDockMenu(accountsChange) - if (process.platform !== 'darwin' && tray !== null) { - tray.setContextMenu(TrayMenu(accountsChange, i18next)) + const sns = await detector(req.server.baseURL, proxy) + const client = generator(sns, req.server.baseURL, null, 'Whalebird', proxy) + const tokenData = await client.fetchAccessToken(req.appData.client_id, req.appData.client_secret, req.code, 'urn:ietf:wg:oauth:2.0:oob') + let accessToken = tokenData.access_token + if (sns === 'misskey') { + // In misskey, access token is sha256(userToken + clientSecret) + accessToken = crypto + .createHash('sha256') + .update(tokenData.access_token + req.appData.client_secret, 'utf8') + .digest('hex') } - return new Promise((resolve, reject) => { - accountDB.findOne( - { - accessToken: token - }, - (err, doc: any) => { - if (err) return reject(err) - if (isEmpty(doc)) return reject(err) - resolve(doc._id) - } - ) - }) -}) + const authorizedClient = generator(sns, req.server.baseURL, accessToken, 'Whalebird', proxy) + const credentials = await authorizedClient.verifyAccountCredentials() -// nedb -ipcMain.handle('list-accounts', async (_: IpcMainInvokeEvent) => { - const accounts = await accountRepo.listAccounts() - return accounts -}) - -ipcMain.handle('get-local-account', async (_: IpcMainInvokeEvent, id: string) => { - const account = await accountRepo.getAccount(id) + const account = await insertAccount( + db, + credentials.data.username, + credentials.data.id, + credentials.data.avatar, + req.appData.client_id, + req.appData.client_secret, + accessToken, + tokenData.refresh_token, + req.server + ) return account }) -ipcMain.handle('update-account', async (_: IpcMainInvokeEvent, acct: LocalAccount) => { - const proxy = await proxyConfiguration.forMastodon() - const ac: LocalAccount = await accountRepo.refresh(acct, proxy) - return ac -}) - -ipcMain.handle('remove-account', async (_: IpcMainInvokeEvent, id: string) => { - const accountId = await accountRepo.removeAccount(id) - - const accounts = await listAccounts() - const accountsChange: Array = accounts.map((a, index) => { - return { - label: a.domain, - accelerator: `CmdOrCtrl+${index + 1}`, - click: () => changeAccount(a, index) - } - }) - - await updateApplicationMenu(accountsChange) - await updateDockMenu(accountsChange) - if (process.platform !== 'darwin' && tray !== null) { - tray.setContextMenu(TrayMenu(accountsChange, i18next)) - } - - stopUserStreaming(accountId) -}) - -ipcMain.handle('forward-account', async (_: IpcMainInvokeEvent, acct: LocalAccount) => { - await accountRepo.forwardAccount(acct) -}) - -ipcMain.handle('backward-account', async (_: IpcMainInvokeEvent, acct: LocalAccount) => { - await accountRepo.backwardAccount(acct) -}) - -ipcMain.handle('refresh-accounts', async (_: IpcMainInvokeEvent) => { - const proxy = await proxyConfiguration.forMastodon() - const accounts = await accountRepo.refreshAccounts(proxy) - +ipcMain.handle('list-accounts', async (_: IpcMainInvokeEvent) => { + const accounts = await listAccounts(db) return accounts }) -ipcMain.handle('remove-all-accounts', async (_: IpcMainInvokeEvent) => { - await accountRepo.removeAll() - - const accounts = await listAccounts() - const accountsChange: Array = accounts.map((a, index) => { - return { - label: a.domain, - accelerator: `CmdOrCtrl+${index + 1}`, - click: () => changeAccount(a, index) - } - }) - - await updateApplicationMenu(accountsChange) - await updateDockMenu(accountsChange) - if (process.platform !== 'darwin' && tray !== null) { - tray.setContextMenu(TrayMenu(accountsChange, i18next)) - } +ipcMain.handle('get-local-account', async (_: IpcMainInvokeEvent, id: number) => { + const account = await getAccount(db, id) + return account }) +// ipcMain.handle('update-account', async (_: IpcMainInvokeEvent, acct: LocalAccount) => { +// const proxy = await proxyConfiguration.forMastodon() +// const ac: LocalAccount = await accountRepo.refresh(acct, proxy) +// return ac +// }) + +// ipcMain.handle('remove-account', async (_: IpcMainInvokeEvent, id: string) => { +// const accountId = await accountRepo.removeAccount(id) + +// const accounts = await listAccounts() +// const accountsChange: Array = accounts.map((a, index) => { +// return { +// label: a.domain, +// accelerator: `CmdOrCtrl+${index + 1}`, +// click: () => changeAccount(a, index) +// } +// }) + +// await updateApplicationMenu(accountsChange) +// await updateDockMenu(accountsChange) +// if (process.platform !== 'darwin' && tray !== null) { +// tray.setContextMenu(TrayMenu(accountsChange, i18next)) +// } + +// stopUserStreaming(accountId) +// }) + +// ipcMain.handle('forward-account', async (_: IpcMainInvokeEvent, acct: LocalAccount) => { +// await accountRepo.forwardAccount(acct) +// }) + +// ipcMain.handle('backward-account', async (_: IpcMainInvokeEvent, acct: LocalAccount) => { +// await accountRepo.backwardAccount(acct) +// }) + +// ipcMain.handle('refresh-accounts', async (_: IpcMainInvokeEvent) => { +// const proxy = await proxyConfiguration.forMastodon() +// const accounts = await accountRepo.refreshAccounts(proxy) + +// return accounts +// }) + +// ipcMain.handle('remove-all-accounts', async (_: IpcMainInvokeEvent) => { +// await accountRepo.removeAll() + +// const accounts = await listAccounts() +// const accountsChange: Array = accounts.map((a, index) => { +// return { +// label: a.domain, +// accelerator: `CmdOrCtrl+${index + 1}`, +// click: () => changeAccount(a, index) +// } +// }) + +// await updateApplicationMenu(accountsChange) +// await updateDockMenu(accountsChange) +// if (process.platform !== 'darwin' && tray !== null) { +// tray.setContextMenu(TrayMenu(accountsChange, i18next)) +// } +// }) + ipcMain.handle('change-auto-launch', async (_: IpcMainInvokeEvent, enable: boolean) => { if (launcher) { const enabled = await launcher.isEnabled() @@ -621,371 +602,361 @@ ipcMain.on('reset-badge', () => { } }) -ipcMain.handle( - 'confirm-timelines', - async (_event: IpcMainInvokeEvent, account: LocalAccount): Promise => { - const proxy = await proxyConfiguration.forMastodon() - const timelines = await confirm(account, proxy) +// // user streaming +// const userStreamings: { [key: string]: UserStreaming | null } = {} - return timelines - } -) +// ipcMain.on('start-all-user-streamings', (event: IpcMainEvent, accounts: Array) => { +// accounts.map(async id => { +// const acct = await accountRepo.getAccount(id) +// try { +// // Stop old user streaming +// if (userStreamings[id]) { +// userStreamings[id]!.stop() +// userStreamings[id] = null +// } +// const proxy = await proxyConfiguration.forMastodon() +// const sns = await detector(acct.baseURL, proxy) +// const url = await StreamingURL(sns, acct, proxy) +// userStreamings[id] = new UserStreaming(sns, acct, url, proxy) +// userStreamings[id]!.start( +// async (update: Entity.Status) => { +// if (!event.sender.isDestroyed()) { +// event.sender.send(`update-start-all-user-streamings-${id}`, update) +// } +// // Cache hashtag +// update.tags.map(async tag => { +// await hashtagCache.insertHashtag(tag.name).catch(err => console.error(err)) +// }) +// // Cache account +// await accountCache.insertAccount(id, update.account.acct).catch(err => console.error(err)) +// }, +// async (notification: Entity.Notification) => { +// await publishNotification(notification, event, id) -// user streaming -const userStreamings: { [key: string]: UserStreaming | null } = {} +// // In macOS and Windows, sometimes window is closed (not quit). +// // But streamings are always running. +// // When window is closed, we can not send event to webContents; because it is already destroyed. +// // So we have to guard it. +// if (!event.sender.isDestroyed()) { +// // To update notification timeline +// event.sender.send(`notification-start-all-user-streamings-${id}`, notification) -ipcMain.on('start-all-user-streamings', (event: IpcMainEvent, accounts: Array) => { - accounts.map(async id => { - const acct = await accountRepo.getAccount(id) - try { - // Stop old user streaming - if (userStreamings[id]) { - userStreamings[id]!.stop() - userStreamings[id] = null - } - const proxy = await proxyConfiguration.forMastodon() - const sns = await detector(acct.baseURL, proxy) - const url = await StreamingURL(sns, acct, proxy) - userStreamings[id] = new UserStreaming(sns, acct, url, proxy) - userStreamings[id]!.start( - async (update: Entity.Status) => { - if (!event.sender.isDestroyed()) { - event.sender.send(`update-start-all-user-streamings-${id}`, update) - } - // Cache hashtag - update.tags.map(async tag => { - await hashtagCache.insertHashtag(tag.name).catch(err => console.error(err)) - }) - // Cache account - await accountCache.insertAccount(id, update.account.acct).catch(err => console.error(err)) - }, - async (notification: Entity.Notification) => { - await publishNotification(notification, event, id) +// // Does not exist a endpoint for only mention. And mention is a part of notification. +// // So we have to get mention from notification. +// if (notification.type === 'mention') { +// event.sender.send(`mention-start-all-user-streamings-${id}`, notification) +// } +// } +// }, +// (statusId: string) => { +// if (!event.sender.isDestroyed()) { +// event.sender.send(`delete-start-all-user-streamings-${id}`, statusId) +// } +// }, +// (err: Error) => { +// log.error(err) +// // In macOS, sometimes window is closed (not quit). +// // When window is closed, we can not send event to webContents; because it is destroyed. +// // So we have to guard it. +// if (!event.sender.isDestroyed()) { +// event.sender.send('error-start-all-user-streamings', err) +// } +// } +// ) +// // Generate notifications received while the app was not running +// const client = generator(sns, acct.baseURL, acct.accessToken, 'Whalebird', proxy) +// const marker = await getMarker(client, id) +// if (marker !== null) { +// const unreadResponse = await client.getNotifications({ min_id: marker.last_read_id }) +// unreadResponse.data.map(async notification => { +// await publishNotification(notification, event, id) +// }) +// } +// } catch (err: any) { +// log.error(err) +// const streamingError = new StreamingError(err.message, acct.domain) +// if (!event.sender.isDestroyed()) { +// event.sender.send('error-start-all-user-streamings', streamingError) +// } +// } +// }) +// }) - // In macOS and Windows, sometimes window is closed (not quit). - // But streamings are always running. - // When window is closed, we can not send event to webContents; because it is already destroyed. - // So we have to guard it. - if (!event.sender.isDestroyed()) { - // To update notification timeline - event.sender.send(`notification-start-all-user-streamings-${id}`, notification) +// ipcMain.on('stop-all-user-streamings', () => { +// Object.keys(userStreamings).forEach((key: string) => { +// if (userStreamings[key]) { +// userStreamings[key]!.stop() +// userStreamings[key] = null +// } +// }) +// }) - // Does not exist a endpoint for only mention. And mention is a part of notification. - // So we have to get mention from notification. - if (notification.type === 'mention') { - event.sender.send(`mention-start-all-user-streamings-${id}`, notification) - } - } - }, - (statusId: string) => { - if (!event.sender.isDestroyed()) { - event.sender.send(`delete-start-all-user-streamings-${id}`, statusId) - } - }, - (err: Error) => { - log.error(err) - // In macOS, sometimes window is closed (not quit). - // When window is closed, we can not send event to webContents; because it is destroyed. - // So we have to guard it. - if (!event.sender.isDestroyed()) { - event.sender.send('error-start-all-user-streamings', err) - } - } - ) - // Generate notifications received while the app was not running - const client = generator(sns, acct.baseURL, acct.accessToken, 'Whalebird', proxy) - const marker = await getMarker(client, id) - if (marker !== null) { - const unreadResponse = await client.getNotifications({ min_id: marker.last_read_id }) - unreadResponse.data.map(async notification => { - await publishNotification(notification, event, id) - }) - } - } catch (err: any) { - log.error(err) - const streamingError = new StreamingError(err.message, acct.domain) - if (!event.sender.isDestroyed()) { - event.sender.send('error-start-all-user-streamings', streamingError) - } - } - }) -}) +// /** +// * Stop an user streaming in all user streamings. +// * @param id specified user id in nedb. +// */ +// const stopUserStreaming = (id: string) => { +// Object.keys(userStreamings).forEach((key: string) => { +// if (key === id && userStreamings[id]) { +// userStreamings[id]!.stop() +// userStreamings[id] = null +// } +// }) +// } -ipcMain.on('stop-all-user-streamings', () => { - Object.keys(userStreamings).forEach((key: string) => { - if (userStreamings[key]) { - userStreamings[key]!.stop() - userStreamings[key] = null - } - }) -}) +// let directMessagesStreaming: DirectStreaming | null = null -/** - * Stop an user streaming in all user streamings. - * @param id specified user id in nedb. - */ -const stopUserStreaming = (id: string) => { - Object.keys(userStreamings).forEach((key: string) => { - if (key === id && userStreamings[id]) { - userStreamings[id]!.stop() - userStreamings[id] = null - } - }) -} +// ipcMain.on('start-directmessages-streaming', async (event: IpcMainEvent, id: string) => { +// try { +// const acct = await accountRepo.getAccount(id) -let directMessagesStreaming: DirectStreaming | null = null +// // Stop old directmessages streaming +// if (directMessagesStreaming !== null) { +// directMessagesStreaming.stop() +// directMessagesStreaming = null +// } +// const proxy = await proxyConfiguration.forMastodon() +// const sns = await detector(acct.baseURL, proxy) +// const url = await StreamingURL(sns, acct, proxy) +// directMessagesStreaming = new DirectStreaming(sns, acct, url, proxy) +// directMessagesStreaming.start( +// (update: Entity.Status) => { +// if (!event.sender.isDestroyed()) { +// event.sender.send('update-start-directmessages-streaming', update) +// } +// }, +// (id: string) => { +// if (!event.sender.isDestroyed()) { +// event.sender.send('delete-start-directmessages-streaming', id) +// } +// }, +// (err: Error) => { +// log.error(err) +// if (!event.sender.isDestroyed()) { +// event.sender.send('error-start-directmessages-streaming', err) +// } +// } +// ) +// } catch (err) { +// log.error(err) +// if (!event.sender.isDestroyed()) { +// event.sender.send('error-start-directmessages-streaming', err) +// } +// } +// }) -ipcMain.on('start-directmessages-streaming', async (event: IpcMainEvent, id: string) => { - try { - const acct = await accountRepo.getAccount(id) +// ipcMain.on('stop-directmessages-streaming', () => { +// if (directMessagesStreaming !== null) { +// directMessagesStreaming.stop() +// directMessagesStreaming = null +// } +// }) - // Stop old directmessages streaming - if (directMessagesStreaming !== null) { - directMessagesStreaming.stop() - directMessagesStreaming = null - } - const proxy = await proxyConfiguration.forMastodon() - const sns = await detector(acct.baseURL, proxy) - const url = await StreamingURL(sns, acct, proxy) - directMessagesStreaming = new DirectStreaming(sns, acct, url, proxy) - directMessagesStreaming.start( - (update: Entity.Status) => { - if (!event.sender.isDestroyed()) { - event.sender.send('update-start-directmessages-streaming', update) - } - }, - (id: string) => { - if (!event.sender.isDestroyed()) { - event.sender.send('delete-start-directmessages-streaming', id) - } - }, - (err: Error) => { - log.error(err) - if (!event.sender.isDestroyed()) { - event.sender.send('error-start-directmessages-streaming', err) - } - } - ) - } catch (err) { - log.error(err) - if (!event.sender.isDestroyed()) { - event.sender.send('error-start-directmessages-streaming', err) - } - } -}) +// let localStreaming: LocalStreaming | null = null -ipcMain.on('stop-directmessages-streaming', () => { - if (directMessagesStreaming !== null) { - directMessagesStreaming.stop() - directMessagesStreaming = null - } -}) +// ipcMain.on('start-local-streaming', async (event: IpcMainEvent, id: string) => { +// try { +// const acct = await accountRepo.getAccount(id) -let localStreaming: LocalStreaming | null = null +// // Stop old local streaming +// if (localStreaming !== null) { +// localStreaming.stop() +// localStreaming = null +// } +// const proxy = await proxyConfiguration.forMastodon() +// const sns = await detector(acct.baseURL, proxy) +// const url = await StreamingURL(sns, acct, proxy) +// localStreaming = new LocalStreaming(sns, acct, url, proxy) +// localStreaming.start( +// (update: Entity.Status) => { +// if (!event.sender.isDestroyed()) { +// event.sender.send('update-start-local-streaming', update) +// } +// }, +// (id: string) => { +// if (!event.sender.isDestroyed()) { +// event.sender.send('delete-start-local-streaming', id) +// } +// }, +// (err: Error) => { +// log.error(err) +// if (!event.sender.isDestroyed()) { +// event.sender.send('error-start-local-streaming', err) +// } +// } +// ) +// } catch (err) { +// log.error(err) +// if (!event.sender.isDestroyed()) { +// event.sender.send('error-start-local-streaming', err) +// } +// } +// }) -ipcMain.on('start-local-streaming', async (event: IpcMainEvent, id: string) => { - try { - const acct = await accountRepo.getAccount(id) +// ipcMain.on('stop-local-streaming', () => { +// if (localStreaming !== null) { +// localStreaming.stop() +// localStreaming = null +// } +// }) - // Stop old local streaming - if (localStreaming !== null) { - localStreaming.stop() - localStreaming = null - } - const proxy = await proxyConfiguration.forMastodon() - const sns = await detector(acct.baseURL, proxy) - const url = await StreamingURL(sns, acct, proxy) - localStreaming = new LocalStreaming(sns, acct, url, proxy) - localStreaming.start( - (update: Entity.Status) => { - if (!event.sender.isDestroyed()) { - event.sender.send('update-start-local-streaming', update) - } - }, - (id: string) => { - if (!event.sender.isDestroyed()) { - event.sender.send('delete-start-local-streaming', id) - } - }, - (err: Error) => { - log.error(err) - if (!event.sender.isDestroyed()) { - event.sender.send('error-start-local-streaming', err) - } - } - ) - } catch (err) { - log.error(err) - if (!event.sender.isDestroyed()) { - event.sender.send('error-start-local-streaming', err) - } - } -}) +// let publicStreaming: PublicStreaming | null = null -ipcMain.on('stop-local-streaming', () => { - if (localStreaming !== null) { - localStreaming.stop() - localStreaming = null - } -}) +// ipcMain.on('start-public-streaming', async (event: IpcMainEvent, id: string) => { +// try { +// const acct = await accountRepo.getAccount(id) -let publicStreaming: PublicStreaming | null = null +// // Stop old public streaming +// if (publicStreaming !== null) { +// publicStreaming.stop() +// publicStreaming = null +// } +// const proxy = await proxyConfiguration.forMastodon() +// const sns = await detector(acct.baseURL, proxy) +// const url = await StreamingURL(sns, acct, proxy) +// publicStreaming = new PublicStreaming(sns, acct, url, proxy) +// publicStreaming.start( +// (update: Entity.Status) => { +// if (!event.sender.isDestroyed()) { +// event.sender.send('update-start-public-streaming', update) +// } +// }, +// (id: string) => { +// if (!event.sender.isDestroyed()) { +// event.sender.send('delete-start-public-streaming', id) +// } +// }, +// (err: Error) => { +// log.error(err) +// if (!event.sender.isDestroyed()) { +// event.sender.send('error-start-public-streaming', err) +// } +// } +// ) +// } catch (err) { +// log.error(err) +// if (!event.sender.isDestroyed()) { +// event.sender.send('error-start-public-streaming', err) +// } +// } +// }) -ipcMain.on('start-public-streaming', async (event: IpcMainEvent, id: string) => { - try { - const acct = await accountRepo.getAccount(id) +// ipcMain.on('stop-public-streaming', () => { +// if (publicStreaming !== null) { +// publicStreaming.stop() +// publicStreaming = null +// } +// }) - // Stop old public streaming - if (publicStreaming !== null) { - publicStreaming.stop() - publicStreaming = null - } - const proxy = await proxyConfiguration.forMastodon() - const sns = await detector(acct.baseURL, proxy) - const url = await StreamingURL(sns, acct, proxy) - publicStreaming = new PublicStreaming(sns, acct, url, proxy) - publicStreaming.start( - (update: Entity.Status) => { - if (!event.sender.isDestroyed()) { - event.sender.send('update-start-public-streaming', update) - } - }, - (id: string) => { - if (!event.sender.isDestroyed()) { - event.sender.send('delete-start-public-streaming', id) - } - }, - (err: Error) => { - log.error(err) - if (!event.sender.isDestroyed()) { - event.sender.send('error-start-public-streaming', err) - } - } - ) - } catch (err) { - log.error(err) - if (!event.sender.isDestroyed()) { - event.sender.send('error-start-public-streaming', err) - } - } -}) +// let listStreaming: ListStreaming | null = null -ipcMain.on('stop-public-streaming', () => { - if (publicStreaming !== null) { - publicStreaming.stop() - publicStreaming = null - } -}) +// type ListStreamingOpts = { +// listID: string +// accountID: string +// } -let listStreaming: ListStreaming | null = null +// ipcMain.on('start-list-streaming', async (event: IpcMainEvent, obj: ListStreamingOpts) => { +// const { listID, accountID } = obj +// try { +// const acct = await accountRepo.getAccount(accountID) -type ListStreamingOpts = { - listID: string - accountID: string -} +// // Stop old list streaming +// if (listStreaming !== null) { +// listStreaming.stop() +// listStreaming = null +// } +// const proxy = await proxyConfiguration.forMastodon() +// const sns = await detector(acct.baseURL, proxy) +// const url = await StreamingURL(sns, acct, proxy) +// listStreaming = new ListStreaming(sns, acct, url, proxy) +// listStreaming.start( +// listID, +// (update: Entity.Status) => { +// if (!event.sender.isDestroyed()) { +// event.sender.send('update-start-list-streaming', update) +// } +// }, +// (id: string) => { +// if (!event.sender.isDestroyed()) { +// event.sender.send('delete-start-list-streaming', id) +// } +// }, +// (err: Error) => { +// log.error(err) +// if (!event.sender.isDestroyed()) { +// event.sender.send('error-start-list-streaming', err) +// } +// } +// ) +// } catch (err) { +// log.error(err) +// if (!event.sender.isDestroyed()) { +// event.sender.send('error-start-list-streaming', err) +// } +// } +// }) -ipcMain.on('start-list-streaming', async (event: IpcMainEvent, obj: ListStreamingOpts) => { - const { listID, accountID } = obj - try { - const acct = await accountRepo.getAccount(accountID) +// ipcMain.on('stop-list-streaming', () => { +// if (listStreaming !== null) { +// listStreaming.stop() +// listStreaming = null +// } +// }) - // Stop old list streaming - if (listStreaming !== null) { - listStreaming.stop() - listStreaming = null - } - const proxy = await proxyConfiguration.forMastodon() - const sns = await detector(acct.baseURL, proxy) - const url = await StreamingURL(sns, acct, proxy) - listStreaming = new ListStreaming(sns, acct, url, proxy) - listStreaming.start( - listID, - (update: Entity.Status) => { - if (!event.sender.isDestroyed()) { - event.sender.send('update-start-list-streaming', update) - } - }, - (id: string) => { - if (!event.sender.isDestroyed()) { - event.sender.send('delete-start-list-streaming', id) - } - }, - (err: Error) => { - log.error(err) - if (!event.sender.isDestroyed()) { - event.sender.send('error-start-list-streaming', err) - } - } - ) - } catch (err) { - log.error(err) - if (!event.sender.isDestroyed()) { - event.sender.send('error-start-list-streaming', err) - } - } -}) +// let tagStreaming: TagStreaming | null = null -ipcMain.on('stop-list-streaming', () => { - if (listStreaming !== null) { - listStreaming.stop() - listStreaming = null - } -}) +// type TagStreamingOpts = { +// tag: string +// accountID: string +// } -let tagStreaming: TagStreaming | null = null +// ipcMain.on('start-tag-streaming', async (event: IpcMainEvent, obj: TagStreamingOpts) => { +// const { tag, accountID } = obj +// try { +// const acct = await accountRepo.getAccount(accountID) -type TagStreamingOpts = { - tag: string - accountID: string -} +// // Stop old tag streaming +// if (tagStreaming !== null) { +// tagStreaming.stop() +// tagStreaming = null +// } +// const proxy = await proxyConfiguration.forMastodon() +// const sns = await detector(acct.baseURL, proxy) +// const url = await StreamingURL(sns, acct, proxy) +// tagStreaming = new TagStreaming(sns, acct, url, proxy) +// tagStreaming.start( +// tag, +// (update: Entity.Status) => { +// if (!event.sender.isDestroyed()) { +// event.sender.send('update-start-tag-streaming', update) +// } +// }, +// (id: string) => { +// if (!event.sender.isDestroyed()) { +// event.sender.send('delete-start-tag-streaming', id) +// } +// }, +// (err: Error) => { +// log.error(err) +// if (!event.sender.isDestroyed()) { +// event.sender.send('error-start-tag-streaming', err) +// } +// } +// ) +// } catch (err) { +// log.error(err) +// if (!event.sender.isDestroyed()) { +// event.sender.send('error-start-tag-streaming', err) +// } +// } +// }) -ipcMain.on('start-tag-streaming', async (event: IpcMainEvent, obj: TagStreamingOpts) => { - const { tag, accountID } = obj - try { - const acct = await accountRepo.getAccount(accountID) - - // Stop old tag streaming - if (tagStreaming !== null) { - tagStreaming.stop() - tagStreaming = null - } - const proxy = await proxyConfiguration.forMastodon() - const sns = await detector(acct.baseURL, proxy) - const url = await StreamingURL(sns, acct, proxy) - tagStreaming = new TagStreaming(sns, acct, url, proxy) - tagStreaming.start( - tag, - (update: Entity.Status) => { - if (!event.sender.isDestroyed()) { - event.sender.send('update-start-tag-streaming', update) - } - }, - (id: string) => { - if (!event.sender.isDestroyed()) { - event.sender.send('delete-start-tag-streaming', id) - } - }, - (err: Error) => { - log.error(err) - if (!event.sender.isDestroyed()) { - event.sender.send('error-start-tag-streaming', err) - } - } - ) - } catch (err) { - log.error(err) - if (!event.sender.isDestroyed()) { - event.sender.send('error-start-tag-streaming', err) - } - } -}) - -ipcMain.on('stop-tag-streaming', () => { - if (tagStreaming !== null) { - tagStreaming.stop() - tagStreaming = null - } -}) +// ipcMain.on('stop-tag-streaming', () => { +// if (tagStreaming !== null) { +// tagStreaming.stop() +// tagStreaming = null +// } +// }) // sounds ipcMain.on('fav-rt-action-sound', () => { @@ -1119,12 +1090,12 @@ ipcMain.handle('change-language', async (_: IpcMainInvokeEvent, value: string) = }) i18next.changeLanguage(conf.language.language) - const accounts = await listAccounts() - const accountsChange: Array = accounts.map((a, index) => { + const accounts = await listAccounts(db) + const accountsChange: Array = accounts.map(([a, s], index) => { return { - label: a.domain, + label: s.domain, accelerator: `CmdOrCtrl+${index + 1}`, - click: () => changeAccount(a, index) + click: () => changeAccount([a, s], index) } }) diff --git a/src/main/server.ts b/src/main/server.ts new file mode 100644 index 00000000..73d32c87 --- /dev/null +++ b/src/main/server.ts @@ -0,0 +1,29 @@ +import sqlite3 from 'sqlite3' +import { LocalServer } from '~/src/types/localServer' + +export const insertServer = ( + db: sqlite3.Database, + baseURL: string, + domain: string, + sns: 'mastodon' | 'pleroma' | 'misskey', + accountId: number | null +): Promise => { + return new Promise((resolve, reject) => { + db.serialize(() => { + db.run('INSERT INTO servers(domain, base_url, sns, account_id) values (?, ?, ?, ?)', [domain, baseURL, sns, accountId], function ( + err + ) { + if (err) { + reject(err) + } + resolve({ + id: this.lastID, + baseURL, + domain, + sns, + accountId + }) + }) + }) + }) +} diff --git a/src/main/timelines.ts b/src/main/timelines.ts deleted file mode 100644 index f5d5e6bb..00000000 --- a/src/main/timelines.ts +++ /dev/null @@ -1,62 +0,0 @@ -import generator, { detector, ProxyConfig } from 'megalodon' -import { LocalAccount } from '~/src/types/localAccount' -import { EnabledTimelines } from '~/src/types/enabledTimelines' - -const confirm = async (account: LocalAccount, proxy: ProxyConfig | false) => { - const sns = await detector(account.baseURL, proxy) - const client = generator(sns, account.baseURL, account.accessToken, 'Whalebird', proxy) - - let timelines: EnabledTimelines = { - home: true, - notification: true, - mention: true, - direct: true, - favourite: true, - bookmark: true, - local: true, - public: true, - tag: true, - list: true - } - - const notification = async () => { - return client.getNotifications({ limit: 1 }).catch(() => { - timelines = { ...timelines, notification: false, mention: false } - }) - } - const direct = async () => { - return client.getConversationTimeline({ limit: 1 }).catch(() => { - timelines = { ...timelines, direct: false } - }) - } - const favourite = async () => { - return client.getFavourites({ limit: 1 }).catch(() => { - timelines = { ...timelines, favourite: false } - }) - } - const bookmark = async () => { - return client.getBookmarks({ limit: 1 }).catch(() => { - timelines = { ...timelines, bookmark: false } - }) - } - const local = async () => { - return client.getLocalTimeline({ limit: 1 }).catch(() => { - timelines = { ...timelines, local: false } - }) - } - const pub = async () => { - return client.getPublicTimeline({ limit: 1 }).catch(() => { - timelines = { ...timelines, public: false } - }) - } - const tag = async () => { - return client.getTagTimeline('whalebird', { limit: 1 }).catch(() => { - timelines = { ...timelines, tag: false } - }) - } - await Promise.all([notification(), direct(), favourite(), bookmark(), local(), pub(), tag()]) - - return timelines -} - -export default confirm diff --git a/src/main/websocket.ts b/src/main/websocket.ts index 8d862bdd..0cbf093b 100644 --- a/src/main/websocket.ts +++ b/src/main/websocket.ts @@ -1,16 +1,18 @@ import generator, { MegalodonInterface, WebSocketInterface, Entity, ProxyConfig } from 'megalodon' import log from 'electron-log' import { LocalAccount } from '~/src/types/localAccount' +import { LocalServer } from '~src/types/localServer' const StreamingURL = async ( sns: 'mastodon' | 'pleroma' | 'misskey', account: LocalAccount, + server: LocalServer, proxy: ProxyConfig | false ): Promise => { if (!account.accessToken) { throw new Error('access token is empty') } - const client = generator(sns, account.baseURL, account.accessToken, 'Whalebird', proxy) + const client = generator(sns, server.baseURL, account.accessToken, 'Whalebird', proxy) const res = await client.getInstance() return res.data.urls.streaming_api } diff --git a/src/renderer/components/Authorize.vue b/src/renderer/components/Authorize.vue deleted file mode 100644 index be1cc12e..00000000 --- a/src/renderer/components/Authorize.vue +++ /dev/null @@ -1,191 +0,0 @@ - - - - - diff --git a/src/renderer/components/GlobalHeader.vue b/src/renderer/components/GlobalHeader.vue index 0eab5af5..08bb31f3 100644 --- a/src/renderer/components/GlobalHeader.vue +++ b/src/renderer/components/GlobalHeader.vue @@ -12,16 +12,15 @@ role="menubar" > - - - - {{ account.domain }} + + + {{ server.domain }}