UNPKG

7.18 kBPlain TextView Raw
1/**
2 * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty
3 *
4 * @copyright 2016 Huan LI (李卓桓) <https://github.com/huan>, and
5 * Wechaty Contributors <https://github.com/wechaty>.
6 *
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
10 *
11 * http://www.apache.org/licenses/LICENSE-2.0
12 *
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 *
19 */
20import { StateSwitch } from 'state-switch'
21import {
22 PuppetServer,
23 PuppetServerOptions,
24} from 'wechaty-puppet-service'
25
26import type { MessageInterface } from './user-modules/mod.js'
27import type { WechatyInterface } from './wechaty/mod.js'
28
29import {
30 log,
31} from './config.js'
32import { Io } from './io.js'
33
34export interface IoClientOptions {
35 token : string,
36 wechaty : WechatyInterface,
37 port?: number
38}
39
40const DEFAULT_IO_CLIENT_OPTIONS: Partial<IoClientOptions> = {
41 port: 8788,
42}
43
44export class IoClient {
45
46 /**
47 * Huan(20161026): keep io `null-able` or not?
48 * Huan(202002): make it optional.
49 */
50 private io?: Io
51 private puppetServer?: PuppetServer
52
53 private state: StateSwitch
54
55 protected options: Required<IoClientOptions>
56
57 constructor (
58 options: IoClientOptions,
59 ) {
60 log.verbose('IoClient', 'constructor({%s})',
61 Object.keys(options)
62 .map(key => {
63 return `${key}:${(options as any)[key]}`
64 })
65 .reduce((acc, cur) => `${acc}, ${cur}`),
66 )
67
68 const normalizedOptions = {
69 ...DEFAULT_IO_CLIENT_OPTIONS,
70 ...options,
71 } as Required<IoClientOptions>
72
73 this.options = normalizedOptions
74
75 this.state = new StateSwitch('IoClient', { log })
76 }
77
78 private async startPuppetServer () {
79 log.verbose('IoClient', 'startPuppetServer()')
80
81 if (this.puppetServer) {
82 throw new Error('puppet server exists')
83 }
84
85 const options: PuppetServerOptions = {
86 endpoint : '0.0.0.0:' + this.options.port,
87 /**
88 * Huan(202110): FIXME: remove the any
89 * by updating the puppet-service server code
90 * to use PuppetInterface
91 */
92 puppet : this.options.wechaty.puppet as any,
93 token : this.options.token,
94 }
95 this.puppetServer = new PuppetServer(options)
96 await this.puppetServer.start()
97 }
98
99 private async stopPuppetServer () {
100 log.verbose('IoClient', 'stopPuppetServer()')
101
102 if (!this.puppetServer) {
103 log.error('IoClient', 'stopPuppetServer() this.puppetServer is `undefined`')
104 return
105 }
106
107 await this.puppetServer.stop()
108 this.puppetServer = undefined
109 }
110
111 public async start (): Promise<void> {
112 log.verbose('IoClient', 'start()')
113
114 if (this.state.active()) {
115 log.warn('IoClient', 'start() with a on state, wait and return')
116 await this.state.stable('active')
117 return
118 }
119
120 this.state.active('pending')
121
122 try {
123 await this.hookWechaty(this.options.wechaty)
124
125 await this.startIo()
126
127 await this.options.wechaty.start()
128
129 await this.startPuppetServer()
130
131 this.state.active(true)
132
133 } catch (e) {
134 log.error('IoClient', 'start() exception: %s', (e as Error).message)
135 this.state.inactive(true)
136 throw e
137 }
138 }
139
140 private async hookWechaty (wechaty: WechatyInterface): Promise<void> {
141 log.verbose('IoClient', 'hookWechaty()')
142
143 if (this.state.inactive()) {
144 const e = new Error('state.off() is true, skipped')
145 log.warn('IoClient', 'initWechaty() %s', e.message)
146 throw e
147 }
148
149 wechaty
150 .on('login', user => log.info('IoClient', `${user.name()} logged in`))
151 .on('logout', user => log.info('IoClient', `${user.name()} logged out`))
152 .on('message', msg => this.onMessage(msg))
153 .on('scan', (url, code) => {
154 log.info('IoClient', [
155 `[${code}] ${url}`,
156 `Online QR Code Image: https://wechaty.js.org/qrcode/${encodeURIComponent(url)}`,
157 ].join('\n'))
158 })
159 }
160
161 private async startIo (): Promise<void> {
162 log.verbose('IoClient', 'startIo() with token %s', this.options.token)
163
164 if (this.state.inactive()) {
165 const e = new Error('startIo() state.off() is true, skipped')
166 log.warn('IoClient', e.message)
167 throw e
168 }
169
170 if (this.io) {
171 throw new Error('io exists')
172 }
173
174 this.io = new Io({
175 servicePort : this.options.port,
176 token : this.options.token,
177 wechaty : this.options.wechaty,
178 })
179
180 try {
181 await this.io.start()
182 } catch (e) {
183 log.verbose('IoClient', 'startIo() init fail: %s', (e as Error).message)
184 throw e
185 }
186 }
187
188 private async stopIo () {
189 log.verbose('IoClient', 'stopIo()')
190
191 if (!this.io) {
192 log.warn('IoClient', 'stopIo() io does not exist')
193 return
194 }
195
196 await this.io.stop()
197 this.io = undefined
198 }
199
200 private async onMessage (msg: MessageInterface) {
201 log.verbose('IoClient', 'onMessage(%s)', msg)
202
203 // const from = m.from()
204 // const to = m.to()
205 // const content = m.toString()
206 // const room = m.room()
207
208 // log.info('Bot', '%s<%s>:%s'
209 // , (room ? '['+room.topic()+']' : '')
210 // , from.name()
211 // , m.toStringDigest()
212 // )
213
214 // if (/^wechaty|chatie|botie/i.test(m.text()) && !m.self()) {
215 // await m.say('https://www.chatie.io')
216 // .then(_ => log.info('Bot', 'REPLIED to magic word "chatie"'))
217 // }
218 }
219
220 public async stop (): Promise<void> {
221 log.verbose('IoClient', 'stop()')
222
223 this.state.inactive('pending')
224
225 await this.stopIo()
226 await this.stopPuppetServer()
227 await this.options.wechaty.stop()
228
229 this.state.inactive(true)
230
231 // XXX 20161026
232 // this.io = null
233 }
234
235 public async restart (): Promise<void> {
236 log.verbose('IoClient', 'restart()')
237
238 try {
239 await this.stop()
240 await this.start()
241 } catch (e) {
242 log.error('IoClient', 'restart() exception %s', (e as Error).message)
243 throw e
244 }
245 }
246
247 public async quit (): Promise<void> {
248 log.verbose('IoClient', 'quit()')
249
250 if (this.state.inactive() === 'pending') {
251 log.warn('IoClient', 'quit() with state.off() = `pending`, skipped')
252 throw new Error('quit() with state.off() = `pending`')
253 }
254
255 this.state.inactive('pending')
256
257 try {
258 // if (this.options.wechaty) {
259 await this.options.wechaty.stop()
260 // this.wechaty = null
261 // } else { log.warn('IoClient', 'quit() no this.wechaty') }
262
263 if (this.io) {
264 await this.io.stop()
265 // this.io = null
266 } else { log.warn('IoClient', 'quit() no this.io') }
267
268 } catch (e) {
269 log.error('IoClient', 'exception: %s', (e as Error).message)
270 throw e
271 } finally {
272 this.state.inactive(true)
273 }
274 }
275
276}