refs #850 Replace account and auth with typescript

This commit is contained in:
AkiraFukushima 2019-04-16 21:19:15 +09:00
parent b0d95e7fd7
commit 6693efeb3b
3 changed files with 78 additions and 69 deletions

View File

@ -1,8 +1,12 @@
import { isEmpty } from 'lodash' import { isEmpty } from 'lodash'
import Mastodon from 'megalodon' import Mastodon, { Account as RemoteAccount } from 'megalodon'
import Datastore from 'nedb'
import LocalAccount from '~/src/types/localAccount'
export default class Account { export default class Account {
constructor (db) { private db: Datastore
constructor (db: Datastore) {
this.db = db this.db = db
} }
@ -12,7 +16,7 @@ export default class Account {
await this.updateUnique() await this.updateUnique()
} }
updateUnique () { updateUnique (): Promise<{}> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// At first, remove old index. // At first, remove old index.
this.db.removeIndex('order', (err) => { this.db.removeIndex('order', (err) => {
@ -20,7 +24,7 @@ export default class Account {
// Add unique index. // Add unique index.
this.db.ensureIndex({ fieldName: 'order', unique: true, sparse: true }, (err) => { this.db.ensureIndex({ fieldName: 'order', unique: true, sparse: true }, (err) => {
if (err) reject(err) if (err) reject(err)
resolve(null) resolve({})
}) })
}) })
}) })
@ -32,7 +36,7 @@ export default class Account {
async reorder () { async reorder () {
const accounts = await this.listAllAccounts() const accounts = await this.listAllAccounts()
await Promise.all(accounts.map(async (account, index) => { await Promise.all(accounts.map(async (account, index) => {
const update = await this.updateAccount(account._id, Object.assign(account, { order: index + 1 })) const update = await this.updateAccount(account._id!, Object.assign(account, { order: index + 1 }))
return update return update
})) }))
const ordered = await this.listAllAccounts() const ordered = await this.listAllAccounts()
@ -49,16 +53,16 @@ export default class Account {
} }
if (accounts[0].order < 1 || accounts[accounts.length - 1].order > accounts.length) { if (accounts[0].order < 1 || accounts[accounts.length - 1].order > accounts.length) {
await Promise.all(accounts.map(async (element, index) => { await Promise.all(accounts.map(async (element, index) => {
const update = await this.updateAccount(element._id, Object.assign(element, { order: index + 1 })) const update = await this.updateAccount(element._id!, Object.assign(element, { order: index + 1 }))
return update return update
})) }))
} }
return null return null
} }
insertAccount (obj) { insertAccount (localAccount: LocalAccount): Promise<LocalAccount> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.insert(obj, (err, doc) => { this.db.insert<LocalAccount>(localAccount, (err, doc) => {
if (err) return reject(err) if (err) return reject(err)
if (isEmpty(doc)) return reject(new EmptyRecordError('empty')) if (isEmpty(doc)) return reject(new EmptyRecordError('empty'))
resolve(doc) resolve(doc)
@ -69,9 +73,9 @@ export default class Account {
/** /**
* List up all accounts either authenticated or not authenticated. * List up all accounts either authenticated or not authenticated.
*/ */
listAllAccounts (order = 1) { listAllAccounts (order = 1): Promise<Array<LocalAccount>> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.find().sort({ order: order }).exec((err, docs) => { this.db.find<LocalAccount>({}).sort({ order: order }).exec((err, docs) => {
if (err) return reject(err) if (err) return reject(err)
if (isEmpty(docs)) return reject(new EmptyRecordError('empty')) if (isEmpty(docs)) return reject(new EmptyRecordError('empty'))
resolve(docs) resolve(docs)
@ -82,9 +86,9 @@ export default class Account {
/** /**
* List up authenticated accounts. * List up authenticated accounts.
*/ */
listAccounts () { listAccounts (): Promise<Array<LocalAccount>> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.find({ accessToken: { $ne: '' } }).sort({ order: 1 }).exec((err, docs) => { this.db.find<LocalAccount>({ accessToken: { $ne: '' } }).sort({ order: 1 }).exec((err, docs) => {
if (err) return reject(err) if (err) return reject(err)
if (isEmpty(docs)) return reject(new EmptyRecordError('empty')) if (isEmpty(docs)) return reject(new EmptyRecordError('empty'))
resolve(docs) resolve(docs)
@ -93,14 +97,14 @@ export default class Account {
} }
// Get the last account. // Get the last account.
async lastAccount () { async lastAccount (): Promise<LocalAccount> {
const accounts = await this.listAllAccounts(-1) const accounts = await this.listAllAccounts(-1)
return accounts[0] return accounts[0]
} }
getAccount (id) { getAccount (id: string): Promise<LocalAccount> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.findOne( this.db.findOne<LocalAccount>(
{ {
_id: id _id: id
}, },
@ -113,9 +117,9 @@ export default class Account {
}) })
} }
searchAccount (obj) { searchAccount (obj: any): Promise<LocalAccount> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.findOne( this.db.findOne<LocalAccount>(
obj, obj,
(err, doc) => { (err, doc) => {
if (err) return reject(err) if (err) return reject(err)
@ -125,16 +129,16 @@ export default class Account {
}) })
} }
searchAccounts (obj, order = 1) { searchAccounts (obj: any, order = 1): Promise<Array<LocalAccount>> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.find(obj).sort({ order: order }).exec((err, docs) => { this.db.find<LocalAccount>(obj).sort({ order: order }).exec((err, docs) => {
if (err) return reject(err) if (err) return reject(err)
resolve(docs) resolve(docs)
}) })
}) })
} }
updateAccount (id, obj) { updateAccount (id: string, obj: any): Promise<LocalAccount> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.update( this.db.update(
{ {
@ -144,7 +148,7 @@ export default class Account {
{ multi: true }, { multi: true },
(err, _numReplaced) => { (err, _numReplaced) => {
if (err) return reject(err) if (err) return reject(err)
this.db.findOne( this.db.findOne<LocalAccount>(
{ {
_id: id _id: id
}, },
@ -158,7 +162,7 @@ export default class Account {
}) })
} }
removeAccount (id) { removeAccount (id: string): Promise<number> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.remove( this.db.remove(
{ {
@ -173,7 +177,7 @@ export default class Account {
}) })
} }
removeAll () { removeAll (): Promise<number> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.remove( this.db.remove(
{}, {},
@ -186,7 +190,7 @@ export default class Account {
}) })
} }
async forwardAccount (ac) { async forwardAccount (ac: LocalAccount): Promise<LocalAccount | {}> {
// Find account which is the previous of the target account. // Find account which is the previous of the target account.
const accounts = await this.searchAccounts({ order: { $lt: ac.order } }, -1) const accounts = await this.searchAccounts({ order: { $lt: ac.order } }, -1)
.catch((err) => { .catch((err) => {
@ -194,7 +198,7 @@ export default class Account {
return [] return []
}) })
if (accounts.length < 1) { if (accounts.length < 1) {
return null return Promise.resolve({})
} }
const previousAccount = accounts[0] const previousAccount = accounts[0]
const targetOrder = ac.order const targetOrder = ac.order
@ -202,21 +206,21 @@ export default class Account {
// At first, we need to update the previous account with dummy 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. // Because this column is uniqued, so can not update with same order.
await this.updateAccount(previousAccount._id, Object.assign( await this.updateAccount(previousAccount._id!, Object.assign(
previousAccount, previousAccount,
{ {
order: -1 order: -1
} }
)) ))
// Change order of the target account. // Change order of the target account.
const updated = await this.updateAccount(ac._id, Object.assign( const updated = await this.updateAccount(ac._id!, Object.assign(
ac, ac,
{ {
order: previousOrder order: previousOrder
} }
)) ))
// Update the previous account with right order. // Update the previous account with right order.
await this.updateAccount(previousAccount._id, Object.assign( await this.updateAccount(previousAccount._id!, Object.assign(
previousAccount, previousAccount,
{ {
order: targetOrder order: targetOrder
@ -225,7 +229,7 @@ export default class Account {
return updated return updated
} }
async backwardAccount (ac) { async backwardAccount (ac: LocalAccount): Promise<LocalAccount | {}> {
// Find account which is the next of the target account. // Find account which is the next of the target account.
const accounts = await this.searchAccounts({ order: { $gt: ac.order } }, 1) const accounts = await this.searchAccounts({ order: { $gt: ac.order } }, 1)
.catch((err) => { .catch((err) => {
@ -233,7 +237,7 @@ export default class Account {
return [] return []
}) })
if (accounts.length < 1) { if (accounts.length < 1) {
return null return Promise.resolve({})
} }
const nextAccount = accounts[0] const nextAccount = accounts[0]
const targetOrder = ac.order const targetOrder = ac.order
@ -241,21 +245,21 @@ export default class Account {
// At first, we need to update the next account with dummy order. // At first, we need to update the next account with dummy order.
// Because this colum is uniqued, so can not update with same order. // Because this colum is uniqued, so can not update with same order.
await this.updateAccount(nextAccount._id, Object.assign( await this.updateAccount(nextAccount._id!, Object.assign(
nextAccount, nextAccount,
{ {
order: -1 order: -1
} }
)) ))
// Change order of the target account/ // Change order of the target account/
const updated = await this.updateAccount(ac._id, Object.assign( const updated = await this.updateAccount(ac._id!, Object.assign(
ac, ac,
{ {
order: nextOrder order: nextOrder
} }
)) ))
// Update the next account with right order. // Update the next account with right order.
await this.updateAccount(nextAccount._id, Object.assign( await this.updateAccount(nextAccount._id!, Object.assign(
nextAccount, nextAccount,
{ {
order: targetOrder order: targetOrder
@ -264,10 +268,10 @@ export default class Account {
return updated return updated
} }
async refreshAccounts () { async refreshAccounts (): Promise<Array<LocalAccount>> {
const accounts = await this.listAccounts() const accounts = await this.listAccounts()
if (accounts.length < 1) { if (accounts.length < 1) {
return accounts.length return accounts
} }
const results = await Promise.all(accounts.map(async (account) => { const results = await Promise.all(accounts.map(async (account) => {
const refresh = await this.refresh(account) const refresh = await this.refresh(account)
@ -276,51 +280,41 @@ export default class Account {
return results return results
} }
refresh (account) { async refresh (account: LocalAccount): Promise<LocalAccount> {
const client = new Mastodon( const client = new Mastodon(
account.accessToken, account.accessToken!,
account.baseURL + '/api/v1' account.baseURL + '/api/v1'
) )
return client.get('/accounts/verify_credentials') return client.get<RemoteAccount>('/accounts/verify_credentials')
.then(res => { .then(res => {
const json = { const json = {
username: res.data.username, username: res.data.username,
accountId: res.data.id, accountId: res.data.id,
avatar: res.data.avatar avatar: res.data.avatar
} }
return this.updateAccount(account._id, json) return this.updateAccount(account._id!, json)
}) })
} }
// Confirm the access token, and check duplicate // Confirm the access token, and check duplicate
async fetchAccount (account, accessToken) { async fetchAccount (account: LocalAccount, accessToken: string): Promise<RemoteAccount> {
const client = new Mastodon( const client = new Mastodon(
accessToken, accessToken,
account.baseURL + '/api/v1' account.baseURL + '/api/v1'
) )
const data = await client.get('/accounts/verify_credentials') const res = await client.get<RemoteAccount>('/accounts/verify_credentials')
const query = { const query = {
baseURL: account.baseURL, baseURL: account.baseURL,
username: data.username username: res.data.username
} }
const duplicates = await this.searchAccounts(query) const duplicates = await this.searchAccounts(query)
if (duplicates.length > 0) { if (duplicates.length > 0) {
throw new DuplicateRecordError(`${data.username}@${account.baseURL} is duplicated`) throw new DuplicateRecordError(`${res.data.username}@${account.baseURL} is duplicated`)
} }
return data return res.data
} }
} }
class EmptyRecordError extends Error { class EmptyRecordError extends Error {}
constructor (msg) {
super(msg)
this.name = 'EmptyRecordError'
}
}
class DuplicateRecordError extends Error { class DuplicateRecordError extends Error {}
constructor (msg) {
super(msg)
this.name = 'DuplicateRecordError'
}
}

View File

@ -1,11 +1,20 @@
import Mastodon from 'megalodon' import Mastodon from 'megalodon'
import Account from './account'
import LocalAccount from '~src/types/localAccount'
const appName = 'Whalebird' const appName = 'Whalebird'
const appURL = 'https://whalebird.org' const appURL = 'https://whalebird.org'
const scope = 'read write follow' const scope = 'read write follow'
export default class Authentication { export default class Authentication {
constructor (accountDB) { private db: Account
private baseURL: string
private domain: string
private clientId: string
private clientSecret: string
private protocol: 'http' | 'https'
constructor (accountDB: Account) {
this.db = accountDB this.db = accountDB
this.baseURL = '' this.baseURL = ''
this.domain = '' this.domain = ''
@ -14,14 +23,14 @@ export default class Authentication {
this.protocol = 'https' this.protocol = 'https'
} }
setOtherInstance (domain) { setOtherInstance (domain: string) {
this.baseURL = `${this.protocol}://${domain}` this.baseURL = `${this.protocol}://${domain}`
this.domain = domain this.domain = domain
this.clientId = '' this.clientId = ''
this.clientSecret = '' this.clientSecret = ''
} }
async getAuthorizationUrl (domain = 'mastodon.social') { async getAuthorizationUrl (domain = 'mastodon.social'): Promise<string> {
this.setOtherInstance(domain) this.setOtherInstance(domain)
const res = await Mastodon.registerApp( const res = await Mastodon.registerApp(
appName, { appName, {
@ -39,7 +48,7 @@ export default class Authentication {
console.log(err) console.log(err)
return 1 return 1
}) })
const json = { const local: LocalAccount = {
baseURL: this.baseURL, baseURL: this.baseURL,
domain: this.domain, domain: this.domain,
clientId: this.clientId, clientId: this.clientId,
@ -47,15 +56,18 @@ export default class Authentication {
accessToken: '', accessToken: '',
refreshToken: '', refreshToken: '',
username: '', username: '',
accountId: '', accountId: null,
avatar: '', avatar: '',
order: order order: order
} }
await this.db.insertAccount(json) await this.db.insertAccount(local)
if (res.url === null) {
throw new AuthenticationURLError('Can not get url')
}
return res.url return res.url
} }
async getAccessToken (code) { async getAccessToken (code: string): Promise<string> {
const tokenData = await Mastodon.fetchAccessToken(this.clientId, this.clientSecret, code, this.baseURL) const tokenData = await Mastodon.fetchAccessToken(this.clientId, this.clientSecret, code, this.baseURL)
const search = { const search = {
baseURL: this.baseURL, baseURL: this.baseURL,
@ -67,7 +79,7 @@ export default class Authentication {
const accessToken = tokenData.accessToken const accessToken = tokenData.accessToken
const refreshToken = tokenData.refreshToken const refreshToken = tokenData.refreshToken
const data = await this.db.fetchAccount(rec, accessToken) const data = await this.db.fetchAccount(rec, accessToken)
await this.db.updateAccount(rec._id, { await this.db.updateAccount(rec._id!, {
username: data.username, username: data.username,
accountId: data.id, accountId: data.id,
avatar: data.avatar, avatar: data.avatar,
@ -78,3 +90,5 @@ export default class Authentication {
} }
// TODO: Refresh access token when expired // TODO: Refresh access token when expired
} }
class AuthenticationURLError extends Error {}

View File

@ -1,6 +1,6 @@
'use strict' 'use strict'
import { app, ipcMain, shell, Menu, Tray, BrowserWindow, BrowserWindowConstructorOptions, MenuItemConstructorOptions } from 'electron' import { app, ipcMain, shell, Menu, Tray, BrowserWindow, BrowserWindowConstructorOptions, MenuItemConstructorOptions, Event } from 'electron'
import Datastore from 'nedb' import Datastore from 'nedb'
import { isEmpty } from 'lodash' import { isEmpty } from 'lodash'
import log from 'electron-log' import log from 'electron-log'
@ -20,6 +20,7 @@ import Hashtags from './hashtags'
import UnreadNotification from './unread_notification' import UnreadNotification from './unread_notification'
import i18n from '../config/i18n' import i18n from '../config/i18n'
import Language from '../constants/language' import Language from '../constants/language'
import LocalAccount from '~src/types/localAccount'
/** /**
* Context menu * Context menu
@ -99,7 +100,7 @@ async function listAccounts () {
} }
} }
async function changeAccount (account, index) { async function changeAccount (account: LocalAccount, index: number) {
// In MacOS, user can hide the window. // In MacOS, user can hide the window.
// In this time, mainWindow in not exist, so we have to create window. // In this time, mainWindow in not exist, so we have to create window.
if (mainWindow === null) { if (mainWindow === null) {
@ -263,7 +264,7 @@ app.on('activate', () => {
let auth = new Authentication(accountManager) let auth = new Authentication(accountManager)
ipcMain.on('get-auth-url', (event, domain) => { ipcMain.on('get-auth-url', (event: Event, domain: string) => {
auth.getAuthorizationUrl(domain) auth.getAuthorizationUrl(domain)
.then((url) => { .then((url) => {
log.debug(url) log.debug(url)
@ -277,7 +278,7 @@ ipcMain.on('get-auth-url', (event, domain) => {
}) })
}) })
ipcMain.on('get-access-token', (event, code) => { ipcMain.on('get-access-token', (event: Event, code: string) => {
auth.getAccessToken(code) auth.getAccessToken(code)
.then((token) => { .then((token) => {
accountDB.findOne({ accountDB.findOne({
@ -295,7 +296,7 @@ ipcMain.on('get-access-token', (event, code) => {
}) })
// environments // environments
ipcMain.on('get-social-token', (event, _) => { ipcMain.on('get-social-token', (event: Event, _) => {
const token = process.env.SOCIAL_TOKEN const token = process.env.SOCIAL_TOKEN
if (isEmpty(token)) { if (isEmpty(token)) {
return event.sender.send('error-get-social-token', new EmptyTokenError()) return event.sender.send('error-get-social-token', new EmptyTokenError())