import { Application } from 'egg' ; import { existsSync , readdirSync , statSync , readFileSync , mkdirSync , writeFileSync , copyFileSync } from 'fs' ; import { join , basename , dirname } from 'path' ; import { waitUntil , BotLogger , loggerWrapper } from '../../basic/Utils' ; import { cloneDeep } from 'lodash' ; import { getConfigMeta } from '../../config-generator/decorators' ; import { ComponentConfig , ComponentRemoteConfig } from './HostingConfigBase' ; import execa from 'execa' ; export class ComponentService { private app : Application ; private logger : BotLogger ; private config : ComponentConfig ; private componentLoaded : boolean ; private configStructure : any ; private components : any ; constructor ( hostName , config , app ) { this . app = app ; this . logger = loggerWrapper ( app . logger , `[host-${ hostName }-component-service]` ) ; this . config = config ; this . componentLoaded = false ; this . components = { } ; this . configStructure = { } ; } public getLatestConfigStructure ( ) { return this . configStructure ; } public getConfigStructureByVersion ( name , version ) { if ( this . components [ name ] && this . components [ name ] [ version ] ) return this . configStructure [ name ] [ version ] ; return { } ; } public getComponentLoaded ( ) { return this . componentLoaded ; } public getComponents ( ) { return this . components ; } public setComponents ( comp ) { this . components = comp ; } /**
     * Load components
     * Never overlay previous loaded component.
     * If different components have the same name, those after will not be loaded.
     */ public async loadComponents ( ) { try { const historyPath = join ( this . app . baseDir , '.component-history' ) ; if ( ! existsSync ( historyPath ) || ! existsSync ( join ( historyPath , '/config-generator' ) ) || ! existsSync ( join ( historyPath , '/basic' ) ) ) { mkdirSync ( join ( historyPath , '/config-generator' ) , { recursive : true } ) ; mkdirSync ( join ( historyPath , '/basic' ) , { recursive : true } ) ; } copyFileSync ( join ( this . app . baseDir , 'app/config-generator/decorators.ts' ) , join ( historyPath , 'config-generator/decorators.ts' ) ) ; copyFileSync ( join ( this . app . baseDir , 'app/basic/DataTypes.ts' ) , join ( historyPath , '/basic/DataTypes.ts' ) ) ; if ( this . config . file ) { this . logger . info ( 'Gonna load component from local file.' ) ; const basePath = join ( this . app . baseDir , this . config . file . basePath ) ; if ( ! existsSync ( basePath ) ) { this . logger . info ( `Component directory path not exists, path=${ basePath }` ) ; return undefined ; } const basePathInfo = statSync ( basePath ) ; if ( ! basePathInfo . isDirectory ( ) ) { this . logger . error ( `Component base path is not a directory, path=${ basePath }` ) ; return undefined ; } const componentsDir = readdirSync ( basePath ) ; for ( const index in componentsDir ) { if ( this . components [ index ] ) { continue ; } await this . loadComponent ( join ( basePath , componentsDir [ index ] ) , componentsDir [ index ] , this . config . file ) ; } } if ( this . config . remote ) { this . logger . info ( 'Gonna load component from remote file.' ) ; const githubRemoteRepo = this . config . remote . filter ( c => c . type === 'github' ) ; for ( const grr of githubRemoteRepo ) { const s = grr . url . split ( '/' ) ; const repo = `${ s [ 3 ] }/${ s [ 4 ] }` ; const compResponse = await fetch ( `https://api.github.com/repos/${ repo }/contents/${ grr . basePath }` , { headers : grr . token ? { Authorization : `bearer ${ grr . token }` } : { } } ) ; if ( ! compResponse . ok ) { throw { status : compResponse . status , body : compResponse , } ; } const compList = await compResponse . json ( ) ; for ( const comp of compList ) { if ( this . components [ comp . name ] ) { continue ; } await this . loadComponentFromGitHub ( repo , comp . path , comp . name , grr ) ; } } const basePath = join ( this . app . baseDir , '.component-history' ) ; if ( ! existsSync ( basePath ) ) { this . logger . info ( `Component directory path not exists, path=${ basePath }` ) ; return undefined ; } const basePathInfo = statSync ( basePath ) ; if ( ! basePathInfo . isDirectory ( ) ) { this . logger . error ( `Component base path is not a directory, path=${ basePath }` ) ; return undefined ; } const componentsDir = readdirSync ( basePath ) ; for ( const index in componentsDir ) { if ( this . components [ componentsDir [ index ] ] || componentsDir [ index ] === 'config-generator' || componentsDir [ index ] === 'basic' ) { continue ; } const versionsDir = readdirSync ( join ( basePath , componentsDir [ index ] ) ) ; for ( const vi in versionsDir ) { await this . loadComponent ( join ( basePath , componentsDir [ index ] , versionsDir [ vi ] ) , componentsDir [ index ] , this . config . remote [ 0 ] ) ; } } } if ( this . config . git ) { this . logger . info ( 'Gonna load component from remote git.' ) ; const historyPath = join ( this . app . baseDir , '.component-history' ) ; const repoPath = join ( this . app . baseDir , '.component-repo' ) ; if ( ! existsSync ( repoPath ) ) { mkdirSync ( repoPath ) ; } for ( const g of this . config . git ) { const s = g . url . split ( '/' ) ; const repoName = `${ s [ s . length - 1 ] . split ( '.' ) [ 0 ] }` ; if ( ! existsSync ( join ( repoPath , repoName ) ) ) { await execa ( 'git' , [ 'clone' , g . url , ] , { cwd : repoPath } ) ; } else { await execa ( 'git' , [ 'pull' , ] , { cwd : join ( repoPath , repoName ) } ) ; } const basePath = join ( repoPath , repoName , this . config . file . basePath ) ; if ( ! existsSync ( basePath ) ) { this . logger . info ( `Component directory path not exists, path=${ basePath }` ) ; return undefined ; } const basePathInfo = statSync ( basePath ) ; if ( ! basePathInfo . isDirectory ( ) ) { this . logger . error ( `Component base path is not a directory, path=${ basePath }` ) ; return undefined ; } const componentsDir = readdirSync ( basePath ) ; for ( const index in componentsDir ) { if ( this . components [ index ] ) { continue ; } await this . loadComponentFromGit ( join ( basePath , componentsDir [ index ] ) , componentsDir [ index ] , g ) ; } } const componentsDir = readdirSync ( historyPath ) ; for ( const index in componentsDir ) { if ( this . components [ componentsDir [ index ] ] || componentsDir [ index ] === 'config-generator' || componentsDir [ index ] === 'basic' ) { continue ; } const versionsDir = readdirSync ( join ( historyPath , componentsDir [ index ] ) ) ; for ( const vi in versionsDir ) { await this . loadComponent ( join ( historyPath , componentsDir [ index ] , versionsDir [ vi ] ) , componentsDir [ index ] , this . config . git [ 0 ] ) ; } } } this . componentLoaded = true ; return this . components ; } catch ( e ) { this . logger . error ( `Error while load components, e=${ JSON . stringify ( e ) }` ) ; } } private async loadComponent ( path , name , compSourceConfig ) { try { this . logger . info ( `Gonna load component under ${ name }` ) ; const pathInfo = statSync ( path ) ; if ( ! pathInfo . isDirectory ( ) ) { this . logger . error ( `Component ${ name } path is not a directory.` ) ; return ; } const versionPath = join ( path , compSourceConfig . versionPath ) ; if ( ! existsSync ( versionPath ) ) { this . logger . error ( `Component ${ name } version file not exist.` ) ; return ; } const version = JSON . parse ( readFileSync ( versionPath ) . toString ( ) ) ; if ( isNaN ( version . version ) || isNaN ( version . maxAPIVersion ) || isNaN ( version . minAPIVersion ) ) { this . logger . error ( `Component ${ name } version version is NaN.` ) ; return ; } const componentBody = { name , version , config : null , configStructure : null , luaScript : '' , } ; let constructor ; try { constructor = await import ( join ( path , compSourceConfig . configModule ) ) ; if ( constructor . default ) { constructor = constructor . default ; const defaultConfig = new constructor ( ) ; const meta = getConfigMeta ( constructor ) ; if ( meta ) { componentBody . configStructure = meta ; componentBody . config = this . genDefaultConfigByMeta ( defaultConfig , meta ) ; } else { this . logger . error ( `Config meta miss on config ${ name }` ) ; } } } catch ( e ) { if ( e . code === 'MODULE_NOT_FOUND' ) return ; this . logger . warn ( `Error loading config of ${ name }, e=${ JSON . stringify ( e ) }` ) ; } try { const luaScriptPath = join ( path , compSourceConfig . luaModule ) ; if ( existsSync ( luaScriptPath ) ) { componentBody . luaScript = readFileSync ( luaScriptPath , 'utf8' ) ; } } catch ( e ) { this . logger . warn ( `Error loading lua of ${ name }, e=${ JSON . stringify ( e ) }` ) ; } if ( ! this . components [ name ] ) { this . components [ name ] = { } ; this . configStructure [ name ] = { } ; } this . components [ name ] [ version . version ] = componentBody ; this . configStructure [ name ] [ version . version ] = componentBody . configStructure ; } catch ( e ) { this . logger . error ( `Error while load component ${ name }, e=${ JSON . stringify ( e ) }` ) ; } } private async loadComponentFromGitHub ( repo , path , name , remoteConfig ) { try { const compHistoryPath = join ( this . app . baseDir , '.component-history/' , name ) ; if ( ! existsSync ( compHistoryPath ) ) { mkdirSync ( compHistoryPath ) ; } const commitsResponse = await fetch ( `https://api.github.com/repos/${ repo }/commits?path=${ path }` , { headers : remoteConfig . token ? { Authorization : `bearer ${ remoteConfig . token }` } : { } } ) ; if ( ! commitsResponse . ok ) { throw { status : commitsResponse . status , body : commitsResponse , } ; } const commitsJson = await commitsResponse . json ( ) ; const versionsJson = commitsJson . map ( commit => ( { sha : commit . sha , } ) ) ; await Promise . all ( versionsJson . map ( async ( commit ) => { try { commit . content = JSON . parse ( await getContent ( repo , commit . sha , join ( path , remoteConfig . versionPath ) , remoteConfig . token ) ) ; } catch ( e ) { commit . content = undefined ; } } ) ) ; const versionMap = new Map < number , string > ( ) ; for ( const v of versionsJson ) { if ( ! v . content ) { continue ; } if ( ! versionMap . get ( v . content . version ) ) { const compVersionPath = join ( compHistoryPath , '/' + v . content . version ) ; if ( existsSync ( compVersionPath ) ) { continue ; } versionMap . set ( v . content . version , v . sha ) ; mkdirSync ( join ( compHistoryPath , '/' + v . content . version ) ) ; writeFileSync ( compHistoryPath + '/' + v . content . version + '/version.json' , JSON . stringify ( v . content ) ) ; } } const versionList = [ ] ; versionMap . forEach ( ( v , k ) => { versionList . push ( [ k , v ] ) ; } ) ; await Promise . all ( versionList . map ( async ( val ) => { const compConfig = await getContent ( repo , val [ 1 ] , join ( path , '/config.ts' ) , remoteConfig . token ) ; const compConfigStructure = await getContent ( repo , val [ 1 ] , join ( path , '/defaultConfig.ts' ) , remoteConfig . token ) ; const compLuaScript = await getContent ( repo , val [ 1 ] , join ( path , remoteConfig . luaModule ) , remoteConfig . token ) ; const compVersionPath = join ( compHistoryPath , '/' + val [ 0 ] ) ; writeFileSync ( join ( compVersionPath , '/config.ts' ) , compConfig ) ; writeFileSync ( join ( compVersionPath , '/defaultConfig.ts' ) , compConfigStructure ) ; writeFileSync ( join ( compVersionPath , '/index.lua' ) , compLuaScript ) ; } ) ) ; } catch ( e ) { this . logger . error ( `Error while save component ${ name } local files, e=${ JSON . stringify ( e ) }` ) ; } async function getContent ( repo , sha , path , token ) { const contentResponse = await fetch ( `https://api.github.com/repos/${ repo }/contents${ path }?ref=${ sha }` , { headers : token ? { Authorization : `bearer ${ token }` } : { } } ) ; if ( contentResponse . status === 404 ) { return '' ; } const contentJson = await contentResponse . json ( ) ; if ( ! contentResponse . ok ) { throw { status : contentResponse . status , body : contentJson , } ; } const content = Buffer . from ( contentJson . content , 'base64' ) . toString ( 'ascii' ) ; return content ; } } param path param name param gitConfig private async loadComponentFromGit ( path , name , gitConfig ) { try { const compHistoryPath = join ( this . app . baseDir , '.component-history/' , name ) ; if ( ! existsSync ( compHistoryPath ) ) { mkdirSync ( compHistoryPath ) ; } const { stdout } = await execa ( 'git' , [ 'log' , '--pretty=format:{"sha":"%h"},' , '--' , basename ( path ) , ] , { cwd : dirname ( path ) } ) ; const versionsJson = JSON . parse ( `[${ stdout . slice ( 0 , - 1 ) }]` ) ; await Promise . all ( versionsJson . map ( async ( commit ) => { try { commit . content = JSON . parse ( await getContent ( commit . sha , join ( path , gitConfig . versionPath ) ) ) ; } catch ( e ) { commit . content = undefined ; } } ) ) ; const versionMap = new Map < number , string > ( ) ; for ( const v of versionsJson ) { if ( ! v . content ) { continue ; } if ( ! versionMap . get ( v . content . version ) ) { const compVersionPath = join ( compHistoryPath , '/' + v . content . version ) ; if ( existsSync ( compVersionPath ) ) { continue ; } versionMap . set ( v . content . version , v . sha ) ; mkdirSync ( join ( compHistoryPath , '/' + v . content . version ) ) ; writeFileSync ( compHistoryPath + '/' + v . content . version + '/version.json' , JSON . stringify ( v . content ) ) ; } } const versionList = [ ] ; versionMap . forEach ( ( v , k ) => { versionList . push ( [ k , v ] ) ; } ) ; await Promise . all ( versionList . map ( async ( val ) => { const compConfig = await getContent ( val [ 1 ] , join ( path , '/config.ts' ) ) ; const compConfigStructure = await getContent ( val [ 1 ] , join ( path , '/defaultConfig.ts' ) ) ; const compLuaScript = await getContent ( val [ 1 ] , join ( path , gitConfig . luaModule ) ) ; const compVersionPath = join ( compHistoryPath , '/' + val [ 0 ] ) ; writeFileSync ( join ( compVersionPath , '/config.ts' ) , compConfig ) ; writeFileSync ( join ( compVersionPath , '/defaultConfig.ts' ) , compConfigStructure ) ; writeFileSync ( join ( compVersionPath , '/index.lua' ) , compLuaScript ) ; } ) ) ; } catch ( e ) { this . logger . error ( `Error while extract component ${ name } local files from git, e=${ e }` ) ; } param sha param path async function getContent ( sha , path ) { try { const { stdout } = await execa ( 'git' , [ 'show' , `${ sha }:./${ basename ( path ) }` ] , { cwd : dirname ( path ) } ) ; return stdout ; } catch ( e ) { return '' ; } } } private genDefaultConfigByMeta ( config , meta ) { if ( ! meta . properties ) return null ; meta . properties . forEach ( p => { if ( p . type !== 'object' ) { config [ p . name ] = p . defaultValue ; } else { config [ p . name ] = this . genDefaultConfigByMeta ( { } , p ) ; } } ) ; return config ; } public async getDefaultConfig ( config ) { if ( ! config ) return { } ; await waitUntil ( ( ) => this . componentLoaded , { interval : 500 } ) ; const res = { } ; Object . keys ( config ) . forEach ( compName => { const component = this . components [ compName ] ; if ( component === undefined ) return ; const compBody = component [ config [ compName ] . version ] ; if ( compBody === undefined || ! compBody . config ) return ; res [ compName ] = cloneDeep ( compBody . config ) ; } ) ; return res ; } public async getDefaultLuaScript ( config ) { if ( ! config ) return { } ; await waitUntil ( ( ) => this . componentLoaded , { interval : 500 } ) ; const res = { } ; Object . keys ( config ) . forEach ( compName => { const component = this . components [ compName ] ; if ( component === undefined ) return ; const compBody = component [ config [ compName ] . version ] ; if ( compBody === undefined || ! compBody . luaScript ) return ; res [ compName ] = cloneDeep ( compBody . luaScript ) ; } ) ; return res ; } } export interface Component { name : string ; version : { version : number ; minAPIVersion : number ; maxAPIVersion : number ; } ; config : any ; configStructure : any ; luaScript : string ; }