import { isEmpty } from 'lodash' ; import generator , { detector , Entity , ProxyConfig } from 'megalodon' ; import Datastore from 'nedb' ; import log from 'electron-log' ; import { LocalAccount } from '~/src/types/localAccount' ; export default class Account { private db : Datastore ; constructor ( db ) { this . db = db ; } async initialize ( ) { await this . cleanup ( ) ; await this . reorder ( ) ; await this . updateUnique ( ) ; } updateUnique ( ) { return new Promise ( ( resolve , reject ) => { this . db . removeIndex ( 'order' , err => { if ( err ) reject ( err ) ; 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 ) { return new Promise ( ( resolve , reject ) => { this . db . insert < LocalAccount > ( 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 ) { return new Promise ( ( resolve , reject ) => { this . db . find < LocalAccount > ( { } ) . 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 ( ) { return new Promise ( ( resolve , reject ) => { this . db . find < LocalAccount > ( { $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 ) ; } ) ; } ) ; } async lastAccount ( ) { const accounts = await this . listAllAccounts ( - 1 ) ; return accounts [ 0 ] ; } getAccount ( id ) { return new Promise ( ( resolve , reject ) => { this . db . findOne < LocalAccount > ( { _id : id } , ( err , doc ) => { if ( err ) return reject ( err ) ; if ( isEmpty ( doc ) ) return reject ( new EmptyRecordError ( 'empty' ) ) ; resolve ( doc ) ; } ) ; } ) ; } searchAccount ( obj ) { return new Promise ( ( resolve , reject ) => { this . db . findOne < LocalAccount > ( obj , ( err , doc ) => { if ( err ) return reject ( err ) ; if ( isEmpty ( doc ) ) return reject ( new EmptyRecordError ( 'empty' ) ) ; resolve ( doc ) ; } ) ; } ) ; } searchAccounts ( obj , order = 1 ) { return new Promise ( ( resolve , reject ) => { this . db . find < LocalAccount > ( obj ) . sort ( { order : order } ) . exec ( ( err , docs ) => { if ( err ) return reject ( err ) ; resolve ( docs ) ; } ) ; } ) ; } updateAccount ( id , obj ) { 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 < LocalAccount > ( { _id : id } , ( err , doc ) => { if ( err ) return reject ( err ) ; if ( isEmpty ( doc ) ) return reject ( new EmptyRecordError ( 'empty' ) ) ; resolve ( doc ) ; } ) ; } ) ; } ) ; } removeAccount ( id ) { return new Promise ( ( resolve , reject ) => { this . db . remove ( { _id : id } , { multi : true } , ( err , _numRemoved ) => { if ( err ) return reject ( err ) ; resolve ( id ) ; } ) ; } ) ; } removeAll ( ) { return new Promise ( ( resolve , reject ) => { this . db . remove ( { } , { multi : true } , ( err , numRemoved ) => { if ( err ) return reject ( err ) ; resolve ( numRemoved ) ; } ) ; } ) ; } async forwardAccount ( ac ) { 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 ; await this . updateAccount ( previousAccount . _id ! , Object . assign ( previousAccount , { order : - 1 } ) ) ; const updated = await this . updateAccount ( ac . _id ! , Object . assign ( ac , { order : previousOrder } ) ) ; await this . updateAccount ( previousAccount . _id ! , Object . assign ( previousAccount , { order : targetOrder } ) ) ; return updated ; } async backwardAccount ( ac ) { 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 ; await this . updateAccount ( nextAccount . _id ! , Object . assign ( nextAccount , { order : - 1 } ) ) ; const updated = await this . updateAccount ( ac . _id ! , Object . assign ( ac , { order : nextOrder } ) ) ; await this . updateAccount ( nextAccount . _id ! , Object . assign ( nextAccount , { order : targetOrder } ) ) ; return updated ; } async refreshAccounts ( proxy ) { 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 ; } param { LocalAccount } account return { LocalAccount } async refresh ( account , proxy ) { 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 ( ! 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 ) ; } async fetchAccount ( sns , account , accessToken , proxy ) { 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 { } class DuplicateRecordError extends Error { } class RefreshTokenDoesNotExist extends Error { }