diff --git a/.travis.yml b/.travis.yml index e7d137a..23db68a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ script: - node --version - npm --version - npm test - - ./node_modules/.bin/ts-node tests/fixtures/smoke-testing.ts + - ./node_modules/.bin/ts-node --files tests/fixtures/smoke-testing.ts notifications: email: diff --git a/README.md b/README.md index 36a8ded..3a5d8a9 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ We are current DevOps the master branch from the repo to Heroku under the protec You can visit the staging system at ## How to use + use osschat is so easy, just need 4 steps, please refer [How to use](https://github.com/kaiyuanshe/osschat/blob/master/docs/pages/how-to-use.md) ## Meeting Notes @@ -98,6 +99,11 @@ To be added... - Wechaty Puppet Padplus sponsored by: [JuziBot](https://www.juzi.bot) - Heroku Getting Started Template from [Wechaty](https://github.com/wechaty/) +## Links + +- [Scaling your Redux App with ducks](https://www.freecodecamp.org/news/scaling-your-redux-app-with-ducks-6115955638be/) +- [Redux Style Guide](https://redux.js.org/style-guide/style-guide#do-not-put-non-serializable-values-in-state-or-actions) + ## Copyright & License - Code & Docs © 2019-now 开源社 diff --git a/bin/main.ts b/bin/main.ts index ecef59f..35d1d6c 100644 --- a/bin/main.ts +++ b/bin/main.ts @@ -6,7 +6,7 @@ import { log, VERSION, } from '../src/config' -import { getWechaty } from '../src/get-wechaty' +import { getHAWechaty } from '../src/get-wechaty' import { startBot } from '../src/start-bot' import { startFinis } from '../src/start-finis' @@ -40,7 +40,7 @@ export = async (app: Application) => { log.verbose('main', 'main()') - const bot = getWechaty() + const bot = getHAWechaty() await Promise.all([ bot.start(), @@ -48,7 +48,5 @@ export = async (app: Application) => { startFinis(bot), ]) - while (bot.state.on()) { - await new Promise(resolve => setTimeout(resolve, 1000)) - } + await bot.state.ready('off') } diff --git a/package.json b/package.json index 496d582..ee96e7a 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,14 @@ { "name": "osschat", - "version": "0.2.53", + "version": "0.3.6", "description": "Apache OSSChat", "main": "index.js", "engines": { "node": "10" }, "scripts": { + "clean": "shx rm -fr dist/*", + "dist": "npm run build", "build": "tsc", "lint": "npm run lint:es && npm run lint:ts", "lint:ts": "tsc --noEmit", @@ -26,14 +28,18 @@ }, "homepage": "https://github.com/kaiyuanshe/OSSChat#readme", "dependencies": { + "@reduxjs/toolkit": "^1.3.2", + "array-flatten": "^3.0.0", "brolog": "^1.6.5", "commander": "^4.0.1", "finis": "^0.4.3", + "flatten-array": "^1.0.0", "micromatch": "^4.0.2", "moment": "^2.24.0", "node-cache": "^5.1.0", "probot": "^9.8.1", "qrcode-terminal": "^0.12.0", + "redux": "^4.0.5", "smee-client": "^1.1.0", "wechaty": "^0.37.8", "wechaty-puppet-padplus": "^0.5.27" @@ -48,6 +54,7 @@ "@types/read-pkg-up": "^6.0.0", "eslint": "^6.8.0", "express": "^4.17.1", + "shx": "^0.3.2", "tstest": "^0.4.2" }, "git": { diff --git a/src/chatops.ts b/src/chatops.ts index c8bcdfd..47f05dc 100644 --- a/src/chatops.ts +++ b/src/chatops.ts @@ -1,5 +1,4 @@ import { - Wechaty, UrlLink, Message, } from 'wechaty' @@ -12,19 +11,20 @@ import { DEV_ROOM_ID, HEARTBEAT_ROOM_ID, } from './config' +import { HAWechaty } from './ha-wechaty' export class Chatops { private static singleton: Chatops public static instance ( - bot?: Wechaty, + haBot?: HAWechaty, ) { if (!this.singleton) { - if (!bot) { + if (!haBot) { throw new Error('instance need a Wechaty instance to initialize') } - this.singleton = new Chatops(bot) + this.singleton = new Chatops(haBot) } return this.singleton } @@ -38,7 +38,7 @@ export class Chatops { private delayQueueExecutor: DelayQueueExecutor private constructor ( - private bot: Wechaty, + private haBot: HAWechaty, ) { this.delayQueueExecutor = new DelayQueueExecutor(5 * 1000) // set delay period time to 5 seconds } @@ -61,13 +61,17 @@ export class Chatops { ): Promise { log.info('Chatops', 'roomMessage(%s, %s)', roomId, info) - const online = this.bot.logonoff() + const online = this.haBot.logonoff() if (!online) { log.error('Chatops', 'roomMessage() this.bot is offline') return } - const room = this.bot.Room.load(roomId) + const room = await this.haBot.Room.load(roomId) + if (!room) { + log.error('Chatops', 'roomMessage() no bot found in room %s', roomId) + return + } if (typeof info === 'string') { await room.say(info) diff --git a/src/config.ts b/src/config.ts index fd0ed63..50774a3 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,3 +1,4 @@ +/// /** * VERSION */ diff --git a/src/ducks/counter/actions.ts b/src/ducks/counter/actions.ts new file mode 100644 index 0000000..34643ca --- /dev/null +++ b/src/ducks/counter/actions.ts @@ -0,0 +1,6 @@ +import { createAction } from '@reduxjs/toolkit' + +import * as types from './types' + +export const mo = createAction(types.MO) +export const mt = createAction(types.MT) diff --git a/src/ducks/counter/index.ts b/src/ducks/counter/index.ts new file mode 100644 index 0000000..26ff7fd --- /dev/null +++ b/src/ducks/counter/index.ts @@ -0,0 +1,16 @@ +import reducer from './reducers' + +// export { default as counterSelectors } from './selectors' +// export { default as counterOperations } from './operations' + +import * as counterActions from './actions' +import * as counterTypes from './types' +import * as counterSelectors from './selectors' + +export { + counterActions, + counterSelectors, + counterTypes, +} + +export default reducer diff --git a/src/ducks/counter/operations.ts b/src/ducks/counter/operations.ts new file mode 100644 index 0000000..fbb7c7d --- /dev/null +++ b/src/ducks/counter/operations.ts @@ -0,0 +1,14 @@ +// import { + +// } from './actions' + +// // This is a link to an action defined in actions.js. +// export const simpleQuack = actions.quack + +// // This is a thunk which dispatches multiple actions from actions.js +// export const complexQuack = ( distance ) => ( dispatch ) => { +// dispatch( actions.quack( ) ).then( ( ) => { +// dispatch( actions.swim( distance ) ); +// dispatch( /* any action */ ); +// } ); +// } diff --git a/src/ducks/counter/reducers.ts b/src/ducks/counter/reducers.ts new file mode 100644 index 0000000..462fe6e --- /dev/null +++ b/src/ducks/counter/reducers.ts @@ -0,0 +1,31 @@ +import { + createReducer, +} from '@reduxjs/toolkit' + +import * as types from './types' +// import * as actions from './actions' + +const initialState = { + mo: 0, + mt: 0, +} as types.State + +const moReducer = (state: types.State) => ({ + ...state, + mo: state.mo + 1, +}) + +const mtReducer = (state: types.State) => ({ + ...state, + mt: state.mt + 1, +}) + +const counterReducer = createReducer( + initialState, + { + [types.MO]: moReducer, + [types.MT]: mtReducer, + }, +) + +export default counterReducer diff --git a/src/ducks/counter/selectors.ts b/src/ducks/counter/selectors.ts new file mode 100644 index 0000000..9022bcd --- /dev/null +++ b/src/ducks/counter/selectors.ts @@ -0,0 +1,13 @@ +import * as types from './types' + +export function mo ( + state: types.State, +) { + return state.mo +} + +export function mt ( + state: types.State, +) { + return state.mt +} diff --git a/src/ducks/counter/tests.ts b/src/ducks/counter/tests.ts new file mode 100644 index 0000000..d2a076a --- /dev/null +++ b/src/ducks/counter/tests.ts @@ -0,0 +1,16 @@ +// import expect from 'expect.js' +// import reducer from './reducers' +// import actions from './actions' + +// describe('duck reducer', function( ) { +// describe('quack', function( ) { +// const quack = actions.quack( ) +// const initialState = false + +// const result = reducer( initialState, quack ) + +// it( 'should quack', function( ) { +// expect( result ).to.be( true ) +// } ) +// } ) +// } ) diff --git a/src/ducks/counter/types.ts b/src/ducks/counter/types.ts new file mode 100644 index 0000000..12039fd --- /dev/null +++ b/src/ducks/counter/types.ts @@ -0,0 +1,7 @@ +export const MO = 'osschat/wechaty/message/mo' +export const MT = 'osschat/wechaty/message/mt' + +export interface State { + mo: number, + mt: number, +} diff --git a/src/ducks/index.ts b/src/ducks/index.ts new file mode 100644 index 0000000..86fd2dd --- /dev/null +++ b/src/ducks/index.ts @@ -0,0 +1,45 @@ +/** + * Huan(202003): Redux with Ducks + * + * Scaling your Redux App with ducks: + * https://www.freecodecamp.org/news/scaling-your-redux-app-with-ducks-6115955638be/ + * + * Redux Toolkit - Usage With TypeScript: + * https://redux-toolkit.js.org/usage/usage-with-typescript + * + */ +import { combineReducers } from 'redux' +import { configureStore } from '@reduxjs/toolkit' + +import logonoff, { + logonoffActions, + logonoffSelectors, +} from './logonoff' +import counter, { + counterActions, + counterSelectors, +} from './counter' + +export { + logonoffActions, + logonoffSelectors, + + counterActions, + counterSelectors, +} + +const reducer = combineReducers({ + counter, + logonoff, +}) + +export const store = configureStore({ + reducer, +}) + +store.subscribe(() => { + console.info('state:', store.getState()) +}) + +export type RootState = ReturnType +export type AppDispatch = typeof store.dispatch diff --git a/src/ducks/logonoff/actions.ts b/src/ducks/logonoff/actions.ts new file mode 100644 index 0000000..f1f92f3 --- /dev/null +++ b/src/ducks/logonoff/actions.ts @@ -0,0 +1,40 @@ +import { + createAction, +} from '@reduxjs/toolkit' + +import * as types from './types' + +const prepareScan = ( + id: string, + qrcode: string, +) => { + const payload = { + id, + qrcode, + } + return { payload } +} + +const prepareLogin = ( + id: string, + userName: string, +) => { + const payload = { + id, + userName, + } + return { payload } +} + +const prepareLogout = ( + id: string, +) => { + const payload = { + id, + } + return { payload } +} + +export const scan = createAction(types.SCAN, prepareScan) +export const login = createAction(types.LOGIN, prepareLogin) +export const logout = createAction(types.LOGOUT, prepareLogout) diff --git a/src/ducks/logonoff/index.ts b/src/ducks/logonoff/index.ts new file mode 100644 index 0000000..1ab8096 --- /dev/null +++ b/src/ducks/logonoff/index.ts @@ -0,0 +1,14 @@ +import reducer from './reducers' + +import * as logonoffSelectors from './selectors' +// export { default as duckOperations } from './operations' +import * as logonoffTypes from './types' +import * as logonoffActions from './actions' + +export { + logonoffActions, + logonoffSelectors, + logonoffTypes, +} + +export default reducer diff --git a/src/ducks/logonoff/operations.ts b/src/ducks/logonoff/operations.ts new file mode 100644 index 0000000..fbb7c7d --- /dev/null +++ b/src/ducks/logonoff/operations.ts @@ -0,0 +1,14 @@ +// import { + +// } from './actions' + +// // This is a link to an action defined in actions.js. +// export const simpleQuack = actions.quack + +// // This is a thunk which dispatches multiple actions from actions.js +// export const complexQuack = ( distance ) => ( dispatch ) => { +// dispatch( actions.quack( ) ).then( ( ) => { +// dispatch( actions.swim( distance ) ); +// dispatch( /* any action */ ); +// } ); +// } diff --git a/src/ducks/logonoff/reducers.ts b/src/ducks/logonoff/reducers.ts new file mode 100644 index 0000000..45ce8fc --- /dev/null +++ b/src/ducks/logonoff/reducers.ts @@ -0,0 +1,71 @@ +import { + createReducer, + Action, +} from '@reduxjs/toolkit' + +import * as types from './types' +import * as actions from './actions' + +const initialState: types.State = {} + +const scanReducer = (state: types.State, action: Action) => { + if (actions.scan.match(action)) { + return { + ...state, + [action.payload.id]: { + qrcode: action.payload.qrcode, + }, + } + } + return state +} + +const loginReducer = (state: types.State, action: Action) => { + if (actions.login.match(action)) { + return { + ...state, + [action.payload.id]: { + userName: action.payload.userName, + }, + } + } + return state +} + +const logoutReducer = (state: types.State, action: Action) => { + if (actions.logout.match(action)) { + return { + ...state, + [action.payload.id]: {}, + } + } + return state +} + +/** + * https://redux-toolkit.js.org/usage/usage-with-typescript#building-type-safe-reducer-argument-objects + */ +// const logonoffReducder = createReducer(0, builder => +// builder +// .addCase(types.SCAN, (state, action) => { +// // action is inferred correctly here +// console.info(state, action.payload) +// }) +// .addCase(types.LOGIN, (state, action) => { +// console.info(state, action) +// }) +// .addCase(types.LOGOUT, (state, action) => { +// console.info(state, action) +// }) +// ) + +const logonoffReducer = createReducer( + initialState, + { + [types.SCAN] : scanReducer, + [types.LOGIN] : loginReducer, + [types.LOGOUT] : logoutReducer, + }, +) + +export default logonoffReducer diff --git a/src/ducks/logonoff/selectors.ts b/src/ducks/logonoff/selectors.ts new file mode 100644 index 0000000..e071c63 --- /dev/null +++ b/src/ducks/logonoff/selectors.ts @@ -0,0 +1,11 @@ +import * as types from './types' + +export function status ( + state: types.State, + wechatyId: string, +) { + if (wechatyId in state) { + return state[wechatyId] + } + return {} +} diff --git a/src/ducks/logonoff/tests.ts b/src/ducks/logonoff/tests.ts new file mode 100644 index 0000000..d2a076a --- /dev/null +++ b/src/ducks/logonoff/tests.ts @@ -0,0 +1,16 @@ +// import expect from 'expect.js' +// import reducer from './reducers' +// import actions from './actions' + +// describe('duck reducer', function( ) { +// describe('quack', function( ) { +// const quack = actions.quack( ) +// const initialState = false + +// const result = reducer( initialState, quack ) + +// it( 'should quack', function( ) { +// expect( result ).to.be( true ) +// } ) +// } ) +// } ) diff --git a/src/ducks/logonoff/types.ts b/src/ducks/logonoff/types.ts new file mode 100644 index 0000000..23c52ac --- /dev/null +++ b/src/ducks/logonoff/types.ts @@ -0,0 +1,10 @@ +export const SCAN = 'osschat/wechaty/event/scan' +export const LOGIN = 'osschat/wechaty/event/login' +export const LOGOUT = 'osschat/wechaty/event/logout' + +export interface State { + [k: string]: { + qrcode? : string, + userName? : string, + } +} diff --git a/src/get-wechaty.ts b/src/get-wechaty.ts index c3f4dfc..28fa673 100644 --- a/src/get-wechaty.ts +++ b/src/get-wechaty.ts @@ -1,7 +1,3 @@ -import { - Wechaty, -} from 'wechaty' - import { log, } from './config' @@ -10,26 +6,29 @@ import { } from './get-memory' import { Chatops } from './chatops' -let wechaty: Wechaty +import { HAWechaty } from './ha-wechaty' + +// let wechaty: Wechaty +let haWechaty: HAWechaty -export function getWechaty (): Wechaty { - log.verbose('getWechaty', 'getWechaty()') +export function getHAWechaty (): HAWechaty { + log.verbose('getWechaty', 'getHAWechaty()') - if (wechaty) { - return wechaty + if (haWechaty) { + return haWechaty } const name = process.env.WECHATY_NAME || 'heroku-wechaty' const memory = getMemory(name) - wechaty = new Wechaty({ + haWechaty = new HAWechaty({ memory, name, }) // Initialize Chatops Instance: - Chatops.instance(wechaty) + Chatops.instance(haWechaty) - return wechaty + return haWechaty } diff --git a/src/ha-wechaty.ts b/src/ha-wechaty.ts new file mode 100644 index 0000000..7dd2d54 --- /dev/null +++ b/src/ha-wechaty.ts @@ -0,0 +1,200 @@ +import { + Wechaty, + WechatyOptions, + Room, +} from 'wechaty' + +import { StateSwitch } from 'state-switch' + +import flattenArray from 'flatten-array' + +import { + log, +} from './config' +import { WechatyEventName } from 'wechaty/dist/src/wechaty' + +export class HAWechaty { + + public state: StateSwitch + + public wechatyList: Wechaty[] + + public Room = { + findAll : this.roomFindAll.bind(this), + load : this.roomLoad.bind(this), + } + + public async roomFindAll (): Promise { + log.verbose('HAWechaty', 'roomFindAll()') + const roomListList = Promise.all( + this.wechatyList.map( + wechaty => wechaty.Room.findAll() + ) + ) + + const roomList = [] as Room[] + + /** + * allRoomList may contain one room for multiple times + * because we have more than one bot in the same room + */ + const allRoomList = flattenArray(roomListList) as Room[] + for (const room of allRoomList) { + const exist = roomList.some(r => r.id === room.id) + if (exist) { + // We have a room in our list, so skip this one + continue + } + roomList.push(room) + } + return roomList + } + + public async roomLoad (id: string): Promise { + log.verbose('HAWechaty', 'roomLoad(%s)', id) + const roomList = this.wechatyList.map( + wechaty => wechaty.Room.load(id) + ) + + for (const room of roomList) { + try { + await room.ready() + if (room.isReady()) { + log.verbose('HAWechaty', 'roomLoad() %s has room id %s', room.wechaty, room.id) + return room + } + } catch (e) { + log.verbose('HAWechaty', 'roomLoad() %s has no room id %s', room.wechaty, room.id) + } + } + + return null + } + + constructor ( + public options: WechatyOptions, + ) { + log.verbose('HAWechaty', 'constructor("%s")', JSON.stringify(options)) + this.wechatyList = [] + this.state = new StateSwitch('HAWechaty') + } + + public async start () { + log.verbose('HAWechaty', 'start()') + + try { + this.state.on('pending') + + const wechatyPuppet = process.env.WECHATY_PUPPET || '' + const wechatyPuppetList = wechatyPuppet + .split(':') + .filter(v => !!v) + .map(v => v.toUpperCase()) + .map(v => v.replace(/-/g, '_')) + + if (wechatyPuppetList.includes('WECHATY_PUPPET_HOSTIE') + && process.env.WECHATY_PUPPET_HOSTIE_TOKEN + ) { + this.wechatyList.push( + new Wechaty({ + ...this.options, + puppet: 'wechaty-puppet-hostie', + }), + ) + } + + if (wechatyPuppetList.includes('WECHATY_PUPPET_PADPLUS') + && process.env.WECHATY_PUPPET_PADPLUS_TOKEN + ) { + this.wechatyList.push( + new Wechaty({ + ...this.options, + puppet: 'wechaty-puppet-padplus', + }), + ) + } + + if (wechatyPuppetList.includes('WECHATY_PUPPET_MOCK') + && process.env.WECHATY_PUPPET_MOCK_TOKEN + ) { + this.wechatyList.push( + new Wechaty({ + ...this.options, + puppet: 'wechaty-puppet-mock', + }), + ) + } + + if (this.wechatyList.length <= 0) { + throw new Error('no wechaty puppet found') + } + + log.info('HAWechaty', 'start() %s puppet inited', this.wechatyList.length) + + await Promise.all( + this.wechatyList.map( + wechaty => wechaty.start() + ) + ) + + this.state.on(true) + + } catch (e) { + log.warn('HAWechaty', 'start() rejection: %s', e) + this.state.off(true) + } + + } + + public async stop () { + log.verbose('HAWechaty', 'stop()') + + try { + this.state.off('pending') + + await Promise.all( + this.wechatyList.map( + wechaty => wechaty.stop() + ) + ) + } catch (e) { + log.warn('HAWechaty', 'stop() rejection: %s', e) + throw e + } finally { + this.state.off(true) + } + } + + public logonoff (): boolean { + log.verbose('HAWechaty', 'logonoff()') + return this.wechatyList.some(wechaty => wechaty.logonoff()) + } + + public on ( + eventName : WechatyEventName, + handlerModule : string | Function, + ): this { + this.wechatyList.forEach(wechaty => wechaty.on(eventName as any, handlerModule as any)) + return this + } + + public logout (): void { + log.verbose('HAWechaty', 'logout()') + + this.wechatyList.forEach( + wechaty => wechaty.logout() + ) + } + + public async say (text: string): Promise { + log.verbose('HAWechaty', 'say(%s)', text) + this.wechatyList.forEach(wechaty => wechaty.say(text)) + } + + public name (): string { + return this.wechatyList + .map(wechaty => wechaty.name()) + .join(',') + } + +} diff --git a/src/handlers/on-login.ts b/src/handlers/on-login.ts index d4dce2a..5bb9631 100644 --- a/src/handlers/on-login.ts +++ b/src/handlers/on-login.ts @@ -6,12 +6,25 @@ import { } from 'wechaty' import { debug } from '../config' +import { + store, + logonoffActions, +} from '../ducks/' + export default async function onLogin ( this : Wechaty, user : Contact, ): Promise { const msg = `${user.name()} Heroku Wechaty Getting Started v${VERSION} logined` log.info('startBot', 'onLogin(%s) %s', user, msg) + + store.dispatch( + logonoffActions.login( + this.id, + user.name(), + ) + ) + if (debug()) { await user.say(msg) } diff --git a/src/handlers/on-logout.ts b/src/handlers/on-logout.ts index 9a98a7b..75e1bfe 100644 --- a/src/handlers/on-logout.ts +++ b/src/handlers/on-logout.ts @@ -4,9 +4,21 @@ import { Wechaty, } from 'wechaty' +import { + store, + logonoffActions, +} from '../ducks/' + export default async function onLogout ( this : Wechaty, user : Contact, ): Promise { log.info('on-logout', `onLogout(%s)`, user) + + store.dispatch( + logonoffActions.logout( + this.id, + ) + ) + } diff --git a/src/handlers/on-message.ts b/src/handlers/on-message.ts index a83000e..7ae05aa 100644 --- a/src/handlers/on-message.ts +++ b/src/handlers/on-message.ts @@ -12,12 +12,24 @@ import { import { VoteManager } from '../managers/vote-manager' import { Chatops } from '../chatops' +import { + store, + counterActions, +} from '../ducks/' + const BORN_TIME = Date.now() export default async function onMessage ( this : Wechaty, message : Message, ): Promise { + + if (message.self()) { + store.dispatch(counterActions.mo()) + } else { + store.dispatch(counterActions.mt()) + } + const text = message.text() const contact = message.from() if (!contact) { diff --git a/src/handlers/on-scan.ts b/src/handlers/on-scan.ts index 9fc51f4..9a3fc11 100644 --- a/src/handlers/on-scan.ts +++ b/src/handlers/on-scan.ts @@ -5,6 +5,11 @@ import { import { generate } from 'qrcode-terminal' +import { + store, + logonoffActions, +} from '../ducks/' + export default async function onScan ( this : Wechaty, qrcode : string, @@ -15,6 +20,13 @@ export default async function onScan ( qrcodeValueToUrl(qrcode), ) + store.dispatch( + logonoffActions.scan( + this.id, + qrcode, + ) + ) + generate(qrcode) } diff --git a/src/issue-handlers.ts b/src/issue-handlers.ts index 6b14cdf..b803104 100644 --- a/src/issue-handlers.ts +++ b/src/issue-handlers.ts @@ -9,7 +9,7 @@ import { Room, } from 'wechaty' -import { getWechaty } from './get-wechaty' +import { getHAWechaty } from './get-wechaty' import { Chatops } from './chatops' @@ -99,10 +99,10 @@ export const commentIssue: OnCallback = asy // console.info(context) } -function getRoomList ( +async function getRoomList ( owner : string, repository : string, -): Room[] { +): Promise { log.verbose('issue-handler', 'getRoom(%s, %s, config)', owner, repository) const managedList = Object.keys(managedRepoConfig) @@ -130,15 +130,17 @@ function getRoomList ( // matchedList = exactMatchList // } - const idsToRooms = (idOrList: string | string[]) => { + const idsToRooms = async (idOrList: string | string[]) => { if (Array.isArray(idOrList)) { - return idOrList.map( - id => getWechaty().Room.load(id) + const roomList = await Promise.all( + idOrList.map( + id => getHAWechaty().Room.load(id) + ) ) + return roomList.filter(r => !!r) as Room[] } else { - return [ - getWechaty().Room.load(idOrList), - ] + const room = await getHAWechaty().Room.load(idOrList) + return room ? [ room ] : [] } } @@ -154,7 +156,7 @@ function getRoomList ( } // make the id unique (in case an id appear in different repo configs) - const roomList = idsToRooms( + const roomList = await idsToRooms( [...new Set(roomIdList)], ) @@ -183,7 +185,7 @@ async function manageIssue ( 'issue card for chatops', ) - const roomList = getRoomList(owner, repository) + const roomList = await getRoomList(owner, repository) if (roomList.length <= 0) { return } diff --git a/src/routers.ts b/src/routers.ts index ac65934..c2daeef 100644 --- a/src/routers.ts +++ b/src/routers.ts @@ -5,31 +5,25 @@ import { Response, } from 'express' +import { + Room, + Wechaty, +} from 'wechaty' + import { log, VERSION, } from './config' import { Chatops } from './chatops' -import { getWechaty } from './get-wechaty' -import { Room } from 'wechaty' - -const bot = getWechaty() - -bot.on('scan', qrcode => { - qrcodeValue = qrcode - userName = undefined -}) -bot.on('login', user => { - qrcodeValue = undefined - userName = user.name() -}) -bot.on('logout', () => { - qrcodeValue = undefined - userName = undefined -}) - -let qrcodeValue: undefined | string -let userName: undefined | string +import { getHAWechaty } from './get-wechaty' + +import { + store, + logonoffSelectors, + counterSelectors, +} from './ducks/' + +const haBot = getHAWechaty() const FORM_HTML = `
@@ -60,7 +54,7 @@ async function logoutHandler ( } = req.query as { secret?: string } if (secret && secret === process.env.HUAN_SECRET) { - await bot.logout() + await haBot.logout() await Chatops.instance().say('Logout request from web accepted') res.end('logged out') @@ -91,25 +85,82 @@ async function chatopsHandler ( return res.redirect('/') } -async function rootHandler (_req: Request, res: Response) { +async function rootHandler ( + _req: Request, + res: Response, +) { + let html = '' + for (const wechaty of haBot.wechatyList) { + + const info = logonoffSelectors.status( + store.getState().logonoff, + wechaty.id, + ) + + html += [ + '
\n', + await rootHtml(wechaty, info), + '
\n', + ].join('') + } + + const htmlHead = ` + + + + + + + ` + + const mt = counterSelectors.mt(store.getState().counter) + const mo = counterSelectors.mo(store.getState().counter) + + const htmlCounter = ` +
+
    +
  • Message Received: ${mt}
  • +
  • Message Sent: ${mo}
  • +
+ ` + const htmlFoot = ` + + + ` + res.end( + [ + htmlHead, + html, + htmlCounter, + htmlFoot, + ].join('\n') + ) + +} + +async function rootHtml ( + wechaty: Wechaty, + info: ReturnType, +) { + let html - if (qrcodeValue) { + if (info.qrcode) { html = [ `

OSSChat v${VERSION}

`, 'Scan QR Code:
', - qrcodeValue + '
', + info.qrcode + '
', 'http://goqr.me/
', '\n\n', '', ].join('') - } else if (userName) { - let rooms = await bot.Room.findAll() + } else if (info.userName) { + let rooms = await wechaty.Room.findAll() rooms = rooms.sort((a, b) => a.id > b.id ? 1 : -1) let roomHtml = `The rooms I have joined are as follows:
    ` @@ -128,7 +179,7 @@ async function rootHandler (_req: Request, res: Response) { roomHtml = roomHtml + `
` html = [ - `

OSSChat v${VERSION} User ${userName} logined.

`, + `

OSSChat v${VERSION} User ${info.userName} logined.

`, FORM_HTML, roomHtml, ].join('') @@ -138,24 +189,5 @@ async function rootHandler (_req: Request, res: Response) { } - const htmlHead = ` - - - - - - - ` - - const htmlFoot = ` - - ` - - res.end( - [ - htmlHead, - html, - htmlFoot, - ].join('\n') - ) + return html } diff --git a/src/start-bot.ts b/src/start-bot.ts index 08c504a..022b5d8 100644 --- a/src/start-bot.ts +++ b/src/start-bot.ts @@ -1,5 +1,4 @@ import { - Wechaty, Contact, } from 'wechaty' @@ -12,11 +11,12 @@ import { import { Wtmp, } from './wtmp' +import { HAWechaty } from './ha-wechaty' -export async function startBot (wechaty: Wechaty): Promise { - log.verbose('startBot', 'startBot(%s)', wechaty) +export async function startBot (haWechaty: HAWechaty): Promise { + log.verbose('startBot', 'startBot(%s)', haWechaty) - wechaty + haWechaty .on('scan', './handlers/on-scan') .on('error', './handlers/on-error') .on('friendship', './handlers/on-friendship') @@ -33,14 +33,14 @@ export async function startBot (wechaty: Wechaty): Promise { } const ONE_HOUR = 60 * 60 * 1000 setInterval(heartbeat('💖'), ONE_HOUR) - wechaty.on('login', heartbeat(`🙋 (${wechaty.name()})`)) - wechaty.on('ready', heartbeat(`💪 (${wechaty.name()})`)) - wechaty.on('logout', heartbeat(`😪 (${wechaty.name()})`)) + haWechaty.on('login', heartbeat(`🙋 (${haWechaty.name()})`)) + haWechaty.on('ready', heartbeat(`💪 (${haWechaty.name()})`)) + haWechaty.on('logout', heartbeat(`😪 (${haWechaty.name()})`)) const wtmp = Wtmp.instance() const loginWtmp = (user: Contact) => wtmp.login(user.name()) const logoutWtmp = (user: Contact) => wtmp.logout(user.name()) - wechaty.on('login', loginWtmp) - wechaty.on('logout', logoutWtmp) + haWechaty.on('login', loginWtmp) + haWechaty.on('logout', logoutWtmp) } diff --git a/src/start-finis.ts b/src/start-finis.ts index 0476248..23c76ed 100644 --- a/src/start-finis.ts +++ b/src/start-finis.ts @@ -1,5 +1,7 @@ import { finis } from 'finis' -import { Wechaty } from 'wechaty' +import { + Contact, +} from 'wechaty' import { Chatops, @@ -9,6 +11,7 @@ import { VERSION, debug, } from './config' +import { HAWechaty } from './ha-wechaty' const BOT_NAME = 'OSSChat' @@ -16,16 +19,16 @@ const LOGIN_ANNOUNCEMENT = `Der! I just got online!\n${BOT_NAME} v${VERSION}` // const LOGOUT_ANNOUNCEMENT = `Der! I'm going to offline now, see you, bye!\nOSSChat v${VERSION}` const EXIT_ANNOUNCEMENT = `Der! I'm going to exit now, see you, bye!\n${BOT_NAME} v${VERSION}` -let bot: undefined | Wechaty +let bot: undefined | HAWechaty -export async function startFinis (wechaty: Wechaty): Promise { +export async function startFinis (haWechaty: HAWechaty): Promise { if (bot) { throw new Error('startFinis should only init once') } - bot = wechaty + bot = haWechaty - bot.on('login', _ => Chatops.instance().say(LOGIN_ANNOUNCEMENT)) - bot.on('logout', user => log.info('RestartReporter', 'startFinis() bot %s logout', user)) + bot.on('login', async function () { await Chatops.instance().say(LOGIN_ANNOUNCEMENT) }) + bot.on('logout', function (user: Contact) { log.info('RestartReporter', 'startFinis() bot %s logout', user) }) } /** diff --git a/src/typings.d.ts b/src/typings.d.ts new file mode 100644 index 0000000..6b8c4c2 --- /dev/null +++ b/src/typings.d.ts @@ -0,0 +1 @@ +declare module 'flatten-array' diff --git a/tests/fixtures/smoke-testing.ts b/tests/fixtures/smoke-testing.ts index 2006662..c237398 100644 --- a/tests/fixtures/smoke-testing.ts +++ b/tests/fixtures/smoke-testing.ts @@ -1,18 +1,19 @@ #!/usr/bin/env ts-node -import { getWechaty } from '../../src/get-wechaty' +import { getHAWechaty } from '../../src/get-wechaty' import { startBot } from '../../src/start-bot' import { startFinis } from '../../src/start-finis' process.env.WECHATY_PUPPET = 'wechaty-puppet-mock' +process.env.WECHATY_PUPPET_MOCK_TOKEN = 'mock-token' async function main () { - const bot = getWechaty() + const haBot = getHAWechaty() await Promise.all([ - bot.start(), - startBot(bot), - startFinis(bot), + haBot.start(), + startBot(haBot), + startFinis(haBot), ]) return 0