Merge branch 'api' into dev

This commit is contained in:
lyswhut
2021-03-07 12:01:59 +08:00
parent 2bc669c828
commit e0de81230e
37 changed files with 3112 additions and 1598 deletions
+1
View File
@@ -2,3 +2,4 @@ require('./appMenu')
require('./winLyric')
require('./tray')
require('./hotKey')
require('./userApi')
+2
View File
@@ -0,0 +1,2 @@
exports.userApis = []
+11
View File
@@ -0,0 +1,11 @@
const { EventEmitter } = require('events')
const USER_API_EVENT_NAME = require('./name')
class UserApi extends EventEmitter {
status(info) {
this.emit(USER_API_EVENT_NAME.status, info)
}
}
module.exports = UserApi
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
status: 'status',
}
+44
View File
@@ -0,0 +1,44 @@
const Event = require('./event/event')
const eventNames = require('./event/name')
const { closeWindow } = require('./main')
const { getUserApis, importApi, removeApi } = require('./utils')
const { request, cancelRequest, getStatus, loadApi } = require('./rendererEvent/rendererEvent')
// const { getApiList, importApi, removeApi, setApi, getStatus, request, eventNames }
let userApiId
exports.Event = Event
exports.eventNames = eventNames
exports.getApiList = getUserApis
exports.importApi = script => {
return {
apiInfo: importApi(script),
apiList: getUserApis(),
}
}
exports.request = request
exports.cancelRequest = cancelRequest
exports.getStatus = getStatus
exports.removeApi = async ids => {
if (userApiId && ids.includes(userApiId)) {
userApiId = null
await closeWindow()
}
removeApi(ids)
return getUserApis()
}
exports.setApi = async id => {
if (userApiId) {
userApiId = null
await closeWindow()
}
const apiList = getUserApis()
if (!apiList.some(a => a.id === id)) return
userApiId = id
await loadApi(id)
}
+88
View File
@@ -0,0 +1,88 @@
const { BrowserWindow } = require('electron')
const fs = require('fs')
const path = require('path')
// eslint-disable-next-line no-undef
const dir = global.isDev ? __userApi : path.join(__dirname, 'userApi')
const wait = time => new Promise(resolve => setTimeout(() => resolve(), time))
let html = ''
fs.readFile(path.join(dir, 'renderer/user-api.html'), 'utf8', (err, data) => {
if (err) throw new Error('api html read failed, info: ' + err.message)
html = data
})
const denyEvents = [
'new-window',
'will-navigate',
'will-redirect',
'will-attach-webview',
'will-prevent-unload',
'media-started-playing',
]
const winEvent = win => {
win.on('closed', () => {
win = global.modules.userApiWindow = null
})
}
exports.createWindow = async userApi => {
if (global.modules.userApiWindow) return
while (true) {
if (html) break
await wait(100)
}
/**
* Initial window options
*/
global.modules.userApiWindow = new BrowserWindow({
enableRemoteModule: false,
resizable: false,
minimizable: false,
maximizable: false,
fullscreenable: false,
show: false,
webPreferences: {
contextIsolation: true,
worldSafeExecuteJavaScript: true,
nodeIntegration: false,
preload: path.join(dir, 'renderer/preload.js'),
},
})
for (const eventName of denyEvents) {
global.modules.userApiWindow.webContents.on(eventName, event => {
event.preventDefault()
})
}
global.modules.userApiWindow.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => {
callback(false)
})
winEvent(global.modules.userApiWindow)
// console.log(html.replace('</body>', `<script>${userApi.script}</script></body>`))
const randomNum = Math.random().toString().substring(2, 10)
global.modules.userApiWindow.loadURL(
'data:text/html;charset=UTF-8,' + encodeURIComponent(html
.replace('<meta http-equiv="Content-Security-Policy" content="default-src \'none\'">',
`<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'nonce-${randomNum}';">`)
.replace('</body>', `<script nonce="${randomNum}">${userApi.script}</script></body>`)))
// global.modules.userApiWindow.loadFile(path.join(dir, 'renderer/user-api.html'))
// global.modules.userApiWindow.webContents.openDevTools()
}
exports.closeWindow = async() => {
if (!global.modules.userApiWindow) return
await Promise.all([
global.modules.userApiWindow.webContents.session.clearAuthCache(),
global.modules.userApiWindow.webContents.session.clearStorageData(),
global.modules.userApiWindow.webContents.session.clearCache(),
])
global.modules.userApiWindow.destroy()
global.modules.userApiWindow = null
}
@@ -0,0 +1,227 @@
const { contextBridge, ipcRenderer } = require('electron')
const needle = require('needle')
const { createCipheriv, publicEncrypt, constants, randomBytes, createHash } = require('crypto')
const USER_API_RENDERER_EVENT_NAME = require('../rendererEvent/name')
const sendMessage = (action, status, data, message) => {
ipcRenderer.send(action, { status, data, message })
}
let isInitedApi = false
const EVENT_NAMES = {
request: 'request',
inited: 'inited',
}
const eventNames = Object.values(EVENT_NAMES)
const events = {
request: null,
}
const allSources = ['kw', 'kg', 'tx', 'wy', 'mg']
const supportQualitys = {
kw: ['128k', '320k', 'flac'],
kg: ['128k', '320k', 'flac'],
tx: ['128k', '320k', 'flac'],
wy: ['128k', '320k', 'flac'],
mg: ['128k', '320k', 'flac'],
}
const supportActions = {
kw: ['musicUrl'],
kg: ['musicUrl'],
tx: ['musicUrl'],
wy: ['musicUrl'],
mg: ['musicUrl'],
xm: ['musicUrl'],
}
const handleRequest = (context, { requestKey, data }) => {
// console.log(data)
if (!events.request) return sendMessage(USER_API_RENDERER_EVENT_NAME.response, false, { requestKey }, 'Request event is not defined')
try {
events.request.call(context, { source: data.source, action: data.action, info: data.info }).then(response => {
let sendData = {
requestKey,
}
switch (data.action) {
case 'musicUrl':
sendData.result = {
source: data.source,
action: data.action,
data: {
type: data.info.type,
url: response,
},
}
break
}
sendMessage(USER_API_RENDERER_EVENT_NAME.response, true, sendData)
}).catch(err => {
sendMessage(USER_API_RENDERER_EVENT_NAME.response, false, { requestKey }, err.message)
})
} catch (err) {
sendMessage(USER_API_RENDERER_EVENT_NAME.response, false, { requestKey }, err.message)
}
}
/**
*
* @param {*} context
* @param {*} info {
* status: true,
* message: 'xxx',
* sources: {
* kw: ['128k', '320k', 'flac'],
* kg: ['128k', '320k', 'flac'],
* tx: ['128k', '320k', 'flac'],
* wy: ['128k', '320k', 'flac'],
* mg: ['128k', '320k', 'flac'],
* }
* }
*/
const handleInit = (context, info) => {
if (!info) {
sendMessage(USER_API_RENDERER_EVENT_NAME.init, false, null, 'Init failed')
// sendMessage(USER_API_RENDERER_EVENT_NAME.init, false, null, typeof info.message === 'string' ? info.message.substring(0, 100) : '')
return
}
if (info.openDevTools === true) {
sendMessage(USER_API_RENDERER_EVENT_NAME.openDevTools)
}
if (!info.status) {
sendMessage(USER_API_RENDERER_EVENT_NAME.init, false, null, 'Init failed')
// sendMessage(USER_API_RENDERER_EVENT_NAME.init, false, null, typeof info.message === 'string' ? info.message.substring(0, 100) : '')
return
}
const sourceInfo = {
sources: {},
}
try {
for (const source of allSources) {
const userSource = info.sources[source]
if (!userSource || userSource.type !== 'music') continue
const qualitys = supportQualitys[source]
const actions = supportActions[source]
sourceInfo.sources[source] = {
type: 'music',
actions: actions.filter(a => userSource.actions.includes(a)),
qualitys: qualitys.filter(q => userSource.qualitys.includes(q)),
}
}
} catch (error) {
console.log(error)
sendMessage(USER_API_RENDERER_EVENT_NAME.init, false, null, error.message)
return
}
sendMessage(USER_API_RENDERER_EVENT_NAME.init, true, sourceInfo)
ipcRenderer.on(USER_API_RENDERER_EVENT_NAME.request, (event, data) => {
handleRequest(context, data)
})
}
contextBridge.exposeInMainWorld('lx', {
EVENT_NAMES,
request(url, { method = 'get', timeout, headers, body, form, formData }, callback) {
let options = { headers }
let data
if (body) {
data = body
} else if (form) {
data = form
// data.content_type = 'application/x-www-form-urlencoded'
options.json = false
} else if (formData) {
data = formData
// data.content_type = 'multipart/form-data'
options.json = false
}
options.response_timeout = timeout
let request = needle.request(method, url, data, options, (err, resp, body) => {
if (!err) {
body = resp.body = resp.raw.toString()
try {
resp.body = JSON.parse(resp.body)
} catch (_) {}
body = resp.body
}
callback(err, {
statusCode: resp.statusCode,
statusMessage: resp.statusMessage,
headers: resp.headers,
bytes: resp.bytes,
raw: resp.raw,
body: body,
}, body)
}).request
return () => {
if (!request.aborted) request.abort()
request = null
}
},
send(eventName, data) {
return new Promise((resolve, reject) => {
if (!eventNames.includes(eventName)) return reject(new Error('The event is not supported: ' + eventName))
switch (eventName) {
case EVENT_NAMES.inited:
if (isInitedApi) return
isInitedApi = true
handleInit(this, data)
break
default:
resolve(new Error('Unknown event name: ' + eventName))
}
})
},
on(eventName, handler) {
if (!eventNames.includes(eventName)) return Promise.reject(new Error('The event is not supported: ' + eventName))
switch (eventName) {
case EVENT_NAMES.request:
events.request = handler
break
}
},
utils: {
crypto: {
aesEncrypt(buffer, mode, key, iv) {
const cipher = createCipheriv('aes-128-' + mode, key, iv)
return Buffer.concat([cipher.update(buffer), cipher.final()])
},
rsaEncrypt(buffer, key) {
buffer = Buffer.concat([Buffer.alloc(128 - buffer.length), buffer])
return publicEncrypt({ key: key, padding: constants.RSA_NO_PADDING }, buffer)
},
randomBytes(size) {
return randomBytes(size)
},
md5(str) {
return createHash('md5').update(str).digest('hex')
},
},
buffer: {
from(...args) {
return Buffer.from(...args)
},
},
},
// removeEvent(eventName, handler) {
// if (!eventNames.includes(eventName)) return Promise.reject(new Error('The event is not supported: ' + eventName))
// let handlers
// switch (eventName) {
// case EVENT_NAMES.request:
// handlers = events.request
// break
// }
// for (let index = 0; index < handlers.length; index++) {
// if (handlers[index] === handler) {
// handlers.splice(index, 1)
// break
// }
// }
// },
// removeAllEvents() {
// for (const handlers of Object.values(events)) {
// handlers.splice(0, handlers.length)
// }
// },
})
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
</html>
@@ -0,0 +1,12 @@
const names = {
init: '',
request: '',
response: '',
openDevTools: '',
}
for (const key of Object.keys(names)) {
names[key] = `userApi_${key}`
}
module.exports = names
@@ -0,0 +1,82 @@
const { mainOn, mainSend } = require('@common/ipc')
const USER_API_RENDERER_EVENT_NAME = require('../rendererEvent/name')
const { createWindow } = require('../main')
const { getUserApis } = require('../utils')
let userApi
let status = { status: true }
const requestQueue = new Map()
const timeouts = {}
const handleInit = (event, { status, message, data: apiInfo }) => {
// console.log('inited')
if (!status) {
console.log('init failed:', message)
global.lx_event.userApi.status(status = { status: false, apiInfo: userApi, message })
return
}
global.lx_event.userApi.status(status = { status: true, apiInfo: { ...userApi, sources: apiInfo.sources } })
}
const handleResponse = (event, { status, data: { requestKey, result }, message }) => {
const request = requestQueue.get(requestKey)
if (!request) return
requestQueue.delete(requestKey)
clearTimeout(timeouts[requestKey])
delete timeouts[requestKey]
if (status) {
request[0](result)
} else {
request[1](new Error(message))
}
}
const handleOpenDevTools = () => {
if (global.modules.userApiWindow) {
global.modules.userApiWindow.webContents.openDevTools()
}
}
mainOn(USER_API_RENDERER_EVENT_NAME.init, handleInit)
mainOn(USER_API_RENDERER_EVENT_NAME.response, handleResponse)
mainOn(USER_API_RENDERER_EVENT_NAME.openDevTools, handleOpenDevTools)
exports.loadApi = async apiId => {
if (!apiId) return global.lx_event.userApi.status(status = { status: false, message: 'api id is null' })
userApi = getUserApis().find(api => api.id == apiId)
console.log('load api', userApi.name)
await createWindow(userApi)
// if (!userApi) return global.lx_event.userApi.status(status = { status: false, message: 'api script is not found' })
// if (!global.modules.userApiWindow) {
// global.lx_event.userApi.status(status = { status: false, message: 'user api runtime is not defined' })
// throw new Error('user api window is not defined')
// }
// // const path = require('path')
// // // eslint-disable-next-line no-undef
// // userApi.script = require('fs').readFileSync(path.join(global.isDev ? __userApi : __dirname, 'renderer/test-api.js')).toString()
// console.log('load api', userApi.name)
// mainSend(global.modules.userApiWindow, USER_API_RENDERER_EVENT_NAME.init, { userApi })
}
exports.cancelRequest = requestKey => {
if (!requestQueue.has(requestKey)) return
const request = requestQueue.get(requestKey)
request[1](new Error('Cancel request'))
requestQueue.delete(requestKey)
clearTimeout(timeouts[requestKey])
delete timeouts[requestKey]
}
exports.request = ({ requestKey, data }) => new Promise((resolve, reject) => {
if (!userApi) return reject(new Error('user api is not load'))
// const requestKey = `request__${Math.random().toString().substring(2)}`
timeouts[requestKey] = setTimeout(() => {
exports.cancelRequest(requestKey)
}, 20000)
requestQueue.set(requestKey, [resolve, reject, data])
mainSend(global.modules.userApiWindow, USER_API_RENDERER_EVENT_NAME.request, { requestKey, data })
})
exports.getStatus = () => status
+46
View File
@@ -0,0 +1,46 @@
const { userApis: defaultUserApis } = require('../config')
const Store = require('electron-store')
let userApis
const electronStore_userApi = new Store({
name: 'userApi',
})
exports.getUserApis = () => {
if (userApis) return userApis
userApis = electronStore_userApi.get('userApis')
if (!userApis) {
userApis = defaultUserApis
electronStore_userApi.set('userApis', userApis)
}
return userApis
}
exports.importApi = script => {
let scriptInfo = script.split(/\r?\n/)
let name = scriptInfo[1] || ''
let description = scriptInfo[2] || ''
name = name.startsWith(' * @name ') ? name.replace(' * @name ', '').trim() : `user_api_${new Date().toLocaleString()}`
if (name.length > 10) name = name.substring(0, 10) + '...'
description = description.startsWith(' * @description ') ? description.replace(' * @description ', '').trim() : ''
if (description.length > 20) description = description.substring(0, 20) + '...'
const apiInfo = {
id: `user_api_${Math.random().toString().substring(2, 5)}_${Date.now()}`,
name,
description,
script,
}
userApis.push(apiInfo)
electronStore_userApi.set('userApis', userApis)
return apiInfo
}
exports.removeApi = ids => {
for (let index = userApis.length - 1; index > -1; index--) {
if (ids.includes(userApis[index].id)) {
userApis.splice(index, 1)
ids.splice(index, 1)
}
}
electronStore_userApi.set('userApis', userApis)
}