From 278379d3008e0c3700c608353988285cecb2d20c Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Sun, 30 Aug 2020 02:03:39 +0800 Subject: [PATCH 01/22] chore: update readme --- README.md | 14 ++++++++++---- packages/plugin-eval-addons/package.json | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3ee4da6c69..ad5c1e0a7b 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,9 @@ Koishi 是一个在 [Node.js](https://nodejs.org/) 环境下运行的机器人 这个项目的名字和图标来源于东方 Project 中的角色古明地恋 (Komeiji Koishi)。 -![demo](./.github/demo.png) +
+demo +
## 安装 @@ -85,7 +87,7 @@ koishi-plugin-common 包含了一些常用功能,它们在你使用 koishi 库 - 输出聊天记录到控制台 - 欢迎入群,复读,处理申请,频率限制,自定义回复…… -### [koishi-plugin-eval](https://koishi.js.org/plugins/eval.html) [![npm](https://img.shields.io/npm/v/koishi-plugin-eval/next?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-eval) +### [koishi-plugin-eval](https://koishi.js.org/plugins/eval.html) [![npm](https://img.shields.io/npm/v/koishi-plugin-eval?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-eval) koishi-plugin-eval 允许用户直接使用机器人执行脚本。它利用了 Node.js 的 [vm](https://nodejs.org/api/vm.html) 和 [worker_threads](https://nodejs.org/api/worker_threads.html) 模块,在保护执行安全的前提下能够获得较快的响应速度。同时,插件还提供了一些内置的 API 供用户调用,结合教学功能可以在客户端实现复杂的行为。 @@ -99,14 +101,18 @@ koishi-plugin-eval-addons 在前一个插件的基础上,允许用户编写自 ### [koishi-plugin-monitor](./packages/plugin-monitor) [![npm](https://img.shields.io/npm/v/koishi-plugin-monitor/next?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-monitor) -### [koishi-plugin-puppeteer](./packages/plugin-puppeteer) [![npm](https://img.shields.io/npm/v/koishi-plugin-puppeteer?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-puppeteer) +### [koishi-plugin-puppeteer](https://koishi.js.org/plugins/puppeteer.html) [![npm](https://img.shields.io/npm/v/koishi-plugin-puppeteer?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-puppeteer) -### [koishi-plugin-recorder](./packages/plugin-recorder) [![npm](https://img.shields.io/npm/v/koishi-plugin-recorder/next?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-recorder) +koishi-plugin-puppeteer 本身提供了网页截图(shot)指令和 TeX 渲染指令(tex),同时也封装了一系列与网页进行交互的接口。利用这些接口我们可以开发更多以渲染图片为基础的插件,如 koishi-plugin-chess 等。 ### [koishi-plugin-rss](./packages/plugin-rss) [![npm](https://img.shields.io/npm/v/koishi-plugin-rss?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-rss) +koishi-plugin-rss 提供了 [RSS](https://en.wikipedia.org/wiki/RSS) 支持,允许不同的群订阅不同的 RSS 信息源并实时进行通知。 + ### [koishi-plugin-schedule](./packages/plugin-schedule) [![npm](https://img.shields.io/npm/v/koishi-plugin-schedule?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-schedule) +koishi-plugin-schedule 允许用户设置定时任务并执行。这些计划任务会被存储在数据库中,即使重启机器人也能继续工作。 + ### [koishi-plugin-status](./packages/plugin-status) [![npm](https://img.shields.io/npm/v/koishi-plugin-status/next?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-status) ### [koishi-plugin-teach](./packages/plugin-teach) [![npm](https://img.shields.io/npm/v/koishi-plugin-teach?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-teach) diff --git a/packages/plugin-eval-addons/package.json b/packages/plugin-eval-addons/package.json index 79cba96065..4379a5c068 100644 --- a/packages/plugin-eval-addons/package.json +++ b/packages/plugin-eval-addons/package.json @@ -1,6 +1,6 @@ { "name": "koishi-plugin-eval-addons", - "version": "1.0.0-beta.8", + "version": "1.0.0-beta.9", "description": "Execute JavaScript in Koishi", "main": "dist/index.js", "typings": "dist/index.d.ts", From a01be705703625a3c7124c4e26b278ef4c445117 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Sun, 30 Aug 2020 14:47:53 +0800 Subject: [PATCH 02/22] feat(cli): hint for exit command --- packages/koishi/src/run.ts | 16 ++++++++++++++-- packages/koishi/src/worker.ts | 32 ++++++++++++++++++++++++++++---- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/packages/koishi/src/run.ts b/packages/koishi/src/run.ts index 66812ecf1f..0e307c71ca 100644 --- a/packages/koishi/src/run.ts +++ b/packages/koishi/src/run.ts @@ -26,6 +26,13 @@ process.on('SIGINT', () => { } }) +interface Message { + type: 'start' | 'exit' + payload: any +} + +let payload: any + function createWorker(options: WorkerOptions) { child = fork(resolve(__dirname, 'worker'), [], { execArgv: options['--'], @@ -33,9 +40,14 @@ function createWorker(options: WorkerOptions) { let started = false - child.on('message', (data) => { - if (data === 'start') { + child.on('message', (message: Message) => { + if (message.type === 'start') { started = true + if (payload) { + child.send({ type: 'send', payload }) + } + } else if (message.type === 'exit') { + payload = message.payload } }) diff --git a/packages/koishi/src/worker.ts b/packages/koishi/src/worker.ts index 6fe3061c78..fe56ad8f7d 100644 --- a/packages/koishi/src/worker.ts +++ b/packages/koishi/src/worker.ts @@ -1,6 +1,6 @@ import { App, AppOptions, Context, Plugin } from 'koishi-core' import { resolve, dirname } from 'path' -import { Logger } from 'koishi-utils' +import { Logger, noop } from 'koishi-utils' import { performance } from 'perf_hooks' import { yellow } from 'kleur' import 'koishi-adapter-cqhttp' @@ -105,14 +105,38 @@ if (config.logLevel && !process.env.KOISHI_LOG_LEVEL) { Logger.baseLevel = config.logLevel } +interface Message { + type: 'send' + payload: any +} + +process.on('message', (data: Message) => { + if (data.type === 'send') { + const { groupId, userId, selfId, message } = data.payload + const bot = app.bots[selfId] + if (groupId) { + bot.sendGroupMsg(groupId, message) + } else { + bot.sendPrivateMsg(userId, message) + } + } +}) + const app = new App(config) app.command('exit', '停止机器人运行', { authority: 4 }) .option('restart', '-r 重新启动') .shortcut('关机', { prefix: true }) .shortcut('重启', { prefix: true, options: { restart: true } }) - .action(({ options }) => { - process.exit(options.restart ? 514 : 0) + .action(async ({ options, session }) => { + const { groupId, userId, selfId } = session + if (!options.restart) { + await session.$send('正在关机……').catch(noop) + process.exit() + } + process.send({ type: 'exit', payload: { groupId, userId, selfId, message: '已成功重启。' } }) + await session.$send(`正在重启……`).catch(noop) + process.exit(514) }) if (Array.isArray(config.plugins)) { @@ -131,5 +155,5 @@ app.start().then(() => { const time = Math.max(0, performance.now() - +process.env.KOISHI_START_TIME).toFixed() logger.success(`bot started successfully in ${time} ms.`) - process.send('start') + process.send({ type: 'start' }) }, handleException) From 008ae389ab9fc3f6aebe5af9dd6dbbd974b7725b Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Sun, 30 Aug 2020 17:51:57 +0800 Subject: [PATCH 03/22] fix(eval-addons): fix namespace sandbox escape --- packages/plugin-eval-addons/src/index.ts | 7 ++++--- packages/plugin-eval-addons/src/worker.ts | 16 +++++++++------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/plugin-eval-addons/src/index.ts b/packages/plugin-eval-addons/src/index.ts index 655e8825c7..e30d667597 100644 --- a/packages/plugin-eval-addons/src/index.ts +++ b/packages/plugin-eval-addons/src/index.ts @@ -5,18 +5,19 @@ import { safeLoad } from 'js-yaml' import { promises as fs } from 'fs' import { attachTraps, FieldOptions } from 'koishi-plugin-eval' import Git, { CheckRepoActions } from 'simple-git' +import { AddonWorkerConfig } from './worker' const logger = new Logger('addon') -type AddonConfig = Config +export interface Config extends AddonMainConfig, AddonWorkerConfig {} -export interface Config { +export interface AddonMainConfig { gitRemote?: string exclude?: RegExp } declare module 'koishi-plugin-eval/dist/main' { - interface MainConfig extends AddonConfig {} + interface MainConfig extends AddonMainConfig {} } interface OptionManifest extends OptionConfig { diff --git a/packages/plugin-eval-addons/src/worker.ts b/packages/plugin-eval-addons/src/worker.ts index 74437becca..b773dfa899 100644 --- a/packages/plugin-eval-addons/src/worker.ts +++ b/packages/plugin-eval-addons/src/worker.ts @@ -1,5 +1,5 @@ import { config, context, internal, WorkerAPI, Context, response, mapDirectory, formatError } from 'koishi-plugin-eval/dist/worker' -import { promises, readFileSync } from 'fs' +import { promises as fs, readFileSync } from 'fs' import { resolve, posix, dirname } from 'path' import { Logger, Time, CQCode, Random } from 'koishi-utils' import json5 from 'json5' @@ -9,10 +9,12 @@ const logger = new Logger('addon') const { SourceTextModule, SyntheticModule } = require('vm') +export interface AddonWorkerConfig { + moduleRoot?: string +} + declare module 'koishi-plugin-eval/dist/worker' { - interface WorkerConfig { - moduleRoot?: string - } + interface WorkerConfig extends AddonWorkerConfig {} interface WorkerData { addonNames: string[] @@ -120,7 +122,7 @@ async function loadSource(path: string) { for (const postfix of suffixes) { try { const target = path + postfix - return [await promises.readFile(resolve(config.moduleRoot, target), 'utf8'), target] + return [await fs.readFile(resolve(config.moduleRoot, target), 'utf8'), target] } catch {} } throw new Error(`cannot load source file "${path}"`) @@ -142,7 +144,7 @@ async function createModule(path: string) { await module.evaluate() if (!path.includes('/')) { - internal.setGlobal(path, module.namespace) + internal.setGlobal(path, internal.decontextify(module.namespace)) } return module } @@ -158,5 +160,5 @@ export async function evaluate(path: string) { export default Promise.all(config.addonNames.map(evaluate)).then(() => { response.commands = Object.keys(commandMap) mapDirectory('koishi/utils/', require.resolve('koishi-utils')) - internal.setGlobal('utils', modules['koishi/utils.ts'].namespace) + internal.setGlobal('utils', internal.decontextify(modules['koishi/utils.ts'].namespace)) }) From 3354b839e34bbf024c1f5dd8a14cb8c147b53240 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Sun, 30 Aug 2020 17:55:52 +0800 Subject: [PATCH 04/22] feat(eval-addons): empty module should not be exposed to global --- packages/plugin-eval-addons/src/worker.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/plugin-eval-addons/src/worker.ts b/packages/plugin-eval-addons/src/worker.ts index b773dfa899..d2d7012c55 100644 --- a/packages/plugin-eval-addons/src/worker.ts +++ b/packages/plugin-eval-addons/src/worker.ts @@ -144,7 +144,10 @@ async function createModule(path: string) { await module.evaluate() if (!path.includes('/')) { - internal.setGlobal(path, internal.decontextify(module.namespace)) + const namespace = internal.decontextify(module.namespace) + if (Object.keys(namespace).length) { + internal.setGlobal(path, namespace) + } } return module } From c991fd5e4f8e6b807f9e397e774a592a388284b8 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Sun, 30 Aug 2020 20:31:42 +0800 Subject: [PATCH 05/22] typings: adjust typings --- packages/koishi-core/src/app.ts | 4 ++-- packages/koishi-utils/src/logger.ts | 12 +++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/koishi-core/src/app.ts b/packages/koishi-core/src/app.ts index 1cb51f8404..af7300ed38 100644 --- a/packages/koishi-core/src/app.ts +++ b/packages/koishi-core/src/app.ts @@ -46,8 +46,8 @@ export class App extends Context { _commands: Command[] _commandMap: Record _hooks: Record any][]> - _userCache: LruCache>> - _groupCache: LruCache>> + _userCache: LruCache, Promise>> + _groupCache: LruCache, Promise>> private _nameRE: RegExp private _prefixRE: RegExp diff --git a/packages/koishi-utils/src/logger.ts b/packages/koishi-utils/src/logger.ts index 2b270eb811..90563b2d27 100644 --- a/packages/koishi-utils/src/logger.ts +++ b/packages/koishi-utils/src/logger.ts @@ -14,6 +14,10 @@ const instances: Record = {} type LogFunction = (format: any, ...param: any[]) => void +type LogType = 'success' | 'error' | 'info' | 'warn' | 'debug' + +export interface Logger extends Record {} + export class Logger { static readonly SUCCESS = 1 static readonly ERROR = 1 @@ -44,12 +48,6 @@ export class Logger { private code: number private displayName: string - public success: LogFunction - public error: LogFunction - public info: LogFunction - public warn: LogFunction - public debug: LogFunction - constructor(private name: string, private showDiff = false) { if (name in instances) return instances[name] let hash = 0 @@ -71,7 +69,7 @@ export class Logger { return Logger.color(this.code, value, decoration) } - private createMethod(name: string, prefix: string, minLevel: number) { + private createMethod(name: LogType, prefix: string, minLevel: number) { this[name] = (...args: [any, ...any[]]) => { if (this.level < minLevel) return process.stderr.write(prefix + this.displayName + this.format(...args) + '\n') From fbb372f7dd12f0647824663316c8f10258116fc1 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Sun, 30 Aug 2020 23:51:27 +0800 Subject: [PATCH 06/22] feat(cli): support ws-reverse --- packages/koishi/src/init.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/koishi/src/init.ts b/packages/koishi/src/init.ts index 3a6c5d5206..9f47724cbb 100644 --- a/packages/koishi/src/init.ts +++ b/packages/koishi/src/init.ts @@ -14,12 +14,18 @@ async function createConfig() { choices: [ { title: 'HTTP', value: 'cqhttp:http' }, { title: 'WebSocket', value: 'cqhttp:ws' }, + { title: 'WebSocket Reverse', value: 'cqhttp:ws-reverse' }, ], }, { name: 'port', - type: (_, data) => data.type === 'cqhttp:http' ? 'number' : null, + type: (_, data) => data.type === 'cqhttp:http' || data.type === 'cqhttp:ws-reverse' ? 'number' : null, message: 'Koishi Port', initial: 8080, + }, { + name: 'path', + type: (_, data) => data.type === 'cqhttp:http' || data.type === 'cqhttp:ws-reverse' ? 'text' : null, + message: 'Koishi Path', + initial: '/', }, { name: 'server', type: (_, data) => data.type === 'cqhttp:http' ? 'text' : null, @@ -41,7 +47,7 @@ async function createConfig() { }, { name: 'token', type: 'text', - message: 'Token for CoolQ Server', + message: 'Token for CQHTTP Server', }], { onCancel: () => succeed = false, }) From 9dfd4cb7c2da2791291db761784f6804bc71ffa8 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Mon, 31 Aug 2020 12:29:31 +0800 Subject: [PATCH 07/22] fix(mongo): fix typo of timers --- packages/plugin-mongo/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin-mongo/src/index.ts b/packages/plugin-mongo/src/index.ts index e3aa4e5c10..6bb403afcb 100644 --- a/packages/plugin-mongo/src/index.ts +++ b/packages/plugin-mongo/src/index.ts @@ -112,8 +112,8 @@ extendDatabase(MongoDatabase, { if (data.timers) { $set.timers = {} for (const key in data.timers) { - if (key === '$date') $set.timer._date = data.timers.$date - else $set.timer[key.replace(/\./gmi, '_')] = data.timers[key] + if (key === '$date') $set.timers._date = data.timers.$date + else $set.timers[key.replace(/\./gmi, '_')] = data.timers[key] } } if (data.usage) { From 967da352f6b360fe1536a7b48cf5cae418a4c5ff Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Mon, 31 Aug 2020 14:46:02 +0800 Subject: [PATCH 08/22] feat(cli): init database config --- packages/koishi/src/init.ts | 182 ++++++++++++++++++++++++++---------- 1 file changed, 132 insertions(+), 50 deletions(-) diff --git a/packages/koishi/src/init.ts b/packages/koishi/src/init.ts index 9f47724cbb..44f48449c2 100644 --- a/packages/koishi/src/init.ts +++ b/packages/koishi/src/init.ts @@ -2,61 +2,143 @@ import { existsSync, writeFileSync, mkdirSync } from 'fs' import { yellow, red, green } from 'kleur' import { resolve, extname, dirname } from 'path' import { AppConfig } from './worker' -import prompts from 'prompts' +import prompts, { PrevCaller, PromptObject } from 'prompts' import { CAC } from 'cac' +import { AppOptions } from 'koishi-core' +import { omit } from 'koishi-utils' +import * as mysql from 'koishi-plugin-mysql' +import * as mongo from 'koishi-plugin-mongo' -async function createConfig() { +function conditional(type: T, key: string, ...values: string[]): PrevCaller { + return (prev, data, prompt) => { + if (!values.includes(data[key])) return null + return typeof type === 'function' ? (type as PrevCaller)(prev, data, prompt) : type + } +} + +const serverQuestions: PromptObject[] = [{ + name: 'type', + type: 'select', + message: 'Server Type', + choices: [ + { title: 'HTTP', value: 'cqhttp:http' }, + { title: 'WebSocket', value: 'cqhttp:ws' }, + { title: 'WebSocket Reverse', value: 'cqhttp:ws-reverse' }, + ], +}, { + name: 'port', + type: conditional('number', 'type', 'cqhttp:http', 'cqhttp:ws-reverse'), + message: 'Koishi Port', + initial: 8080, +}, { + name: 'path', + type: conditional('text', 'type', 'cqhttp:http', 'cqhttp:ws-reverse'), + message: 'Koishi Path', + initial: '/', +}, { + name: 'server', + type: conditional('text', 'type', 'cqhttp:http'), + message: 'HTTP Server', + initial: 'http://localhost:5700', +}, { + name: 'server', + type: conditional('text', 'type', 'cqhttp:ws'), + message: 'WebSocket Server', + initial: 'ws://localhost:6700', +}, { + name: 'selfId', + type: 'number', + message: 'Your Bot\'s QQ Number', +}, { + name: 'secret', + type: 'text', + message: 'Secret for Koishi Server', +}, { + name: 'token', + type: 'text', + message: 'Token for CQHTTP Server', +}, { + name: 'database', + type: 'select', + message: 'Database Type', + choices: [ + { title: 'None', value: null }, + { title: 'MySQL', value: 'mysql' }, + { title: 'MongoDB', value: 'mongo' }, + ], +}] + +const mysqlQuestions: PromptObject[] = [{ + name: 'host', + type: 'text', + message: 'MySQL / Host', + initial: '127.0.0.1', +}, { + name: 'port', + type: 'number', + message: 'MySQL / Port', + initial: '3306', +}, { + name: 'user', + type: 'text', + message: 'MySQL / Username', + initial: 'root', +}, { + name: 'password', + type: 'text', + message: 'MySQL / Password', +}, { + name: 'database', + type: 'text', + message: 'MySQL / Database', + initial: 'koishi', +}] + +const mongoQuestions: PromptObject[] = [{ + name: 'host', + type: 'text', + message: 'MongoDB / Host', + initial: '127.0.0.1', +}, { + name: 'port', + type: 'number', + message: 'MongoDB / Port', + initial: '27017', +}, { + name: 'username', + type: 'text', + message: 'MongoDB / Username', + initial: 'root', +}, { + name: 'password', + type: 'text', + message: 'MongoDB / Password', +}, { + name: 'name', + type: 'text', + message: 'MongoDB / Database', + initial: 'koishi', +}] + +async function question(questions: PromptObject[]) { let succeed = true - const data = await prompts([{ - name: 'type', - type: 'select', - message: 'Connection Type', - choices: [ - { title: 'HTTP', value: 'cqhttp:http' }, - { title: 'WebSocket', value: 'cqhttp:ws' }, - { title: 'WebSocket Reverse', value: 'cqhttp:ws-reverse' }, - ], - }, { - name: 'port', - type: (_, data) => data.type === 'cqhttp:http' || data.type === 'cqhttp:ws-reverse' ? 'number' : null, - message: 'Koishi Port', - initial: 8080, - }, { - name: 'path', - type: (_, data) => data.type === 'cqhttp:http' || data.type === 'cqhttp:ws-reverse' ? 'text' : null, - message: 'Koishi Path', - initial: '/', - }, { - name: 'server', - type: (_, data) => data.type === 'cqhttp:http' ? 'text' : null, - message: 'HTTP Server', - initial: 'http://localhost:5700', - }, { - name: 'server', - type: (_, data) => data.type === 'cqhttp:ws' ? 'text' : null, - message: 'WebSocket Server', - initial: 'ws://localhost:6700', - }, { - name: 'selfId', - type: 'number', - message: 'Your Bot\'s QQ Number', - }, { - name: 'secret', - type: 'text', - message: 'Secret for Koishi Server', - }, { - name: 'token', - type: 'text', - message: 'Token for CQHTTP Server', - }], { + const data = await prompts(questions, { onCancel: () => succeed = false, }) - if (!succeed) return - const config = {} as AppConfig - for (const key in data) { - if (data[key]) config[key] = data[key] + if (!succeed) throw new Error('interrupted') + return data +} + +async function createConfig() { + const data = await question(serverQuestions) + const config = omit(data, ['database']) as AppConfig + config.plugins = [] + console.log(data.database) + if (data.database === 'mysql') { + config.plugins.push(['mysql', await question(mysqlQuestions)]) + } else if (data.database === 'mongo') { + config.plugins.push(['mongo', await question(mongoQuestions)]) } - config.plugins = ['common', 'schedule'] return config } @@ -88,7 +170,7 @@ export default function (cli: CAC) { } // create configurations - const config = await createConfig() + const config = await createConfig().catch(() => {}) if (!config) { console.warn(`${error} initialization was canceled`) process.exit(0) From 85ec2160e344b5cf02c6724fa744191c12eed0e1 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Mon, 31 Aug 2020 14:57:53 +0800 Subject: [PATCH 09/22] chore: remove recorder plugin --- packages/plugin-recorder/README.md | 3 -- packages/plugin-recorder/package.json | 46 ------------------- packages/plugin-recorder/src/index.ts | 45 ------------------ .../tests/__snapshots__/index.spec.ts.snap | 7 --- packages/plugin-recorder/tests/index.spec.ts | 41 ----------------- packages/plugin-recorder/tsconfig.json | 10 ---- 6 files changed, 152 deletions(-) delete mode 100644 packages/plugin-recorder/README.md delete mode 100644 packages/plugin-recorder/package.json delete mode 100644 packages/plugin-recorder/src/index.ts delete mode 100644 packages/plugin-recorder/tests/__snapshots__/index.spec.ts.snap delete mode 100644 packages/plugin-recorder/tests/index.spec.ts delete mode 100644 packages/plugin-recorder/tsconfig.json diff --git a/packages/plugin-recorder/README.md b/packages/plugin-recorder/README.md deleted file mode 100644 index 6d88da71be..0000000000 --- a/packages/plugin-recorder/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# [koishi-plugin-recorder](https://koishi.js.org/plugins/recorder.html) - -[![npm](https://img.shields.io/npm/v/koishi-plugin-recorder?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-recorder) diff --git a/packages/plugin-recorder/package.json b/packages/plugin-recorder/package.json deleted file mode 100644 index b5bcf79b8e..0000000000 --- a/packages/plugin-recorder/package.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "name": "koishi-plugin-recorder", - "description": "Save Chat Records for Koishi", - "version": "2.0.0-beta.7", - "main": "dist/index.js", - "typings": "dist/index.d.ts", - "files": [ - "dist" - ], - "author": "Shigma <1700011071@pku.edu.cn>", - "license": "MIT", - "scripts": { - "build": "tsc -b", - "lint": "eslint src --ext .ts", - "prepack": "tsc -b" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/koishijs/koishi.git" - }, - "bugs": { - "url": "https://github.com/koishijs/koishi/issues" - }, - "homepage": "https://github.com/koishijs/koishi/packages/plugin-recorder#readme", - "keywords": [ - "bot", - "qqbot", - "cqhttp", - "coolq", - "chatbot", - "koishi", - "plugin", - "record", - "recorder" - ], - "peerDependencies": { - "koishi-core": "^2.0.2" - }, - "devDependencies": { - "del": "^5.1.0", - "koishi-test-utils": "^4.0.0" - }, - "dependencies": { - "koishi-utils": "^3.1.2" - } -} diff --git a/packages/plugin-recorder/src/index.ts b/packages/plugin-recorder/src/index.ts deleted file mode 100644 index 5ad4a5ced5..0000000000 --- a/packages/plugin-recorder/src/index.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { WriteStream, createWriteStream, existsSync, mkdirSync } from 'fs' -import { resolve, dirname } from 'path' -import { Session, Context } from 'koishi-core' -import { pick } from 'koishi-utils' - -declare module 'koishi-core/dist/context' { - interface EventMap { - 'before-record' (session: Session): any - } -} - -const streams: Record = {} - -export interface RecorderOptions { - folder?: string -} - -const cwd = process.cwd() - -export const name = 'recorder' - -export function apply(ctx: Context, options: RecorderOptions = {}) { - async function handleMessage(session: Session) { - if (session.subType === 'group' && session.$group.assignee !== session.selfId) return - if (await ctx.serial('before-record', session)) return - const output = JSON.stringify(pick(session, ['time', 'userId', 'message'])) + '\n' - const path = resolve(cwd, options.folder || 'messages', `${session.groupId}.txt`) - if (!streams[path]) { - const folder = dirname(path) - if (!existsSync(folder)) { - mkdirSync(folder, { recursive: true }) - } - streams[path] = createWriteStream(path, { flags: 'a' }) - } - streams[path].write(output) - } - - ctx.on('attach-group', (session: Session) => { - handleMessage(session) - }) - - ctx.on('before-send', (session: Session) => { - handleMessage(session) - }) -} diff --git a/packages/plugin-recorder/tests/__snapshots__/index.spec.ts.snap b/packages/plugin-recorder/tests/__snapshots__/index.spec.ts.snap deleted file mode 100644 index 7b25290913..0000000000 --- a/packages/plugin-recorder/tests/__snapshots__/index.spec.ts.snap +++ /dev/null @@ -1,7 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`check content 1`] = ` -"{\\"$ctxType\\":\\"group\\",\\"$ctxId\\":321,\\"userId\\":123,\\"message\\":\\"foo\\"} -{\\"$ctxType\\":\\"group\\",\\"$ctxId\\":321,\\"userId\\":789,\\"message\\":\\"baz\\"} -" -`; diff --git a/packages/plugin-recorder/tests/index.spec.ts b/packages/plugin-recorder/tests/index.spec.ts deleted file mode 100644 index 6081a09522..0000000000 --- a/packages/plugin-recorder/tests/index.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { MockedApp, BASE_SELF_ID } from 'koishi-test-utils' -import { startAll, stopAll } from 'koishi-core' -import { readFileSync } from 'fs-extra' -import { resolve } from 'path' -import { sleep } from 'koishi-utils' -import * as recorder from '../src' -import del from 'del' - -const app1 = new MockedApp() -const app2 = new MockedApp({ selfId: BASE_SELF_ID + 1 }) - -app1.plugin(recorder) -app2.plugin(recorder) - -beforeAll(() => startAll()) - -const outFolder = resolve(process.cwd(), 'messages') - -afterAll(() => del(outFolder)) - -test('group message', async () => { - const mock = jest.fn() - app1.on('record-writing', mock) - await app1.receiveMessage('group', 'foo', 123, 321) - await app1.receiveMessage('group', 'bar', 456, 654) - await app1.receiveMessage('group', 'baz', 789, 321) - expect(mock).toBeCalledTimes(3) -}) - -test('private message', async () => { - const mock = jest.fn() - app1.on('record-writing', mock) - await app1.receiveMessage('user', 'foo', 123) - expect(mock).toBeCalledTimes(0) -}) - -test('check content', async () => { - await stopAll() - await sleep(100) - expect(readFileSync(resolve(outFolder, '321.txt'), 'utf8')).toMatchSnapshot() -}) diff --git a/packages/plugin-recorder/tsconfig.json b/packages/plugin-recorder/tsconfig.json deleted file mode 100644 index a497f05e83..0000000000 --- a/packages/plugin-recorder/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.base", - "compilerOptions": { - "outDir": "dist", - "rootDir": "src", - }, - "include": [ - "src", - ], -} \ No newline at end of file From 7403b876b97afec10b654a91acaa5bf9c2c3dab8 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Mon, 31 Aug 2020 15:09:23 +0800 Subject: [PATCH 10/22] build: auto generate ecosystem meta --- .gitignore | 4 +-- build/bump.ts | 11 +++++- build/utils.ts | 3 +- package.json | 1 + packages/koishi/ecosystem.json | 64 ++++++++++++++++++++++++++++++++++ 5 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 packages/koishi/ecosystem.json diff --git a/.gitignore b/.gitignore index 3dbe0d016b..4514a66f85 100644 --- a/.gitignore +++ b/.gitignore @@ -3,10 +3,10 @@ temp /addons /atri +/coverage /docs /shiki -/coverage -/plugins +/test todo.md yarn.lock diff --git a/build/bump.ts b/build/bump.ts index 2bec33f835..8bed6e66a7 100644 --- a/build/bump.ts +++ b/build/bump.ts @@ -109,7 +109,7 @@ function bumpPkg(source: Package, flag: BumpType, only = false) { const { name } = source if (target.name === name) return Object.entries({ devDependencies, peerDependencies, dependencies, optionalDependencies }) - .filter(([_, dependencies = {}]) => dependencies[name]) + .filter(([, dependencies = {}]) => dependencies[name]) .forEach(([type]) => { target.meta[type][name] = '^' + newVersion target.dirty = true @@ -150,4 +150,13 @@ const flag = options.major ? 'major' : options.minor ? 'minor' : options.patch ? } return pkg.save() })) + + const ecosystem: Record> = {} + for (const path in packages) { + if (!path.startsWith('packages/') || path.startsWith('packages/koishi')) continue + const { name, version, description } = packages[path].meta + ecosystem[name] = { version, description } + } + + await writeJson(resolve(__dirname, '../packages/koishi/ecosystem.json'), ecosystem, { spaces: 2 }) })() diff --git a/build/utils.ts b/build/utils.ts index e11227986b..14f88a9a39 100644 --- a/build/utils.ts +++ b/build/utils.ts @@ -17,6 +17,7 @@ export type DependencyType = 'dependencies' | 'devDependencies' | 'peerDependenc export interface PackageJson extends Partial>> { name?: string + description?: string private?: boolean version?: string } @@ -36,7 +37,7 @@ export function spawnSync(command: string, silent?: boolean) { export function spawnAsync(command: string) { const args = command.split(/\s+/) const child = spawn(args[0], args.slice(1), { stdio: 'inherit' }) - return new Promise((resolve, reject) => { + return new Promise((resolve) => { child.on('close', resolve) }) } diff --git a/package.json b/package.json index 745b6cbcd5..647d1a2aa1 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "workspaces": [ "atri", "addons", + "test", "shiki", "shiki/core", "shiki/cosmos", diff --git a/packages/koishi/ecosystem.json b/packages/koishi/ecosystem.json new file mode 100644 index 0000000000..09d3c1040c --- /dev/null +++ b/packages/koishi/ecosystem.json @@ -0,0 +1,64 @@ +{ + "koishi-adapter-cqhttp": { + "version": "1.0.1", + "description": "CQHTTP adapter for Koishi" + }, + "koishi-plugin-chess": { + "version": "2.0.0-beta.10", + "description": "Chess Plugin for Koishi" + }, + "koishi-plugin-common": { + "version": "3.0.0-beta.15", + "description": "Common plugins for Koishi" + }, + "koishi-plugin-eval": { + "version": "2.0.0", + "description": "Execute JavaScript in Koishi" + }, + "koishi-plugin-eval-addons": { + "version": "1.0.0-beta.9", + "description": "Execute JavaScript in Koishi" + }, + "koishi-plugin-github": { + "version": "2.0.0-beta.9", + "description": "GitHub webhook plugin for Koishi" + }, + "koishi-plugin-image-search": { + "version": "2.0.0", + "description": "Image searching plugin for Koishi" + }, + "koishi-plugin-mongo": { + "version": "1.0.1", + "description": "MongoDB support for Koishi" + }, + "koishi-plugin-monitor": { + "version": "1.0.0-beta.14" + }, + "koishi-plugin-mysql": { + "version": "2.0.0", + "description": "MySQL support for Koishi" + }, + "koishi-plugin-puppeteer": { + "version": "1.0.0", + "description": "Take Screenshots in Koishi" + }, + "koishi-plugin-rss": { + "version": "1.0.0", + "description": "Subscribe RSS Url for Koishi" + }, + "koishi-plugin-schedule": { + "version": "2.0.1", + "description": "Schedule plugin for Koishi" + }, + "koishi-plugin-status": { + "version": "2.0.0-beta.13", + "description": "Show Status of Koishi" + }, + "koishi-plugin-teach": { + "version": "1.0.2", + "description": "Teach plugin for Koishi" + }, + "koishi-plugin-tools": { + "version": "1.0.0" + } +} From 661435e6c04c74ebc382ac867c52bdaa37115f65 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Mon, 31 Aug 2020 16:03:19 +0800 Subject: [PATCH 11/22] feat(cli): support multiselect plugins, remove bundled plugins --- packages/koishi/package.json | 12 +--- packages/koishi/src/init.ts | 120 +++++++++++++++++++++++++---------- tsconfig.json | 1 - 3 files changed, 90 insertions(+), 43 deletions(-) diff --git a/packages/koishi/package.json b/packages/koishi/package.json index 05da115ad0..4bc743caa2 100644 --- a/packages/koishi/package.json +++ b/packages/koishi/package.json @@ -8,7 +8,8 @@ "node": ">=12.0.0" }, "files": [ - "dist" + "dist", + "ecosystem.json" ], "bin": "dist/cli.js", "author": "Shigma <1700011071@pku.edu.cn>", @@ -34,21 +35,14 @@ "koishi" ], "devDependencies": { - "@types/js-yaml": "^3.12.5", "@types/prompts": "^2.0.8" }, "dependencies": { "cac": "^6.6.1", "kleur": "^4.1.1", - "koishi-core": "^2.0.2", "koishi-adapter-cqhttp": "^1.0.1", + "koishi-core": "^2.0.2", "koishi-plugin-common": "^3.0.0-beta.15", - "koishi-plugin-mysql": "^2.0.0", - "koishi-plugin-mongo": "^1.0.1", - "koishi-plugin-schedule": "^2.0.1", - "koishi-plugin-status": "^2.0.0-beta.13", - "koishi-plugin-teach": "^1.0.2", - "koishi-utils": "^3.1.2", "prompts": "^2.3.2" } } diff --git a/packages/koishi/src/init.ts b/packages/koishi/src/init.ts index 44f48449c2..1400c94714 100644 --- a/packages/koishi/src/init.ts +++ b/packages/koishi/src/init.ts @@ -1,11 +1,9 @@ -import { existsSync, writeFileSync, mkdirSync } from 'fs' +import { promises as fs, existsSync } from 'fs' import { yellow, red, green } from 'kleur' import { resolve, extname, dirname } from 'path' import { AppConfig } from './worker' -import prompts, { PrevCaller, PromptObject } from 'prompts' import { CAC } from 'cac' -import { AppOptions } from 'koishi-core' -import { omit } from 'koishi-utils' +import prompts, { Choice, PrevCaller, PromptObject } from 'prompts' import * as mysql from 'koishi-plugin-mysql' import * as mongo from 'koishi-plugin-mongo' @@ -16,7 +14,7 @@ function conditional(type: T, key: string, ...va } } -const serverQuestions: PromptObject[] = [{ +const serverQuestions: PromptObject[] = [{ name: 'type', type: 'select', message: 'Server Type', @@ -120,6 +118,11 @@ const mongoQuestions: PromptObject[] = [{ initial: 'koishi', }] +const dbQuestionMap = { + mysql: mysqlQuestions, + mongo: mongoQuestions, +} + async function question(questions: PromptObject[]) { let succeed = true const data = await prompts(questions, { @@ -129,31 +132,97 @@ async function question(questions: PromptObject[]) { return data } +type DependencyType = 'dependencies' | 'devDependencies' | 'peerDependencies' | 'optionalDependencies' + +interface Package extends Partial>> { + version: string + description?: string +} + +const ecosystem: Record = require('../ecosystem') +const builtinPackages = ['koishi-plugin-common'] + async function createConfig() { const data = await question(serverQuestions) - const config = omit(data, ['database']) as AppConfig - config.plugins = [] - console.log(data.database) - if (data.database === 'mysql') { - config.plugins.push(['mysql', await question(mysqlQuestions)]) - } else if (data.database === 'mongo') { - config.plugins.push(['mongo', await question(mongoQuestions)]) + const { database } = data + const config = { ...data, database: undefined, plugins: [] } as AppConfig + + // database + if (database) { + config.plugins.push([database, await question(dbQuestionMap[database])]) } + + // official plugins + const choices: Choice[] = Object.entries(ecosystem).map(([title, meta]) => { + if (!title.startsWith('koishi-plugin-')) return + const value = title.slice(14) + if (value in dbQuestionMap) return + const { description } = meta + const selected = builtinPackages.includes(title) + return { title, value, description, selected } + }).filter(Boolean) + + const { plugins } = await prompts({ + type: 'multiselect', + name: 'plugins', + message: 'Choose Official Plugins', + choices, + }) + + config.plugins.push(...plugins.map(name => [name])) + return config } +const workingDirectory = process.cwd() const supportedTypes = ['js', 'ts', 'json'] as const type ConfigFileType = typeof supportedTypes[number] -export default function (cli: CAC) { - const error = red('error') - const success = green('success') +const error = red('error') +const success = green('success') + +async function updateMeta(config: AppConfig) { + const path = resolve(workingDirectory, 'package.json') + const meta: Package = JSON.parse(await fs.readFile(path, 'utf8')) + let modified = false + if (!meta.dependencies) meta.dependencies = {} + for (const [name] of config.plugins as string[]) { + const fullname = 'koishi-plugin-' + name + if (!meta.dependencies[fullname]) { + modified = true + meta.dependencies[fullname] = ecosystem[fullname].version + } + } + if (!modified) return + await fs.writeFile(path, JSON.stringify(meta, null, 2)) + console.log(`${success} package.json was updated`) +} + +async function writeConfig(config: AppConfig, path: string, type: ConfigFileType) { + // generate output + let output = JSON.stringify(config, null, 2) + if (type !== 'json') { + output = output.replace(/^(\s+)"([\w$]+)":/mg, '$1$2:') + if (type === 'js') { + output = 'module.exports = ' + output + } else if (type === 'ts') { + output = 'export = ' + output + } + } + // write to file + const folder = dirname(path) + await fs.mkdir(folder, { recursive: true }) + await fs.writeFile(path, output) + console.log(`${success} created config file: ${path}`) +} + +export default function (cli: CAC) { cli.command('init [file]', 'initialize a koishi configuration file') .option('-f, --forced', 'overwrite config file if it exists') - .action(async (file, options) => { + .action(async (file = 'koishi.config.js', options) => { // resolve file path - const path = resolve(process.cwd(), String(file || 'koishi.config.js')) + const path = resolve(workingDirectory, file) if (!options.forced && existsSync(path)) { console.warn(`${error} ${options.output} already exists. If you want to overwrite the current file, use ${yellow('koishi init -f')}`) process.exit(1) @@ -176,22 +245,7 @@ export default function (cli: CAC) { process.exit(0) } - // generate output - let output = JSON.stringify(config, null, 2) - if (extension !== 'json') { - output = output.replace(/^(\s+)"([\w$]+)":/mg, '$1$2:') - if (extension === 'js') { - output = 'module.exports = ' + output - } else if (extension === 'ts') { - output = 'export = ' + output - } - } - - // write to file - const folder = dirname(path) - if (!existsSync(folder)) mkdirSync(folder, { recursive: true }) - writeFileSync(path, output) - console.warn(`${success} created config file: ${path}`) + await Promise.all([updateMeta(config), writeConfig(config, path, extension)]) process.exit(0) }) } diff --git a/tsconfig.json b/tsconfig.json index f32d0e2261..5bbe1c23f1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,7 +24,6 @@ { "path": "./packages/plugin-github" }, { "path": "./packages/plugin-image-search" }, { "path": "./packages/plugin-monitor" }, - { "path": "./packages/plugin-recorder" }, { "path": "./packages/plugin-rss" }, { "path": "./packages/plugin-schedule" }, { "path": "./packages/plugin-status" }, From d79f8efcc37041ca76bfc47de30f09a15af80c1a Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Mon, 31 Aug 2020 16:43:30 +0800 Subject: [PATCH 12/22] feat(cli): pretty code generation --- packages/koishi/src/init.ts | 50 ++++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/packages/koishi/src/init.ts b/packages/koishi/src/init.ts index 1400c94714..3e6d65e76b 100644 --- a/packages/koishi/src/init.ts +++ b/packages/koishi/src/init.ts @@ -176,7 +176,7 @@ async function createConfig() { const workingDirectory = process.cwd() const supportedTypes = ['js', 'ts', 'json'] as const -type ConfigFileType = typeof supportedTypes[number] +type SourceType = typeof supportedTypes[number] const error = red('error') const success = green('success') @@ -190,7 +190,7 @@ async function updateMeta(config: AppConfig) { const fullname = 'koishi-plugin-' + name if (!meta.dependencies[fullname]) { modified = true - meta.dependencies[fullname] = ecosystem[fullname].version + meta.dependencies[fullname] = '^' + ecosystem[fullname].version } } if (!modified) return @@ -198,16 +198,42 @@ async function updateMeta(config: AppConfig) { console.log(`${success} package.json was updated`) } -async function writeConfig(config: AppConfig, path: string, type: ConfigFileType) { +type Serializable = string | number | Serializable[] | { [key: string]: Serializable } + +function joinLines(lines: string[], type: SourceType, indent: string) { + return `\n ${indent}${lines.join(',\n ' + indent)}${type === 'json' ? '' : ','}\n${indent}` +} + +function codegen(value: Serializable, type: SourceType, indent = '', path = '/') { + if (value === null) return 'null' + switch (typeof value) { + case 'number': case 'boolean': return '' + value + case 'string': return type === 'json' || value.includes("'") && !value.includes('"') + ? `"${value.replace(/"/g, '\\"')}"` + : `'${value.replace(/'/g, "\\'")}'` + case 'undefined': return undefined + } + + if (Array.isArray(value)) { + return path === '/plugins/0/' + ? `[${value.map(value => codegen(value, type, indent, path + '0/')).join(', ')}]` + : `[${joinLines(value.map(value => codegen(value, type, ' ' + indent, path + '0/')), type, indent)}]` + } + + return `{${joinLines(Object.entries(value).filter(([, value]) => value !== undefined).map(([key, value]) => { + const keyString = type === 'json' ? `"${key}"` : key + const valueString = codegen(value, type, ' ' + indent, path + key + '/') + return keyString + ': ' + valueString + }), type, indent)}}` +} + +async function writeConfig(config: any, path: string, type: SourceType) { // generate output - let output = JSON.stringify(config, null, 2) - if (type !== 'json') { - output = output.replace(/^(\s+)"([\w$]+)":/mg, '$1$2:') - if (type === 'js') { - output = 'module.exports = ' + output - } else if (type === 'ts') { - output = 'export = ' + output - } + let output = codegen(config, type) + '\n' + if (type === 'js') { + output = 'module.exports = ' + output + } else if (type === 'ts') { + output = 'export = ' + output } // write to file @@ -229,7 +255,7 @@ export default function (cli: CAC) { } // parse extension - const extension = extname(path).slice(1) as ConfigFileType + const extension = extname(path).slice(1) as SourceType if (!extension) { console.warn(`${error} configuration file should have an extension, received "${file}"`) process.exit(1) From 753ca9f2b5856b7ac86e51c7c0ddeb2967311abd Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Mon, 31 Aug 2020 17:27:21 +0800 Subject: [PATCH 13/22] chore: update readme --- .github/wechat.png | Bin 126667 -> 0 bytes README.md | 8 +++----- 2 files changed, 3 insertions(+), 5 deletions(-) delete mode 100644 .github/wechat.png diff --git a/.github/wechat.png b/.github/wechat.png deleted file mode 100644 index a92bd138ccb074e88632a8fa7bcb20e7ef5ec31b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 126667 zcmeFZcUX>n95;Mf6

MOGao?(H=r*&{i6f_K^0DP+5gkN<*c+w>CmZ+IyGw*3^7H zXZQ2G$MOFCKF|Btb06+oH`jHY=lL7o?`Qq4$JZ2OsCF>yAdyH^moJ@HB9XRI5&u$< z;ZM%EWsT#vtv2T_t5Dz{SBl#{BoZs>@_8v0$I!73Cr8zW;`Qk&apeK0&8MY4i;^FI zcPXcvG^_L}L38wav073}Ua?#{MErP;J;iYEK{^K>e=UpP_o9okB9 z=;1G+zO~gJo8R(+%e?)elD*4wqHYXX4)dBu5qoaB&1-fUyoog-ZjnUl7R=4+*tGGZ zl_eYh#t)=G3fwdPxp(y3mW>~7KCa&Qopk;G|Ccak6j*)aZ)k5%3^|;WDCR6@kz(=c z{o+GM1A|x&b`jajm$zpgxR_k9af6b1_POPaUM3-i{5-}XvF*%$)Xc)kTgYiD`ldC* z2d#w~;v7<)7k;XSRvdjW_1`mH9{k-bJQ6AOwk@T2uWF*WR10ete-90A ze6&x%Sn@NzrTi>`+}zy_=X@$FQ-rOircM;czmxn}fB%6uo{FI|b>p?wTw${Mqw!v3 z0y3tDQ?mqj`IRT(fosP8o2QQ2eEwYDk-raHqsxWovW~j#tQs<^vi(WE@#evh|1J?s z_9=y>V997>LWfTLj>)i7k^XNAt8LB;@cUf#k>HTnNxVn$>Rs;VDbWlJw+&(=0wT5% zfA%g;Fz8#OA{CW&X4W5Tp{vV-THJB?IjUtZ@z-^cLZTPL^@1^$~w z$*TYPaU{-N|NSLN_93x9Nu-avBmdhB>F58u;{V5&{PAgc zVfXdJ+u;8*7yRFI+yD1dh?)97wJ84Y=>Ff){oh+&X{n_8a?&#vp zR-xn6_+8myg;bo9BHz%^AZ*orPm4P!Gb^hhNu4KOi~I5`J$mih{iR)LUS4EOf>x4q zb8}a&UR93}cY7TYa}8N-wOVs2GV`nVam=-NUXh_Nt`5v2i5WD7+?A$mY+qoL+ z`0a+O52a`q=1SjWy(}JaHNy4pV_!z0+M}!QayOGmH8QCCA5C^pU#n`AY~YvA$<4(R ze*XKRZCuUHF2_||H)uAlq(p~`iYl-i4^?=Cz)TrLL-}~%w7fDt5#a2vbFubr)~#)q z@rSHPJwnit*x!=3mqok%=ayd|66s> z@33?dWyTMNtRzzP_IiALH~#^zLwMB6w`a|!d%q^8rK#)agvZJSN3E?cR~SBZJ2q)6 zY}X&4KBBw66x5ie%d%9`VcFE2p)cUNIK@5etw;Z}s;Vg_NGCDJtSK=e;e;)xkdQ`` zu17I0eyh{(oqoCRa8{Lgofp-?KeoaG#?`Fajy;bewHfhL!>4}Hf1L!XIj3u6Wbh?w zJBjprM?_UzzRloBQ0Yp{&x(q@6kCb6nbsd)ozE+6c({Gft&+-{RwiHKBHR;$blo-H zp1l)T(O=vB>49AZSDx|gP)%mrxc2F5XXKT!7@b$=ngl%7T=C-x5s@2whQF!>nzW*` zv$MHdmNGEdG-9(abX>*-@PZ$_lm>=|Be8ESYl{~;lg7r(J-3h~{c892SrS-P;ekm= zYpF-6`_u!LA*{z89A_No8q`M<;=+{-4Go7}G8p5@HgCf43Yjkz6jj7rkb^Q9`ZIZ9d&%6y)h&z4y^y|=2#u;I< zm-JqT-d4tb_#hY)7snwkULqSPvlX|Kay^5MllR5@GH)8Q!QWwjmshnLSx6-39sld8 zOpT4x8AGq=CMR^I>q!TVYgw&4{M7B?aN*`%*FrV7;Y9bXJ6SIZ56t{K?n8mK$hxs` zwl@8Ui;2--b|~yNq8lh8rwvg^kc!7o7 ztE1b$Kkoh(@{`=$zfbcEI?u-ET6QEpJRh~Uw0&W5 zMc@<0E^4W5n6twe7h9L{r|Vxetr*sPb{&2l7MAku+qZNZO-;>nc2rv2>c5r^LSi*< z+)!0lj~f{=-AhMzoQo?(wl%P%c|IdDQYt4W2lGP=5S|tjOj$NmyCAc4OXl&HFJGv; z%)0!tB9kV8BWphQijySs=#m>fa>?|Io#&J$JBzvewr$>&qLFnGCFInkIxB5;AlD7s z$>N2Pm6@uh`UGVGyAhSu;j13CHPJHq;@9S#i#0!3MW*HtFlbwS+E&o*xs%_lk+XPp zzAaLNUR%Sc=e&iTLAH_RF5D#=k>_OwG)8x=l8fuI0S)_n+&SbU&*? z;%!~{#xHNtQJ_jAzW7cpO^2!V?-aIzqPBL}jvYJrE!#ivG~8_Am&cPi73<*F*RNlH z^jKYcxQkS6L165HFiF;Txa4SuLsvs%$_+q#ew2&KH>adAEF-U^66=QRzdYX4ZzcF= z=HF-Eruus0@zxxMvzA(NqYNtQ>Z2%6JGYTZVbGih!yIf;GGyABPcxh`Z+0_XbWO1Q)SL57t#XTKqL53lEnV~@gG5p{MBkH!OJA!UUWrPHpikZH+`nH zwRQFrIm^(ugLf!z=(5NkJZq_9U=Y)vVK>^CfSwqA@%av}exHGR!n73enVCvDX=lgY zG>xj_HJk5C2JL_FY<^T!743qjbS2Y$E=e~5vw{Crh*O>{Sx#kk`m;+uTr~IqIr^D* zX+DY6@uX|A>B(LWeuoJyCNXDzeyWwBnrP>~=VxbYWLUmtug*WHL5-n0c!sl=WYS=8M3 zDQ(OBd+*nin5x%8Q!`EKj)~1T>$m2Z1>u1)HTbN=Dv5Qiw9>T#qWxt{kVxmZfHb|k zF}=K+7$0AawZ!Qc5EOKSd%w@4FJI1Ah6*Wf-@ZMtd||RHt)tMPJe%g+9dg$Od_l)n{B&Q;WUZ_sx!idd$Aii-sjN&9$FH@lKlCOQ|~k2D}cP%XFeWKSaW# z1Pd^wprC-8ic+pl<;IP-I&O2S6&mb1Y3d>l6VK=;NF*z6eW%U1CBd~H)<-fJS;=#{ zO71kL>;3%x{hF+-Y_k)ypNv^!a?OSP+H|e%ysb;~<4Lo#whfJqUQ9E1jHD!gy22C7 zD2`3iG-B_Abeu2u_xEGdGYDF}BX%MclQ4g5#a4!c2j>PQ)-}?K=R@!&19~qYs*O(k zTVzs2O-=1rEARj`dD2Fb$v%N+$=D%E__%IN#r{hUk&$Uwb)z`tn2hMb~S>~DH z4z&xs#3dwjF*}KgiJ77$Si2q0qbWas{d#tJ*~o=MBB&zfgmNsgk4;TYl~`%7nVKf< z-h++vWg_B0tcht$roj?+ZIfQ(WmQ!`t`O`_L2c*Z8126rQ_jW2_~qW|vUF}xpuAwm?Br7SX;{7)5({jS zwzRaAuJ29FR69T3R(L!3iAl&w9-gM<$x@cw2N$l{+vjo@j-5Vw^yt?jVrd=Z+ZZW5 zdhEg=_xY>{lb^3ov}PIk73rjrQPRHypyXg@kCTlkD*yV`1Yiq;mB+cW13N?oQzkrF zG;^yqEjyckZoKVmukr-8#}I(g&mTV?JGa%gv~^ZbE_mDj7SzE=+Su3xagrOGnbj3e zmLy@pY33Tyd;R?Rli%s@?F!GAs1>6d6X#oLsO;`ul3F~kiPnvxC&cKRb^4y2AR~jY zZ9J9$m-H5ZiSf3)ig9}x&Ns~m5)u+l9zQ;G%_y>XY00^QYnOh^staw!LfNpbY4^wb z)`EABkfM|AluSkG_c#rip&0=S}<_W6%l9H;bs=~HI zFaFNV=q|X6unZG2Gf$6ws2Z#b7jprRiCtV=jHh9H z&?XT+7HYh6>*m&M6Mq63jdzvm4i66(EcQMX(EGd8ich z+F~Dpa8ddU4P#SF*PJWE#ojXQ!Bv@w0adI9p?l6GqL!qVk}1mUy2P9mLiRHCAjQ`D z+S=W%dTg3OSzH9eJi7c51MDPQG}ZshK1>ggqo`AaD%!oRpp1FpD{a( zq^+FJYiI=IkQVcNJ!`;Cues1 zUks9>fq{Xd2aiIw*}V?Spo)Is7|oqx+MK!`Q?1x)n3Oue*?0`tPc(&*`h*Yxvp`}P z8pTQ7754#xt52Y=`A!s0iUTBZWE*;P7C9|7q`A+C^E#gxvr;6qLIFu9e8`kv>{Q|89*f9&71GV+}3yiv`v?!Qtz*LVY{Q4NryWIiWt_?0ZT zqP6v3jnN=9Je1D<)iadR==`T5hBuRjT8Lg4*S}~tzJ;8+9-v-?uGgasXg~cWU;=O zqiyWurf?fM;Ofq-7U+D)uxF7lthe>Xx^ zaH(J3L>h8FUj_O)V4Xi(Jn43Izn0Z~wBi7aesVG=K}ZYmcmct|PK`^AK(RtL1IA$5 zOeK@{g>xvrCO=*s1;RUJ(q{U$aDKh=jAPBN3YX@!Rzg3rsS=CBQ`!oa56)oy>eNC| zx>f6~te4j|hkd7tTA&6*P+MF3IxsL6{cNbPYc=(vFXLs9=^;9TkqI08{qw`Q_c436 zemOOhxa)PuKSYrLuQteQt7^R$1Eu5O8S`{T0yvtkt^8YRZ7PYaudjbIpONHw|59CD zVL``)xTKhS>_boi0HPdlor)L3)sfN!@J`n&E69BNYHdFFt$GH7SuKCFO$3edW@nbkTu1=eZtl7E{zB zf`iq!S*|b1Z-8y^3D5vMWuxBCi=hk~HB&`bHx0^(!R_0}QRZR=4x%|JEBgZ-#vPXN zy&gVjCHVTy8+w27P&wZ2^cM4zCr>Kr>FF5H3d43v^um;+ ze*7RRPgqyL4O=f6qS!XkyJj&7(Bkxlt_qiW+VSu!Y7A!jT!RqWrS7kCd| zajZM=>AocZI>Gab{|&s2Jh~YTk*FBN6;Syc95b1p+sE((eh^(A%ObE>zmu1q@waCO z7V5bOBxCl!?pSNQ3kG>Vn7^Wi9mDmzE!T$Oz>Ae)Zk&HOtvA zT)dk|+TIvFTANstpQCe$SmDbHlR<6c6R+5NFysXgF#rf@r$hhE44U=-3{YfISzdLw zX#Y@SEjc>pOw>WBFfuVI^AA3Q)oPfzV1~Y8r5Ada9?igO3cm8ZQDi;h(=36 zs8rBLX+$hTV(rIUV?Yz*^V(`_qab#Q_2vLk0x>OBPi)O#cthUBhsgsRAMYqshiJR& z0c^vl=fv$2GH!&&T0Q||!NN6^W94f4gLG2~B1k;BwwVQ5ESkMZUF;7a*KlP>MUZF9 zZ%dBb3Lh@Xeez@nfp<=hlSQ0Yhk7Bg)+aHG4V73>pb7b?wb&((K+Jf{Rh_iFIt#m) z#TfTT|0@g7ZFHE2ho`r0rL)+DLrhF8kxvz%t-x+{c5Qi5MwI>RS#^k{f;}#bR4C)b zglRi;#O&lLy0%}}HDhhOYfau`bySz@0Uhx`A!MX`Z(8&2L@>KA>Y-<|s>oRa{1ECJ zcWGIS9@R*MFQV}hr2?gf$t5;E=dFeKy z%zgt^5ot-O1t(1#7S39px53dIN{_O4n!63(Rx;I@I z#>#&@_Y_!GOk-nX`Xu@0O{yv?@G&mt{4Ea9DRv$XykS}MGk_xpD2=)tdNSm*_;?!8 zqbCFeREhnbX;8sg)dpgqmV7<3x3`y%TF%oVI2Ni>qR%I3EbqJ<;j~cc?I3z2Bqz%~ zfBxLm!a^0pi4TLMy1ya)2W%6_2Ly-gzY9oDfR>fT=~Juixj8w*0qQz6ZFfqUPSMB5 z$6qruGyApk)MipiJHZ)!4ghaoyY>#Uc7;B{Vc&9hl?aAG)BCz#HyLdD#_H0XGOkry zSJ#lHE8(`j+|{&gzZPv)Zu#?rarN~o6&r#YgsdNZ)3yc0{*MQoJ(A2WxGX%K2Y!K2 zqNWx|s5FP9z2Y4vI~8dtj|_i*c~~>s_@zPh(aXWX!NjtvPf|~N!$l5!L{w6uwnFB= zD4GVej%qXJi#t6*^x|8I@86#vjPP*x{*(b|18N0USZlC)>C&ZY0FUSj?n1eMfB^V8 z5MmhkjbETB-)fxraJP>jbl}Y2{w@OWsG{$+x3}{ZxLc(g(|ciI9S$Fy`6tYzkiEZs zS6Scmcf%}{haZ-C<=N>5$(RkI%>Db@zpLs5IzQo)oT*?H)c@0B6d~3#!2oE*AmQ#R zw*(gM0;lcX8+6LrEi|IVb8?IxHQ&#I?qi8D5xdp8aasPY5*{d9&OMLz+m1D#s+a*NPQ`#LaIp&=$ZJa> z9t(!st{{I%Kg5>sXTR!u8UBT_-$pqFLWYS~|4!-w@pgD>C*kv8&h%vW6pWm(TaKHl zq2}~04X@}|x_tRDKHNa?1u6|3Aced;^j_9BHivcMa06xg+>HJfJdmX&WM6U?*J~^u zD;e#Ud6Z?>6cl_3dc0b?zOb$!*w>?#>oj8sv@hN}4IO({p5M2=^(tVE0%wJmTgcEy$PQo`1Q zW#vLFS;%tdWa+RqH&l1<%2P=ev^zhQz?wn}=I!0>3qsUkTPKgZeKI^~i$2?0U}yHl zg`R~attHFItoHq7MXt^RbQP_1@I@XYOfq%UCUet$lMq1IzyDk#ZN!-UU}YGmW6K+4 zwFpy&7gXp>k1y{ZeZjW4F6~8G0|avdQwQneCxi}YPs9v4l*}**h5c5TJL)*8%^ieK zXr6a{1=){E=P7V}^Zk2A9@%qk?^G}BVD_ioOsZa;&f7wA zy`1Aw%S^cg!g7wypn^qLiFRAwouH~g|E@`O07|S!=`BxD0*s+WfK9_~y3)L7_V*BX zPuVbt1Q1pT85P5On9<-wmkG;>_vSt4g$bsdCNw}@0tJsYrs$Rb{rgu~yGnBDR^=)z z)Wcp^@uzimP1;QfRVd&Xzr^I^dO&;v*aBWsF$!KYF)?`+wf>KnQW{UK24tE8Ua(P# zZ7T(u;@`bvO|;MyQjAwe8C;$yyt;*qQZS~818hVJ0X z8G@wg1qliL?gS_i-R-&eUDBkF!H4)s_jrQ)$TtD2p$a)KbU1*y+*P);ldph9-7s~*P+Z1%cyKx`+RHZmG!&N}|+`9DRgRiO8yMKizaAi0)3 zITZN%^>H4aw7P|y|Io-TKN)?~LQwT0r5rdr%D! z*$#IziaJfzKDj06bL^Xq(}UsLiAT8nBh2 z9$%I(+sO9;nW0ODh&ZGU{0feSoEx8#GX6TX1*#eW`87 zIexqv>w@S|mzgG)hv(xG6Gt9iSmOTB?a9~ae5yGO`)A|_dqh2YvvgNdT3S=r`kDh3 zi@4T8yNw?swT#My!LpekBQ>F>h4AWmHYRHvhBjQ{wrmPM2s!b|zrku=O2Tg^kb=OS zPSKp~?C-pkNC=E#GGZr6mM5r7Y0A6wVT3d{#{+2$p{v%T7pDM~veumX8qEt{o=d z{Yit1#dYfbMA3`_rjLCrbhs|g3>P3gk73rv$%P93N^af3$%SQA4UL4OvVq166CH5x zM!HH%LAl-+6%|q6*IKS-U9_nZoq0y6JW$@NGIPHUr?-tGnOKuoGRM{7I#Wqh(-Ea3 zB+0@5nZTNUNmm(Nz<^l-NPznM)|5(j5o9M@m`(TPi)>O6eCS(5$Nyz z*7>i4g5qEiBdI$CNr#|F;O)|1cl+JR1O1fv_v7jxh=SueP2p1}d1x=l0I*r|sbfvV z#vVf`#J<-uwBQoo;i%#yX-ZA7m!fK)}dr&ACZPL=a(W z=MOh;P7yf7&tpaZ%O+h-&5(=F8DgOOUPlTxxOV?b6)n{v}aH9+1qF0wl;~bA2r9*P$H}%Lb41IwB%XmOKAE z2=H*mmIh@f?Qw}{qtUciqIXn3x?RVZ7Kzpy4F{9SMdbZ=;~#u z#do^Ii6Dg%@sgsca}_DQo^8Jf#S6yG2=C$#h=iP$VH_W6|sMmdTy6Iub-NWuEra-lgHEvb7fV|@kYvlAzGd+m}(oBh#a z$B1;+$II~Ve*U~7HuZ4}1pJ(S|5^9drQw${%r#VJ+uwP5ZU(@ke(<>a%>8l?WSSJP zslhsmQ1G+y!u0#^XTLssfL$yf!heF_rpFP66j5ry_~E(2h9I1&)j#YKgy2?iJ8iMM zcsm>{kVYN%e+HGod@{ta0m32D6G6Q?^hh>=^jL`l)2nOA88@;gv!!!k90#IXtTZd}rb=%C(fa#dPXblm(8wCepBf=3e<0wj0 z886BMQRtJdCS5y8g8Oa%%Vt+UK$WZv6H(_-OQv{RVovV`tU24P?>Bs~Yw@D-zK=f| zTUjKkFUiW*q0b%%TSg1L4l8NMssyzO@>(|Jh-=61uFOHz_${&S`kYxz)6UM0C>KcY z!9^=DFL0mQZVuC7qF|JpOS^zyAbA5Ij8+hi{Vv^w zC>LRvcdgBz#YYO8SNycO0O@(6blqKSu3njlYCzH4v2&*#oNyxO!sMA?JiEGR7HYN- zowUv2pwser9+)eOy(lbAXqPF(Talre%r-r;)$HU4EpDQ?m9DQ&9(Bv89GI>kq78G{ zy`yHj%bLIPO1F|iwZTrSw`?Q-2sy6_!qYn>^?VTyH)_#v_8|LPS{ zSCevbREak6*^m)>_7Fy_8bK!%hBTk1%W`rm=gxgTun?Vv^+j}V)ZFAUUn(LaP{C80 z%#l{4L{uzO92H;^vOW*(DoeD)sEQdl>G-Kr)1iz$LSYI-%*6kgyb@3cJ_b3T*Z%%x zwfDJ+0FdMKEi}BNV{i26L&V(*i06gg6*@mSxHyA2$Xr|3ni|Y0z!N>$y+~ul=5;#D zo^7{!&|iC)Bw1xOp>|_4$(G395a~2%e(Czy*#(eo1we!1#eQ79a%BT<9kl!)1qofx zO#`Yk{Ob1puVhzc&>bLETjs}fqh;BmO2lXDXSJ|(E_4^n*aV}w!XkxLkN~{}xZLmK zyQlp8{KnwZfsu7YWD?yz;r;s~CkZ)+*viP35LEiJco4M64LeO;oxP4)`i)}&Jb&;CJ*ObiqMqMRtnpuYO&8zar zxe{h4s@X*WM`Qpt{3x3Nxr$acA{d1dqtd#W6#5pH(L3@TsV=!8pBb@R6O`k;VtFEr zt#&c4Y}+zl zv&T2P?m%iMC^$+eG7Hg+)ZT~J5q%AVQ1o#Ea_vg=_H{N_uJ8$Hg*PsKzz=?*NNpwp@*@UA*_CkC@@pihd zcrLCkfw248*$3Y@O*x99Sl41@LnXkTi~juiT*c;?@ls|)r3iM525%==1@XVANDH?C zj38`sNR=eHAPvt)>;*y*1S<_Zc1a1#i3lw3S&J5@r7(tewN#Yru#%G5ulJA-mVz!8eoP@W zUXqnIMCo_-+7JR8LRK;v^Biu5KD@PGmk z0RJCwz>)fatyk&IB|_uC%%@jff-H(1EsC}lZnf|PPVfeTNlGh8gp1HJZod~X7gVTR z!{4D!pIHwR32CF>^{{AKBL^U}s9(772y7P5WT%-%PO28$z)=k)OCBx|Kq5`%)wZbA z@e^5Aqj0B-$aYY*7}z#I3U@ncVye@Qv7XEtCM^v8~w7)T>P% zt3g=Dws5%Y5(gxwbeci(A&6l8LTkcakNo_ohT^EN$8MLXC?6kRh%duLE_~p3*uf=H z5k_hvCO||?FoV%+w~O-gH3;(#0H=?-Ees-yng`Ti0i@KB^0xK7PeULyIWSm$*Fl()H}{*tf9>82|Ck z;x_AnFE00{C>JIS0v0 zfD_aRo7N_341=4PxMOCb>aCF2PLFjrklM5|0pMyP?~HyOF}1)>&v<>?o|jA8NJ9q* z9bW7OB23E4%FQ3Ed7t@P`Wc19Vx4i357q8h4n4@gpwL_bDSacge53hhFGc`bS6oh7 zVbx$(JK3ptY*B2tBBGRddV2EA)&1_K+zl3l*d`%c3%f3+V<`~6$MXYx7oo9Sa^RNlW zBw*J0HAi@Pd0neE-Ya-X3HJsW9f8mK9!GSqFA_WmHnAY}Gb_m*BnuHgaD7q2brW ztr_~>Yl9wZGBACIrYrV9DMz~?q_cto;R+k<9pGA2ZiRx?p<(=3%azw2g<@_`{|qY2 z4cO(6041+3hv53GwSV^j=t7=Jf-+yWKvh;v7;E-p%|3cMQ=OqBe<^^LKFe((3Ce`V zkNgYv;TM#EaShlS@aomAmOv0TdHIHgk_9mcgN?@*5(c`Ji4;;eP$c^8 z{XW^%C^<@f+=J03qKgDq0$t=augHIgryGisSWO_JAt-wn+*s+y@~Q@R?!BD7!$={n za-L5d%0UCx2Iww;6O=TE4{Hf>MrSFd(Mbr*-|vQFFp8+0$K>8vH3lBfUQfo}M@kKT z(Mg2@gaPWkF#;G2V$Rw9)-RL^LCT$zp~nSDGMHzMRU4TTFFu(A%FsJKvvhj`*m-@ zP}I}k#@jDU*)fUO%Mqz^OdVHMR*QK-?-LFc6_xsgCMz}~zgq5#kq*WItS`Zv<4ztu zdZF*zb3*A7F7iMOP&3y&IQ2vClj_u?Y7u+A%OZjN2%e5F~2TC~gAUnxPpiBv-bQ3l@_i)!EwkYq}SG*uRBn3tPNXfMRgDhOBG z35OAfKP)X%`|l>jJ;9P%f|A2Zcwf1Ll4h<{T{p~IaHkM)f_JhG04LaMFf_M-&t zSvQ*eT^1c*yL#UvDKXDhczA5*-M+kySIh986#=wD0iyRhvg*F&*Gl9%p{Etl99_$w3?Cc1mvT(EsL@ji>E zC}OZ@S7Bn30+)8ZQhMQcl=AKX`kDap17`brxcAJ=j0 z*_NzR%wU~gq+YJ7sCXZTJxGQ2;|Rg+ymv4l1KOPQ$$(opfz~bfyGKn6p7HsM7tbB@ zx4RNL*w^>q`}gmM#uY5ZQOwDTkn|PA8N;I#U+yw;YZp8Or@J{n)>14S;mO*K{jNQ5 znu4188}?73+>TaV^Euifd72&hTE3_*4m-Tw$Q=t6cVhs?=pgI=C3Mbt_lr~H4yn4O zmRWL`_fh5QUphkD4?U!$;U_((J?G`))Skgg!AnmGZ(34Yn+d|eQv1(uyp-R3nVvp; z_)wwZSKYDJMADnmQR9CWCpbC3e0o4Gl5^=wj1uSdbdqUKK~s4-FC`lr+Z6OQ`7_+?Cj;W4GTD(`KdNWwr6~tD$u~CVI)>5A>F*?^ccX0 zatt2_$4)|Y;_7&kl6NQdn&4^s&Y}_x3kxoHV7O=uLEVzLxcG-YUlGpBgU-@)7pxe) z4JrbA@MMcnvbeZrn3^hn{rUnq?E!Jrv!aEWF{Z}&nwD03!mT4u*iyCfJ#7Ym>mha! zxX(bw@8zHqpuoHQ{FZ^gK*Rh30(0?4gA6pjl$ZAbA%8}vLNb8t(YgDm^Kud5OjlxR zBge1vvFDki^x4?(T@QSemBkkt8tQE47b15|tJJ+?fOn7okB0yPLvzl@>7FnLWu>H0 zp;g**gh@$BUH@RESNe-pPkZ~>!JAe`_DkP!-!gEs8_Y=&H#xD0@-z**UJs@2jhIVr ze~9g`fZDsUbS~w$bu3Q@TfyH(T9M{VoEVM^iAF_7FNc=Di;t&3vF)0wyDA|SSNEy1 zk_x=-6aJ41J{)(2EXASQHx;Xi3Grd25OkPeJY(7J)84LCF$km~=(fZUyfOu)m$)jX zOsrG9B9j>87jNG9pfKj#XsoUEM97xy5;$x9#JtdE+GlBM9S_cAdVR6!#Tf} z)PU5+&`kACDcTo-fnMMibc9+ISeT#R1JN@>L@X&9z-t;NT{n8L<^Jv>+u_}|!c4@8 zQed%Jp&x9A9=_=8fhw78!g{7XYW-@(eWezOzRTMW-OlD^k6wlnJq>xoPO}rkjQ!3k zCdTyfdB)1gfJB+T>dKD#P2I4_!ZdAjG`YBSiYRh4pAQWgf4`k6zKwDXCt`c9dMxeX zXONHU-?V9yBMwf#c=4h-E@O7!ibzG~)a>lTxHxvSA;lXv%xv5qe7c7K@_8AV!)$Dy zC-`6c@&?NV?#N29Mj(h4^GlqDp0+z3EGK9)KnZNtw(VHGsQ{5CJ96~sT)}4JaQ8al z!s5Bii8oDG<>kK;wq7p|;Yzu?i^KOO#t0g)9AffqCW00L6+eD#FJ1Y&6W&-W*G>wG zk7YCx&p=G)M%?u@_96F;iZos?Jkj6(2xjX|1P=l&7G}d~FYJPt*_>~~PSil{Li-+& z38MI7k(ByP=5_K^WL`^9xpehvUQ2Y`hY$3S{(YW3dx*M~=6QeMV+A53(~zVH4ie_c z%UN-&Ch7tJhIE>9>UECe$3M!jxIM&a5BrF>a>weMn)* zg3$o1;vMQ#~!F9eZ>{cb8j7de$kkpfm6GH?)UHC zVn3;Ruxughz01tpEgVK|ozI9*a$K1%pN5v4BRkMsH_TY5*;Qk4fRb!wCSv^y&WSZy zs*x$uDeCI_VPDCG@F$etr$V!R#w4;2?trnSrPsO0kI>DhVGonxx$ZP=iDeh`eGnA1 zA3WolrlzS7%|PH?8_!>z^Qk6gW@VWD#}Im{jH<#fC@4_lMOM9Ewj1YY{t6LDQZGKj z*pHh8@kPrmsGDL8AAA`UwO@DD~bw7)j-WNRNT?DbCbkg>3ld^Ku6|H&-W_jz@t*8#S1hh^h=4ko&_`c#) zMvXE56HnIWc0X2%s2XEB0fy&qUcNk(3g)JAoX);N94BW_AzLnEF!2}$#$Bi!QF6z! zZg_Qf-<*b$>c%aNb3}p$Kj{jWfeL$}p`2uRezC&AIrD0Y8V5A&C;PdzXp@qXE`$#* zt8}ihQg~ihQex`_J7KHH4D;1E8SsSgnB!}Vor~R`Up#DRVDR|BfdjYTB^sX&UW#eQ z`P5$MSzoazRoju#cl`5xR|b|>RU6mIle_ox=$Owe@MCyRadBCEoN&kjL55z0%RoD*CdpImBif^ZQBVe`5f3{%dpioiiz_jXIAw56RkgvS72>s69opsC8`aFvVtU7W_h1o3m^3$D~r^wVMzk zh_AKSMMqbkR&EsjSij7R>UyC)7o_g)u`$X!cP4cPb66kJ)_NS@1#m;TX*B3&u$1E(ZVD5vE~fCFQx4AYVwWL7^M@yt35qE_d(8aL~h>fkq+C-n>cVw ztVS#7;F_8X|tgxQ_ol=n5)E>IgzK#)yY|LxNVs)4uk@2 zS(t18gVy8TUh4>n!?b&S&TNSs%X8yJw)Wm7Kh9Jg@5;NbB@)Ttx5eaGf!tK-ryeBbX)( zZa$SeRtD2`*REanv5HyH$)x4wpLaNoGaU-J(la>d4JD!nn?goK<+b$Q?`zDN-RQP( z^Yzi5j-Nc~c_rpEn)xX-1zD%$2S>zy9KWLzp+_$lD!2;=p`;-*RCbb~@h{?32`@ag z4+RC(8ee5-x4y?Wd;C?+&dp^L6r`sSogxKtYTOF`X32lhNADD81sj$$DC}3*toWh^ zDohc+p5i0EgHCs$|I)w8{(lfHrl!kT2qJY3U-eO0bWjW%n0D{pM^M^mX(U#7>rL$< za%E7zZf^iD%EQ1fU@lVU&u@k52l7SDdiL!8WB%Ju2JApYDmM0;;ew&dd!-YuuCBZYytSjhUb=GSGv<%cQhQQ< zTZzc&U`ia6^m+2cv}vTBMeO9Sy@J@mg%brkQB|^nuh$y$_hJIzmwG29nMEwL-9T$} zgw=c(2cBfD`YevBzn2MONpKn{Za_v&2H~E*zl2T#p;*C< z0u7sx#M~ZLJPL^OmS&^gzu{H4#CC#MK7|Nk4@{m>J~_8On1wL4y$7lADf;%l)UFp@qJEr5b@3ki^-?rTGwh^4EX^bgT?SYKvxT zWzk*4I^%*_DFP2|8^{z9zPJu{E{L>RVJEF8D=Vrd(e6-fIX4}5d3NzIZj0>sMSYw6 zD>3JQ5d6IKe4;4)e*nuZxpo}66oCXL1-C?azB`0?+Sve9$vZ#ix;JpXn& z`BOu~UelabOM^~Ps<6Dgvlt{^K*SYLvI|mDn;<7q<2k|y(GzJL92_nk*$syL<=Z#S zlc&Uf>|uSv2Kjv&xS|K_uG%yQpyMlo9aHtnSLZ(bHIY>x%#p}TLc_oHb;k-)N9jUd zML9Wpaa^PfBf1NR(W6fTM!*0?!u{3YqJV+P znmIdQmW`i`@37cOg4;6eE>mlR^8lU!s-dDz-2M1X?Q8Q(=2NRJWs-4qdeHX? zO-4~gWve3HF0golDdx^nXEY^!TFF;RIFDTp29|Esa}Ldbjf;y4M%7JvGC*7_YwOHV z8xIc;4A$rJa`Mx`zpTYX$YheYd)@;~M|=V5m2`~KCx{FR3JUh6bxb=%!PlmwrM5tP z8W2{a9#fsdBd6o5eh{bJ1I7+UNysmmx3?o}_Y8pHZC6&OpU}PT@$u)#tjUf>0R*b3 z2&*MJ|9zF#G&+rPc#Am1Juv|$x`=!V`14ax9j9xjs}_(90Li`wmZ^_Ua)zS;S6IXu z)JH@HiCX>rdByXps(XnTsw*04;N9Uak99Hkm1!AQ9NT*X9waR$46Ax9m--+jS>gSA z;a2ZuOPrkC+{7RgJcL$#-B*_FCF{|n+wPREp25k3dFE};eVN6c5MPF%Tpe3f#7I;O z>>}q-{f~{bq#F@5bYTkKfBxJyZ^?vxxh-%N2g9Ns-_GgWzq%Q-1FpXTEu4$$re-6iOsW(pykX5erfs>k*4{fB!N;`NfV(s_f+VRQB+4;Xw83=7qmiF7raeTX0 zF9?NY(0AWKCjzkIi`BLhaH6lTZn%)io9FxO(}l|kDJd5a1}VK7(G&oDa|@ed+_&QP z=|cp__GSEp=GwMR-TYKgPDaKqXwR8K4<4~npvvd9RD4zhfezx)y?EimwyjctZP~rtt#v0Wj$QRUpb?h@f+{?c2)c$vOxR)e zLpxEiU+N7F+-Cz;;>gvs2c;t>ab;yiVYI2C;gFEf0pwS{{12wy0~+i8{~xzq_N*?F ztgNKV$jV+xC8R>gPAMUqvS%_%MkGR^A}yipQD#z6C?lziLPANu$GiLUJ^%kX_kHfp z$?aU%`~7-7U(e@by-@h``=@>@u2m~Pf3I&AB}78my{CF=vuD8uU2kuBLiv`x?#5`G z16fR5+z~jEUk{XtT<6wSj@87|?Hmgf#QH0~b!+OuUS5Noo&N05qp@Guehs3@VIecD zD!?z*GUp`|+)kZ>oFRJnj}$L+&4F<%i9AIzD`1C;tc?xM^7#M~ad@$Bxx;I~b*ww5 z0b|xl|4j%$v75N~A0)F3%|ryUOU8hGH8>?}#GvaPA$IokC%Vtu7UnQTjuA7Rd-+q> zr8Q+ny`YP?eey8wjlvCg)z!B5#6y*HtB<>WXJEfR)Bm?$Z`|HGIo!1ao6Zc7UZvv+ zMSfUV62L(dji;onYz($$_U9LNRzZUzr`9}7O-(cIGojDlGhdGi;;>QWZdoSqI0ta~ zN=MehH#Ql8KXnlrX`c1a)?b}D9v$Hwl;*w+4LqT-;{F){;AI4BiVew~9+jDr(LqjdSwzzjNW>-sPLYlRtj{ zW&sW>Vqp#WIXQ`9#@hBl0>{q1e<-aXHO?k%J)i7&yIZd@!PV9SG9^fr&py>UcQS*6 zf*g^DVC#3}Q=p*M+OjB7XLxyee{?+l9S%YSYLYkrgxTxFut|UYIKB#i5hx$~!Rx%t z(Zf=-fLp+s(A_x%2U3u^!~Mgm)3a;#R&Qp{b%v9`-av9H8?1!Ll#WN$8~23H4yg*L zdN+HFh@lT8a)vOW*2o06d1J`Ma~;R)ejvv4@X&z+kpK_1Iit>Hx8AR+;scuw75mx? zmnro&uT1qoc|r(2>74(k;lqay9#k(nI#_0BXCrMybVe)V0P119AF5Gh5c9(jOsFv1dpe9eXsD_F|z`pqfd;Bga zvMlcx6%}ooeZh+ZA4ktSjZirG-+980^bgqgP8v^1ON&CQImq^8;DD)V7|a+;;HB>F zJP#IYPu*}b*e9&>0DQDl?AzoOICi|nB!Dqc#)Z!q$Xw=5e~+7IroVnozz3{{k}8M~ z+dxw1-!`DPOgGTgrNIH<{M`Eex`JL}P#J?GR0H6os!g@e^HI5_v+D!|65&Bx+dK=m zmuy?kl#kl#?+<@l@fJ? zS6+|iZa6m?NLx*lI9Bu~>HYW$8BFvwCl-}H#KQ0)i9C_uWOxkLgLtNC^RoSPg3=@7 zwYuz8Q$>$reb;-UUGShaUyRQ2<5C2>m!lEz)TxCq(fp2fSOL@_UZUzxmyr~P5p z3$rbO6A%yuGB$$*NME)F*X_jEKs1d|cyAyMRCFAuaATNf+C-DpX^PBMizmdeF+Bgx z;st<0n}FTKBh-u=K>4)1f0z~a4}@7s0CairrZ+vXo(VhH*>zb4Yydxh0+t0md{JNJ zJ?-VQh<1R$6%n-4q~uwDPsY*7sUm3cM9ui;sjpwLV;}h`Fb*_QSZ!==(Xkv~8bVfr z$j+TLgHZvt^njGW{n=JdDkv-WdzjEBjZeGISJoav4SDwbc}3NBZ1N2oVu9kw#;Rpj zR)zjOhiWirOU(Wd?4?shiSAyvgP)Da$Kum3s< z&9cFIHg=6p+b;pDIeltakcryH{J@kIB z_<@_OE3Psf1RCC9`~Vo5tE)IM4$J!c`L*I@F@=WymF;MtutKkpl9O}NKQj`%3ut;; z{DoiBuIk1G^`uQZW9rrx%l&B8Fj&gSu%gWo0U0&^!f%uCrRUR+o}Ceh1#2*h-`6nK za(-Jwad>GWO9T~g39>HH{QXCq?w%8~fmZznq zt$^WEl9w5fMw3U;KCq`#<#h}_3juJ9a0&>h%Uo=*;xXU?yzv+^jFiilUzgn5l4^v4 z@zIn60Z*J5;0d5UKR>_EyU3i0gM%YhI_0PIDL|y*;o)aXqe@?&z-NAI<67Iw5J?Tl z(y#nZcJ@XzNSO3DF?;&%NEDZnVgjBzWwt*IqD^8b&_?F#urJX%4V~#3eKhFdzEoA! z^0X414NT(6ot>S2dfJ^DD#Y2;2?3~ftXO7wFNm)15b%O2XAhc$R_MPw0u(@t-Tjdp zh!7D1GtAauSx|kX+5f5u%%S16(ZyB+l5pYCyD}{M_wOed2x9t2<3rl8A;Y8YX5SZr ze;mb(4y~j{Soj?}3Tq^g@h5QWSXOzvRI0KQ`xGic2DBiwhi=pYNhRV;yQD}?j*fTX zh>zVGhR2hs0F_OTReB2s@YKxAftg1jAF{KvyId+NDl8!gu58$V12UV5xl3a!gt*n) zA}cE^hwXTvrT^@=(G{FJL4gBYxaneC#GS11GqXs7YGmN)k%I@Lp=}&sGeol}bF@|f z^)<%0=xcvIHShoeL?#dp)ix|aJm=F-7uQ2%=$^$@r1jJN)?|uZd7JWwkVtGlfzXwX zqo}D7;Du0^nQ#i!&B4Q!|M>C1=oc#p2NE@h0AMMxHOCzJ@i#tm>*YsBc>&k53Y=M^ zLdhd0esFb?l9JkI&z{Y=b633wWOiqcDi6d+e0LMV7el^-D8l-qLFeZA=a+Ad?e)Oi zU1&{#(8_s@>3a$&%Sg+*i~Rgs4O0e%Wu^qv2E|v5`PQH1!nz z3!rs9u^%D4{Ej#!U^h1gnd>a14S)*+SI_Hp6G}D8GnK@Grz2h%`{lO2KB&3S7E$4| ziC=1HYNAC3K?BtCG^bCW&R%D|po-rg7Z(@SV6Tl{6m2VbDsV)^fQ|%(t?6fdT82tD zZ`#Bbum7?{9nZRzDCuCDVx%36&vYIPf+}DqT$`!+PaZn5bZs`xE?4pJcDw}fg@WN<(Zlu>f_5i}IrdFz&i4mv ztSl^e3Az#HP>y#LDsW_$mT`FHJRd?9E)0833A8ytLPZ?zn@UfzE9r283rm1nA|K5T zb}=+{aThKC6FT$w$#u!=VEr*oJYw}Ho;!r4GJLC>9OSc7zal>{5ODtf-$^MPJRF6tP+Oad$axWwm1QRu6wc0fzI)Q2I|DM*b#wb{ zzy7TVh#SmlQ4RL>Am1D)Q6@CS=va?P56g70TW=}Qg707yX|+x0lHK_hY5|{|9D2-!x~Y_$U(Pqa z>7coJ1Wq7!Mh?LTiOTq#=sTAo@Q%S3V-Mha_W79YC@&o~hFw&cr?bNHIRJkhK#xmG zNqzCN1?G)#fRMG7$#nh8qN6sX2&&vkVG@mHdXezDs1&o)l&{0Q1bA)7O-Bt-R3-SQ z3}ga;+i~Q0JY775enY3k;-~wK#kH3e2TjWRG3qcw|AJ=~bM4yE46Cs&9ojSEKi53M z6;B`Kq(hp4J^b#V@(BVCFN45mus+{)e%^1i-#16rQ?YXIW>s%G*wr}DLrj-7-ID>h z{tp~Q=8YlTJz*v*1Bc)A-vV)-A#1B^Xvlyc9QpDEiOKqqIxIZuZDOtQBk9$h?W?+f~&w3<#bQCZr;f4OE~cIs^{6S9*(iG!za1v>dFeVT(LxIDl}=Q zD|5zG2;y)V4F_rS=4<(q1nVRD^*H#krWDBp2;-i0cgKLBtTKU`56aM#)YNdGJIt7o zw-e6m=9U&6*^J~k`|vDZtF6+~cM+cjxTta73bGM``zL^DC|(;e8jO7Xs&<>_CYQH@ z+~M0#pFW*yWZ4e&@jCR`ILJ(;F9Wu3#8DCfp*z?*Hm~EsHz9w(D!!^Y87PS60`s_N z=_DOKqr|_ZDkz^^`7QJHhxHh4C)(FN0fa-$^+@>$!|zBO_-F&v=Bo94*hY=*-MJd) z+XRDI5!uib3U`9T&|@!AS#8~CZjk$>%i1Ok3|JCpJ`P>-PCpr_$x*U39Ix%EzmO$7 zy|1Z#L4B`ypHM~|dL|A6F*DkZu5oWBg&Mh`O0g0?&WikAM1Kk7jEs$scfy#T_NN?# zCJ67@dFZA?eto$K=U)@tvlYkyL{rG-$iyVPDT|i+x}+{(L3w#@^qHJ=)B}ADJ?1E{flZAc8(80s zBQMzm{QZ|aE`dP|Dl9AY6{yz%!RON6C zdVCrJsFeBp^SgR7Hmrl{`3O;1?xP^m90$}k68L^LC_*T&q=bnuI^;vF0Uxqni3PhO zv{Ry~LL{Q?(fqr5MnEHo7bS+o%GwM*<}Q%S$2B$nL4H?`l~qBT#s$s^Feqvy_1fB6 zrt)b)Fej2aTfv`GcjaOcYrLAPI8Gs(;=Csi4G1@MA?Y0!WMu(uQAwb~e`Ht-<7bjM1?i?B?#ujX^ zURf&pKk7<7d%?N}yo@)*}tl(Lc4g1KaQ>P^WTNM!_rq795-O5E9~4} zfYt&;BO#sue96VlO{2q!8S_P%`OL?UQNgoSj997|sruqVQeDUlXe)^tMn#1N11yy7 zx!7?P;tcpJpDqppwvTqP=QD+HWg5S|FAcv1lyhj0mqw%VaP*7SJe=Ymsb z2HKTgbUNijt{MlcBMZ_Z8qubqCOG&%29N+0`p3KpsiI?51{v{~Zz8*VVJqu>^@=W9 z*J0)M_?hTj1sr>S{|)_rnpf{hUwY1olb^sHQK`^zQgz$|&-ACdE`P5q!Yqb{hrKZT zJAh#eK%pa;K#&AJ%+A;d#NjQor^=tHiZ3gc&F6AQ%W4&PBw{?sWuPLoM38rw*<;3G z;QL3eg#m5!JCM8hGrHe~PnrJ$iVhH6@2Zd&TgNwbY!(zuBEA$RdO{pakUUTAPG^0c z#P1*mWf>2(-MV&F$29Axut;;? zI@Yb>Bzxzfea@0lFn|O@~|xCWXre9lIsh*9}$8WE7{QNU{I`nQKf z!>T$w4tK=T?mGG%h#)9?uP-l)OgbP_C58;}@hK4ukx|r<3D#_nifr`l-6*-)d>j%I zOqR@iDMzieoH|#S!4g&Yf3pL{K8BMC<3x-QN#lKPfn*pVlZnKl!|QBEOm{l!EP(Bl8?TsmUZ z!T~@aI?k@Hjn49zL~s&}BT&cy9J)*AH_uQe<&55*9u7!4tnWM;_D**hotU7%Y`jB% z%ag#&;JY8!WsHBXI!_5`&Eq#wF|$lw7CIeJRn6|p^_&yqI&P3S^{of*h{1hseWx%va@Vr8C4s2RTQ&SKl@BPl4 z7n>QjrjEfF6#=5w5l8Zj_YOk#h01XChO_9Oss10aZf-BfM&7bKJ~T+D^lX!OQcJX} zlzjVFxm`sOQGa|>Xl!G%lX~5dZOumruHlg+RR6eE#5aHxk%cke(vpk_x(JXp_E3{4 z9WxvN>z*QLPhbLY+x`W#N6>Tna} zqwd(**;zht;{wmj4zcyO-{OCTvB8Z8{AuIAQhSe{GBtLq7PX?y}0g4jv5<=c$Fce#+ zZ;I0+KCbqEOPLiCJmaFX?+n`tU7?bNJ!V9O9h`uC*2(%K%Ycng@?Z}+4Gy}&O;NYR zqVerp_!QED$gGq@oTeWfwdcc|Bum`srsURNpEewOekh*DY%jY_#)N;cz*geFWN)l@LGVq2HAaGs6mL!Yy4L>9ra+{ zZzjH~A_|j?v+)jc7B3CiQ^u4Uj#)_VlX0VwJrY`=Q4812&pSqoy2$;5 z8AU^M@6n^pUH4^B(GcpvC$T^GWJa7RM#A*;F`%ohIF~3u4Q?N)WQSZj%)DfJDS_&s z3`2$_bMfs{_1&Tpru{bdipZz>+e1n45>|-}I85qMCKRveLhD3WOW%?;((P6SM1gLZJZLm|{%>NsLBPs=T2=;Y;!9 zH+s&N+I0FR)P}RHmh`licR5wU4hD|i95NP*`6Z%R&nUAs|4Cc{spB4@as~Y9t&z2t zK}$=E*mLgvWi0^3b}mYPcCTGW0OmZz;r~&*<(>@GG|c!hP$GUcwL&Ct9u3ga#$#l} zbZ6FLAJtpQ@A>@)z4N-m+PwNyto4+XXIC4HoUi`oh-1~SVrl=esFHIeiFA5@G4GGB zly|6cs5$HqEkF3EBnMxU9>gmIf`p+Lrk6$JxSX z^hFfY_+1%-1K6ql@Cxu5^ygd~H?km4!IRP%FCwHHCY0$p@2#Xc=uhH*O0CSm7f00* zKF3To#lxCDPSq+ss6iX%0(|3&qHo-SR|EO*F0c}Q#36bE zHeZ#T15xD7`&G7*@7fQR?=qB08ux_kjd*t8FbP={WM&`=&;+B^&X&ICf#nFu1#Q=ot5=bfIdeCnHM z)fghVf|H9x3W}M>%8#?Q9BrayZHu^ctKRz%HLW{qy-^`WddQPf&fnK|NJL7V#JY8j ze|w($&X8;Hngt8Caotz_*r+&&)p)zBbr(!i#>t1Wp_{>2H!aJePUumI68p-Xxe)eknO zzu;<$xKwl9us`f@vhhtX&>s`8YWli7yizOrS}GX!g~K#o}|RXb%P2gyF9N z8b=(SMC$-90*4gz;P$v~$MWHcb?CZhJldS)N73#tW6;7W#8G4(k30%ut&vz*Xggj8 zEH?=*hRzLf;@`Pdd5w?#&X!kxcTYyF`-I|~9rj24=*QZYMe6(3EkB&3 zVLK?)6z;n{61E4Za)cEWcz~^gpx5^EQ-X@47Y!$*z`2v#rocwyb3x;vzR;;~^+uW) zn#grTsZ-W4<3$7D37Q)S$(>=9bKn6WZNSU2N3r$v=KpiQ0V3m$32*%0*$d@^{Gy!; z>a~A<9)V!?VRrStTdy54xLH*!* ztSZO$hs}tH*829)NgUQ5jbVAXXtYhTiCKAz*ljvi+BkcijEk&d!5Yk+J>Q|z|DUWK z1+y81sprbP3$Cb9Q*72nJD{XAhPoYqP!NxVNe$sBl?b9CiP&iSHT%kylj3V=T|w!i zb^h~*(w*(nn zS)25vjp}QHeC!732(xjwBVnprQ*d4&6l32`a7mK=Y+DN1??{@j5_QfMY(CsiUG2_4?!42Z4qYUKMzlV3==VVsh(JBF}fr6LoCJKB*1O!LQtD?oamel>oFRtI1##rs6_XEFBJ{iJBHMcYN+BU zB$+f1O^tW4`lWQ+4~yO^^{_znWcJP3P1N`rlJrNJFo+5m}=w=;j8u!F}6=Es9|qHXZ0Y z(OzIN0mvr$dcY06y}ba!rurVm@}RfI#0{ZW)$nZ=Du{p*2=(~iK^F2rdz487eH(>J zD53bcTjOln0m)M*=*TPOmt_2GGQxF4qO;F423d4D@_Cw^a2tHEtXj#fK=X%l-AA^( z-%={^r(V^xYM$9oTs}1o2PL4~|Cy69VFeFe%G(KXO{P|;CbSG+-s+Kr>xlm5sgiPHB)&4S*a3+IlsTH?Ekj zA+cZt8X^MksyqE*s1L_8QHs$uv;hX&f1sHgXOW>m-gAHVDSjj?%=>I6+KXpllw(7{ zI`J+et?$epq<_B}aO%W~MvUlC#rO@JT6YHXCgxkP)NzpE6SiW4vV|Sk@YVTSE16I) zrveB^)$8b3#(+mtW#i#*vIy_kKZw`F#Bkp}HY#A)guAE(q|SNptz5pcF*Mu1Kh8WH z)fM`8!@TrYDhg-j#uWxNMz%!ltwsQkp3n7>b^;bV5d4#y}4xJ88kzwV~#B~J$CXoU&_;XL~;NnfObj?kMzb{&h3 zSKq*(0rV?;9q3ZQohmIubA{x`^ytwK#{=PCgU*`q!B7Ep&w`4#@TL+zE6#l~ z^*(1l7*C|A+!9_@3(1!Vr;Tx__vSWA%YEVA?)9WtU_m#8BG<{0Sbx2=g!*Q=T`S$+ z3K9$ZmnO3dCIHbPzlEIfYC4RdHcx}r;ah#5o2x3JbpcfksQ7Ro3qA%k^WdOUA#x+4 z_)*3H)e>LOxX3RyY^ZZGhg0`tJ^^g`6jW|c`Sh)-y1KEyf5YK28?WUW{2~}}iPMQl zZoGQ^#joV2>3vIR#Y?E!MOTJl9cA0Tn zX`02pUXRntC}*hQzKMPPnBj}OSTaM-OZtoJw-`TEn|@**6=b_9$+j-x#}#I`D5cK% z(&j;@R^7DW&vTyl54YY9rTuj1)Db+rQw+Q@3RA~8sX7#jov7ZV2G>7<41o}|tEj5J zWqTseie5+P(sM{2IM=U_!R;~N*Bv2zB#N7PNtA{3!F-R=`4c_}eY;h%XK;VWzFsN-(4 zULRU_r*-{wO?qm=f=@!Or-1pqgKsLh(AAcEg!C?=Tq^$Dt zZ+E}Ays)LW<~2YN0im#m^8(IxfF*F#U?Bo5ct9t=$>r%RUKa(oege*~m)}&jk_AGG z5D$XL6;@N@2XO$CQ!Ki13+aQEzkPfdHb5Kj_8)e{kZB{^2IUyzW#ec7bZAEqs?Up< zBinQP#V=XeJ)Xe@5slZ@en_ms3IJ7ilw<;`sUC=b5GP!C^5B{QRn!j-)*L8{$#*SF zAv6b|D+#?kdWVJubun&Y-&KGxY;JC5_1aw{kq|}|w$31oOE}?EJ0tCx9fQ@cyI{PNO6Q|jQ672M%QEfL+|~QsBomZt+o}Voa;kWhX5}jW9Q~^m{Eg4K z&G-G;wQkj`9#=@6LN|59sC8*0sD(<5dee&adv6%cZY{Tc>3(_o&U}!|%0rhU58pjJ zdC6RmYZ{@dS*!pDIb==cG;^4xS*5Z;viV)(8&tbR(smR&xbDx8^yHcoCxh9vW zbAnHQnC&}Tll@11GH@+4lKRrg!&A;NN{04BtgR}|Ch2pnqtgKio_07&A63v%#C zL61kcN~|!jocYv=&c?>myV0@2Qf4yNcmG{WneaxoNEJ^_J`V4>6zjz;{u3ikTu zty|}fi{wjabc~-vNZ6%ZJI99KrZ9tS$B*S}U(1(=Kl~Z~#X8jdZD&&CRJHHM%IBq#h4E3C7!@rG_HXIkK;Cb=-Y_Z|2?yJav;@UEXKNl6AN8I-xw5U7Hnz&(p z^+V{={?*B?!SRUpezx~0pt`6=CzJGq(ZWGU4C@3f(1egQ|@&ra( zIOXPp>z0C78@I30banN&&6QsMepq@P0&A9$FxLvEr5!5WPJ{=8ZWg}Q!(-5!_ESV^ z3qiOL>NCiK=ww&D(#6Nm z|58z@^2L?sFLT;oUfkW6gFl?gE6ESHNol>ry}{I|@Rq6Wm{EM`+6B%7ml(v~+TY&s zcYb_wrSI?L(UK_UGqS@}PAP^3eQYWl-^cgW%IxO%6xT6hdHJAMpC|a$)G4OYD?=3R zL`51~X-@Xd^rD=ODJM_xD)S!;*(+auiuP}t?XBhLBZscvKHtcjDEDJ&=?q|G^sWSePJ*Dc*j{;6%5wYq^BX3l zDHKY5eLWQdw}i60?m_0GrlvLku>ji2W7uTGZ-9H%yyVrv?JN6k_8mTTBx57>@>?1C z*UjPMe~f;mvS|do<&~ZhwvE4~BSbMAXW6av{kT!lGkPihNZt!ySTE>YX|=dwlOjG- zRcK={60}?v`>mR;Z2yRwz|@zC=imOMP;Niu+xzHzw#cgCL*s_I6rD$ENhdmV9$u{f ztUr9T{{j1ngQR2E+8+8Ehm-C#d#t}H&n&*LNG0i_i@=PWfRx`7`(2k75&i>GVxI(F zb1HAf)(619nDeIaFpU$dwR>w@8dpP#!@f8D8k5@^<%ASwekMj>9lV)qzUgV@2XBbFofVw;`%yS91|cx z?;<@H5AHGgPonrh)%~PQe_i>mL@Eax{`-9CGMP;ksb2ciTpSxDS=Vva>MIp`-IMw* z5JgMx7}G(oU!`KDS5HoSqoT|Gn7wxEa}~|p@f1Iemr1c3nx5<3R8>`B7x&WU9NTNE zJhLJyw4ULK;$!LxhOVrik#q++zDlgU>+3v7YjQnbNU`K3OLVwbp$}7mVwoxVzzs*o zpR9>5cc`0mFQt+)YXd&rm@t-LxpBl_piSSbIbSc9kOp802@sPKz-wnWw>aDa+CDMA zvRFaxtQYRD^SFc`Ghf$q43Xo7_`|{EI(zvf$Y$ah6r_KZhesIw3 z*QvUoG9m9JykAjGfBM3xVqwY!K@py?f*W6%NSwypOyqtpCl|8hL7}XhyWIAb_@{mM z2_GO|WQaYJSYBEr)SJ@It?F^`Zm+%TAs6-2&M7*}R(k`Zj9g3fkBb#ZIwno-VEi3p zsj3`nQIfSu__S`Bt4KFlZ~uM{T;=f*hWAh4LW#KEA3uM}tE;OQ+!<}To;C(r)e)AQ z4Bw~gu|BG*2mUKwPOc}kGM}ID8Vmk*ZmDJkckgvW;rSfp(HODO6W%M=+v9rP`;lX? zz570V=z-RK`TbID8*F9WoBz&j?%z{#?APo*t5nlrm;I_TR|k#X_>*Ne7IKbA++i{) zijYl@4_e4FV_>SDO`gXhGR6pO3kCxUIYZl9hz@Ws3=<8AH6 zHaQ@9YisAKYiiyCDnt~WnikUI-&UU9jB_MIewq(K3h1T~0UZF3;0m&0_Tjc*+pXg_ zo+uMbh%IR!Kk?rkuHs#h3k_ZZf1yb9Xnk8%5G!5Bz2jtDs#b(Mz;q^a_=}d(o1dNyTEmU!#+Zs zS@i5P$L-e4ega$XyPJu+f9esP_>J<$OJlb0(5u$`-Ry6D)R<2j$hP@9Btlx)NGU{+ zXLvX@)B^3mzLt@Lk8L7>Z~RndDf~W|nv+VfudqNpi+dMiZ~;sPtP=OZ(y8~*zQ+s331xkhS0eUb7%kNbo9G!xVdrR$U0%VGaIwrPM`R=Tu*wYFiS_Hz1vwW+RN6b zBu>h}G{*QewQ7!pad5Fs*kGn^0J|=8V(}(Dy&MiHj`gY4)?2i>JvTaWT1ymRnnD1W^Vp@FnNWPCN}=dnKs#mGi8B>-&~46LZ9=V>ZL7LjYp*!x6ql#RSVrr zqmjB;*Yo$}mHfZg*ZefL9oYAHug%D>0zcapug>LVmEqxG!jAI<+2cf&M+8yd@NgF{ zBEYu1=W&D-{_VG-^1tVbm1n$b7vrW)n;_FpK~sS%Q--@2nX`PXh(agrc!>eK-kVmT zVyN;b<-?mdgr^ix7~8jiwwYP}70vqHu3z`el5(z;DxYdof90gtr_;jv5Bu|~^A)byQ;IEk z8hhjqXxsF^$z0ah!Wn-4z&`R`o12`+%sFVd4>L&6n+a|r-{UrRFd|KF`aC}JnPNQU z94K|MFf2{QNI*E%TqsePlF(|QEj~eXMJdL;Y27c<+EY55-AFGB3-#I$1SFlGe7u|# zIjORo8#|?9B>W=&hvUQJO|ND|rjCxt6oD)ohBm9P!2B1Lu#A@)B0 z{s*KllbXIK(0%>la|OWm``_=Eyb+dh4}KH~`t~AyB57P~lHmZo%IO&Wd}5)${@+3m z&&s5z{5~s8S2hKUD3rZG@Cb5PFnC%Hu_7&SNe04r1q6!`Y51i*#)qUY^X=dY`EU$n zdy&dP?YiWcgw_gE=+9%Jt@vS>MvbJhAfJN{bp()=3zTQbHw-(ebbc~WvdGmUrH+d> z#YU~^Uf|4fQir8#MXx$fq)Ddm2AX~r&on)YQqIHe&s4a`ocY4D8}&FortYJ@E~UM} zm}1x1%N(tfDfT^g?eSiJnQ))=Uv8GZX1U+_rvDvxn*J}@QnfScn=Uz>4EOGfGKl8% zFc;2aZO+>#Qe9G5{Msn*LPN9MUZW6uZkA^}W`c6ozBh!JW-l}F?2@FG>X*pgo$5=& z9}$-Lt#_p88H3cl+FjjjMtCAZ1|xyaXOj~rFPRj57hpVGTxdWOesTA@NOR^bTXb!z zX5-F0oZfp|48+;Tg+Sc)sS>hG(OPcc2tcZms@E9Z`1p9Um->vF?0R7s{b7B>m3*Kt zuK=^Uj8Po1CzVM+?$vv49X{9js@uE3<`!`lswyiTw?|KZ+M)ZL=V9agG)DvgBh?^c z30DhaA|upw?1JjgkTmla8EDuUpYW1cIXD30n;YMID?`L_Y(~=XF*cIPHO;P!+85Dz|=W=oop1IJX-7Vz0JrNNch#QrDGgv@B>BUgoZo)8-ZN$rL!^K<65< zx#8rlb`sMU!>Eiafm!ie&AGn>8|}LK_q4s@%RRz{=f~+j8;wiGT1rqCz2O&~uoAuc zSy{EhdvtG2n2vpvkOfUXuUrf>SJR3dofAb}nELt-b0eEAUWJk0mLD6BvV2dBoP11I zwoA$A5}Um9D(_p1q@1aHB9r%ycNwpq*uM7qdb%XaNPfOB&I8Cjo-`2uB6d?1QBlyA zxgsg|-eGQ%Fy08T`dH$P;Eo&bj%}5M){z){uyb@cUu9_gt|8t@pu5w5C5vbLw`T1( z@I3;=WnE(`uAZJL5N6-UObyl!_pNR~L;)V`_cH&d=MkCFrKT4Iwg~AGs^hF~6_y_n zRsk#}Z1d^}qwnS=D2(r7&#CR)Iko!&Y@c~))pHj8^fFfl4c(QRO7ew$a(;~NSN63% zNT)4a?<=RvxxziZ+oFBfvaE;kSm{UdGjTl|*7(+hsHQ-l{D?^Z8_J=N#y*T#-Z#~bS^Bu-@f@lLMkj?Igg>*e-ST45CzetgL}9gl z)x4p{d;^6h+_+VijvTKS*P9?2Bgr7Fk-}~$)&5R+y{3qtY3r_1USm49nd_p_(kjLG zSY8@P#!inMUt8MwNp#}1YX6D&2-I&ATwS}Ln}8jN$pCU01$FfhgGaVJ6hdMB_O1gh z>Vp$X39$=MdiM3oRJ@}jPKAa4oen(m2dMJW9HF8kUJ+gocbnJKm-W8G)sNqvoSOOa zrAlb-VQ;UQ;jUe(KA$-eeF5bx;Jq)OKaX7f79w#vcyN#quYXmzs!T)Eu=?iEdZmeg zacA!*59BU6etUPClP5CDsExF1MDd1a-@ZIC-S*~}DP;Xbst4R7J5|^;8-@gW1LAc< z1PENP~_wr3EP8$g>jWixQ-X2;f*6J&q-|ubv*!Js%^|*1Q zx1!o!nzMIwB*XO`OYcywP~N7eN{VtmCg;aR3n$T;(V44|zT^*6r6qH+=TD74Q!LA9 zSTZ-_$}19EA2ydNQYNm!Yt8h4lO&pQaOZO)x3qYs;Qd~^3YrF$%k-pF7fkHLf#47m z2Ey%W%Ad?LpMEmO0P#QXd4n;P+UZk_4^QOdlDoUO0|0lpdB8Q=iS!T{{I7bSy*BLX zskdbY2XTwH)&C9yCe_1KLZxFqWm9%9f{N0R%@yUNcD6zD`@@&hS3JjI!6FV}oaY3d zF@*bJl=qH^u&Nl}t##tl8hTi6m%hF5_8`j%2Z}rw->dM(=gz6Qk?VJBg>ruuKQmL7 zM%pJ*>c;w#6lLmmRQPQ2$pf@=mf>y`jup!B(=Xz`JD%D`!{!mzHz5CfS6;3*cYI)h zC$F?8PjJhXr(wN3f9Q*EawYE8EUQxdOyWuB^rA0p_E&EtMfq`>ixi3^31|yPCv!vGUtTwrIG!20+7|k8DfI7BxzQG#eLc;-wy3UX zH%1Y=GlJPn2qwoZmyNF~P-ADytlz_yt@v-Cb8&%y^2zVvZ=XKkLS!iXGoIAGe{Y8i zsX8Do@^Ye6J*Z6mV61z`lxHVLF-h;_^}^|4UrH|B{w)f>aw8RVnuJ&ONC@AhYCGc< zNyXJ-DxF_&LN#2j_}!do-j$asRlHsm5qw*@FC3<{a7uHzQ8#kl%;wTr?_?owc);Dv zD2ld_@#Vs$+q-zV-PyzqGsz`Rtirky9~5px#5q1FvG<{Oa*Si9yo^oSO6JLLA5YWY zc;mQ+@$Q^~E$myY8Qj|TDe?$xE_ouBZ{Ffi`nVwLJFct#%vUPZTv)`tr(LWl#b*aO zMNZ^Rn#oeHl2T`A@He$jN|T-+FZ(Y^W^_!yf0_*;YsWoJVLFHgeruxfMkHJ&MC?R( z9g1BRs7C@xB+(pEC62mumezkGFcu^@`O*8={>b33N6~{T!2*dy+4FWs_Zh4i>J`lO zQD^yR>lls%lRxM!d-%T5=w;5_=EDOp^LW}+1noQDe4B=}t(?&ZGBRi`vr&$3w$|HQ zk;u0-zpcHQKJtXeE=P)kV7qVyyW9;reMv^getXAAITs;)el~s1STfIb-%K%9A3A*% zAXRM5&sjr@v~9@x}+1e_4)7Cgi2jg4tJO3*`3(#d+{-s zy~w^f{p~)BbG0?=D*}CY8r!Wrih37-9qD|PN@@#;rQ1jAa#6`lUW!@XwWltzD4pFa z?=?uAFcmhW|Bn6ij)b&}gB~>(dP3J0LSW-PhOTcF9y?d0Pr()w;a#O(P&Dp?)LQXN zRS}}Kl)VQ#IZ7AaKJ=q0kkhU*ckGGhS)I()cp~>=kHa~IcKvwGaLP05FEzdP&acZ% zor;;Mu84W=a9g!tpLo1`F`Po45IVrEo>6RBpQh(r%w@v*j+)NFfTt~n%(%`ur8c?m ztB|p&=ELNBvu(#&DVih)Q@6A!A&m`YWwU$64yGt7lfJHFN;t8#vGDn_n9yaB*3Tjr zNoE(G8VK-YlWYYH7^PB1s$$IP3?;Uabu$ZtN9O8X}dEM&Y+xTVa*vd{PFF{)beOZ*Vf6;7>< zWbL857gKy zT|2Sw@T1_L!9R88<|%Unlm9wmq7x2;13(qvDyBzdyaL7wRY(y zMSH7dvXsu_gOya&jXIAtJ@iDAt{G2rMJIF0zC2^vzDs(;HabJnn4}?ZhyG=G!^5R! z_jNO^*|bGg4{KI!UT96`xkJNMU>n(=+2DRE>|~hnii*cRDmfi(dGYSiJo}65qvtdJH{YM|1x3G{QDwD-tP}=eN9Tvbd2gs3 zjJG661W5{f6D?w`3+gdHH$%H|_u(1(=oAGp16eHqouMlkp`%F3kw@qdfPSY$5Gp_e z3FC|v5~u*^A%-FzGWPEbw0W|I5xl1j|?5646}D zq5^c%P3{l>PYb{-YOurdV0?G@#Q>eKYbFP`ZBTW;@wd}UF@A!&)y;=|eXGmf5{sE@ z#Shn$3*)7x|MHEE^ibTQ3Q}@)RhUKV1Guxiz51v{CdY3a7!h-f9}Bs};FUE$Ae5BE z>8rZyS;j_AwymZf?B&Oq8V|<5b}-;>VVICCET&(YTy(E5bn-M5F(Vz&XywW1cXN2r zA|G8aoVK4WzcG$cse2++{a#k|9n~m{MPIu+h2e5Bkdz@ficntTx)OdfUcxtXA7>Jb zlDpCISJ{-6?Xmx6kdYC;R4AG7_0$0~UZmm@8Y*Sw{ewRpa8~uR92T8@G{Z!^fkI1} zc_bOm!&7n%!NRGI70)2CCWc@tr0W3vebL(sg(8dF>C<(sqP25bv*CljwqS#i;^>GT z0o9DVZLY9z-znF{5LfzgvF+bf8B5x=fT^Gmy2AC(tRN$V3lw}k~Z@bi{F`^HPo=lGtWM<96lsrkx$Eq3 zTy&KTG!9XW0^Hjv{|ptAD&!26QAqFaX| zAL{GKr#oxy6>-;=cee?=f4=?sIS~PGooAv)4p$s^IdU*(-nx)Q{8~@$S2`c^}@l{_wfB7;BA$e{ZMP;KC>q>}V7(u@xZM_27 zGHdhO|6W8;NO$!bwiJeJ!tNFs8Tp{H@(i)1T)VCW*?Y=yT$ee>4`Q>=qUyU7{jt`sq8y3|S`z%Cq7_Te! zmGO#a7zAimTk3EujWw@xZqJfdm+X%E^iYwJB*txQs`@I~h;97A4bHbz$`ity81K+E z7AwqftLbvf&-FG^8Mnpnj53d!a2WF5*w`bKqLQRNRVWkUTBCPBW9gO%8;y2B+HG-` z?w~X|22s`+^1Mqz+b^lB8_eX$_r~<9Xu6A!P{*8WDhdBj-8cFaw6tUxaLq< zVH?e?)PeFUfB$a^48Hm(jXSgL+xb$fytO-5sQ!t?tKq` zsK3856!@-)8_7}gW$yQ_g7-QLQCw$jawiFgH9tSU_(OxjzAxwBE$tcAZWF%@5zc)-{s$w&XR*PgF1mFuxrc z|Mc?lagO{QtwyC?)W?1{^ycl5WE5`Dr}t4!HgeG+3+xiR!eS^PlKQZeM(DT>RY|F1 zs^gf=>$O0Db_r%?=8X9!a zNpBC>yW>`;Lj2vSZ+e!7Qz6y03X^Yduu!w?v^i0e3h{rWx<|7MMA%^F4Y+_U$7_Af_xB1~#|1Ro!&q=W}%`+V$$TGQenb zME-7j$x)l%g6-G*#fYp3L67U5#LphBJe&`T^yc*>>`_)uI$5b)Idi^z^%+H#Dp`k~ zO^=Rg=+v+B9^FIFbV@v<6bq9mG5)J*=5}2uwb&XHLZV{YBPCk30biVgOlMZ(*P(Mx z<+#Me)Dc1|S%ERUEipyX$*vl9qPS8te4N?5eO{x%B9sRzwa7RAY?&_v zjx01^^0vcpEGKFRkmXZHc)yRlnh8xQ%$3jITL?!4;pvbgK9 z2wGOE7@Agrc@WCZO1~@>|h111FoA%$m!ut?1W+=cAt|Gn*$1*u3 zhz~%2O4AZGf$k;ktCf7= zI_Q2i_zn|a|0V-@`PS@%7pYiO39l{S6-ev@0(aNd2|b;L;XvoxmH}g&y6b-hM;`fHa@9;TbH-?@MprrW%h}}=ty{c4RKRRA9l3KiyFm-;m^Go>@tAEYkZVJ_E z@pJCWqbo*0UA5Hk6VFm(`=ICG@`8}z8K z&y2MnI6n@6aO2Hy-zv(>9s2gyfdM#Bzx!Ps;Vw%It95b@ z9~Eakus>g!Wea+!OgtpNVFI?4yPx4I44L%Yn$?|nWgUXzJdNGL;s zC~CaNM#X|My^oWL0QLPh6}Nz?@`2d)86u1t?fu~mUM!Kk=q|O%+r)z_pHcWSS{8Is zD(LVkv~RUy^+irP>XSdKIax~Y%qhZN$i|SK6B(*+=gFX+{>U}r!+o{q!Ty+Y1_sT? zo}^+srjZqQsJI?{p{HvhF^wq;qgvig{)z`+O?LbH60S#WkTi53w(ESErrTxca4=)HB z(`&slYN-xFo9^yzqo0FlWl+e#e*)>cP*V394$lUN&4Z=pcSSCe3A`p}LU$wE4)S;M zetb$dYy{e=eK%Tow^}bG0jh*CQc@bg0wSsZsPL=|2oXRyd!pO6-jirCJCitsI;Q{M zwT!*|(fuz@qKrVc@PxF(VVr2b4hbem_##3R0MdiNy=Q_K@L|l_D=~Qdc zghfLjia`q0ch^9GZK50AsBDx&)%4LhMVeGfwx!KX z-DbsnDeO9jPK!>SCeNTqfGLe#j18k5#(d^Hb@FtNgJ#Yzk$^&5BeEooPd6DQmrZK* zS?lPiqw%g>s;U%y8b8+Bx_Rx_XXEEiaWVb8%u~9Z=-qLY~;wJLmlO2YD(VGk{(< zh7G3JLPI_rjJUXQJ@Ta`4?ivf3I4dIJEzns-J+61*Pzwo3OJt1t0D+vg|HPRe&4$ zHPMrEbMo7MrOgGG zNKk{)CPMd}URepkVt`k6Y-&n^&maeCszA%@M--eiHb8y?AwRMCUG=lwV|hg0n|JOX$#k#j#k+z(@e%gzJ60)dxSFC%MumUq zAQ^87!uUkshB*%g4`fa(e)TG(&-*$X0eoQ-LpXAuhxDUX z@5AQxq_Se%k89$4yFr&cH|$xCQYcK^iQOvjX*T0ka-QU<@64u3g+_S3H8j68A9Llo z{?=znbGsGJrmkxueV}s@GQk*Fu%yz_@>7?H$|&H;6o^g=jhxi}VPT9EidFGS+U1w3 z#>uixs8@C;EM{YtH16Z_;v)N4U%@EufUCrvI8M3ugq7vqpr99RxC&pqR)B9y8cH-{ zgTgsKE@Q+vn}b~AyusJ=Wlv8D3NW`t3*_h`Ln-%*ddEZbloZlWzJ#im=L~p+B1-EnycJr$@J15GCP# ztHW%omJQQK@pv_mrfLV-1h9{>u%;{r)^_wbc!#^N?BvTjOlmbQlQLK$54-+>d;JNCe#Nq18iz zW~d1|lV<&xW5M#;1n2e7o%y@t%dgG4FnK1c#~;~uv%1f3UEN&>31R;Bhp(J;s;DJU zexU6ByaB1vbW|zFM*d4i#tW=3ZRF9vq=&QUz+(nY=BT5esp?hW3^h6v1V5fYUu?xg9}wNKd#Z|9jS^5NsY}8 zzAi%xJx4Un4dSRWA5DCgw%W*;rwW_TUtFMKr|y2UesG`DHBbl=Q;_js^n5?km$--^ zBhYcC^Y?_B0Cxwv**Cv_&HE8ro>kJ+lvy9VdNC;_#gIQ61`YeBYapI>(0upKS68Sf zdJ+3P=oMEMA}$N;`AhF}*s>kV159yFUzBe%YH~&Ul6q&p_ng8MfHC|Mj zy%FDqfEoAW;C}hg*4i`Oxe2(}=zofl>agmsuH0?ks}3b1F6h7fS@rg3*H6;!J=o$; zuGJ^ve~9`xi6XhDH@qKrDPsKOrKnvK$vAQ*#sb{9a0Xim^eOrrRKYeNQ^guVXZS~j zK8}=%0iRbRiLv$@k2Aj7xi|`bXSIjohA2h5vWODBmX|6RtB8wiLR1W~UXy~F4l%oN zy1DOrG!#uTiPS*tQT_b~Osl#UaXeQQ(2R|1le`H^ z>m;RzDo@7Hk-l_Vd`~$H9w!qFGh~{#FiCh&Px#bWk~+m3GT|E})kXD8-6IguonWn2 ziHoOf*RnZ!z1+JP@LVoWSC>zII)c|!#fyo}8|&-SwSlctRvrxE0uYrHO^ zRQCW|EzN<}7rBzy>2{E=MO`bfIo*ru*G-|Nr(#mq%+eVe_cN~W4Gillp6uz}&>_Uq z7gH4A7Y2^n%I__3iz-cJFUnkYaup&u{jPV~gS<&r3^qnHe(3*7&olv=m1Tz*0CX^lmj{~o7cEF{9{k&3$xMl+ea z0Rp@gk8nxQ&9OB_+J{E$H6VBzgew}N&xT#A$M5r2=p`eva9@kL(0=}BJC_F)PG7&Q zawax$r`K@Ip-@9bQKL9SyllmInWIJVd;+6vPlvOv_>RAD5c594V>WQVTBWjKNa!UW zo`*|o64ysu%N=z#hE2^`+4$ayIj|D}^fYyse)DA=`5841qxjW= z+}j6uv={m1sE2x_d^q@fsUuipRoJ4Hleoo2qD0Cw<>*qrD@vJcE7^|WO^=N3Ui?*G z+RQe5$gNRF*n9x_aoO{K9?#o?srMKXfiJ^3h68uWTZSgO+?h6zU6GkV)V`xPL6K&- z)GCCHOMK?njJ&H3+&6mP}z|Q5!E*{9EtukB6%ChNWc^Kl~~n z$kC0y%6Sm?x z^XO5H1hB@7ZEZf+qU`V?LVZIevINp^ZJnnO3b>ml&7aV-;63zRs+Z zv64m_m4)tC^>@i4`&e6~P4A0lVUa?qB7(hFT^767Mn{vDopwBUA%W+} zvxJj`mHRF&m4C`oyTs$RkiR9a@vl@0^37g!N)=AyJjDEyElqX(PlTf@eZ9=<89|p{ zvId@y9cw=6i_?PZ?|nAJPoKJ<@hTIcjkL^_BF}L(7F5ZQA2TnK(8!k}b}o%iDPWh8 z(80e_{MNHO*-Sx z>@*ZuTr8WHh!)2LZpu2%eSYUvHn~&T2Lv_CHN0H~q!XK+nv!-&WxkK@@AlwuU+OXB zyp@`HM#|dkUD@yVug-&BGK@~*dDOXu;M|T6bwsq%M-N`G)e#~eR$KX#f6D#_Ga*Ur z#N0$rrTkCIf|Efv=WWyB@3jsaI(&`w?ZUP95@IkykyIM^Ud2iB6Y9Z%al5+)Oy536 ziQO4VDnC_o`~DlnUJ!aY%~;KNC}z)qnEFrLdsQ?cI79d|E)j5XX^54-RnjuBGG+=l zAk8IVosAkPOIwugx|1%Yh+eiG(w7xU(E34@>^4*xGgeH%I3vN9W9*Rr^#jlR=gydv z0I4s1F6vS&x1MSNB?NzNzyC(e1`EkgPfUm=soc8jx?|Hvq=r^HG;~BwQah=%(Xbjf|7mU3)AY_lz_qe>Jy;t2x^d_8#r?#z zO3LIi)z@*zMeju!XtW)P*z%cpK~~a=Eayt!^8EH|EC3b$tN{cWQI@5xH_;SmoP5Bz)Ia? z!V@i(l3ru}&ANj=Ciar}Zo?h-HgkndiA6QJQ$ZLP_NP1ld+lsn-Z(#&_H9hO8~pRN zh7+NeS&_vMMr4=4_nR~_{kq6~*)&$&D1E&1xjpx2!ZB2-Nn$@I3zu0gYdZWT zHT1d9E+(Hshvw|z(y>9?Evb-opnL`5PVWAklha74Z96^5^Jxvb74%y3Wrpciw&|Vg zeb4BwhaG;9!hOTlcYKW1psq~tUOmf?%WuMm^-M4>kGL2>LRdyRpp9Li@CXklNb2sz zy~#*s6~(39$hl>!`LcG(^=i=khnCyFq<2{4#9nf24`n>6^^@n@@W>Wa85tEkcjShUdcIqe|Ryw?J#dA0K=f@1zDQ*0XoDxnC+GmBo;x{6jZ?E@= z**M~{6F8e3ElW~&M#tD3M`z^C?&Nyt@OY<4lzxv9u1yXHDg#Xr$LpDbUEByUNZT&9 zAm$)cLzgWVDYI;nju9;%|< zQtA6e3v77w&3XcrukR02*?<2K&+Age=@A#uDwNf#QlxKa5UR8iQbcV@v&f~z9gD|z zUIklxgV>0dhAt%Qr_*C{!Ue6i6g8URS$#|9(ZY(VSk$EDyMo52$zy2FnDQS&EUi9V z+SfDg@8iEJv(O+L-#R)*{+h$tT?vSJ#J0uu%~^sQwSucUn=I#JzFz(~JSoh=x_qR; zSyvm!qbw>WZ{A-bi3(5WcAes-=U*$axX(+b#F5m=OJZ(|N>9P#AUO`J(!=NFu;$ms zge2v58qo!|R(*cKkoguXubh)k*A!EP?sh(tr2U~5$Cqx0EIf;q-vPxn61X9O8AQSj+eAv%Z2kVkQ@dJ~R=#{8HfE7`6iM%b|!1b2%to}Ycc)5A*LbBG6_>Z)G zOHkLXVdmMv%g+#vm-iypGk?FEzbq?CiE8^fz7}5>YY)NrPpXI#l61StsLYqVZGZTy zRlOQ{5~DWJS8{OjG&8mp7n6iEBh_yi?mo^EDUQPd`>d zg~tV;uG;Jq`idxfe_*G%crT(E~}@@!T=cK`R; z2Yk*V#GC&m?e~Qa#Q0KD15hQx6_;C3um&6o2u}j$!IcEc(d>R;=>b&k!jRsyYe3&U zS-*?0T=D#i3$8uZsOGi9zr^pK9nt&p>J$0$-OePc`9gg9hQI%=5DAPB; z-Xf*QTBf8$tYdE&M_{tAG#1`h;fJzWA?<*gvm^fBdZvWmyOm=es$Pkfwk2sycAn4Q5GweQJB8($RVbqJUjG8NZ2k=0i%(Nj+leL?P>~(1{J5Te z&+($dwJM`0tccShSa+4^J#BFoBzp zl@p@UTkDW@tPL&>cx>R!Kr)YfK%F!>KJGO*lAD-4ZXLpeyog`=|GkLb^xoBmNyA%j z*AEDkIsn6mAiy{+e6R>AU3yqvS{e!X4$@KxkAFla1w)Rs=lDZ-8|WdO%Lx{3f82-U z;>zdGf7P53a%_}7Dcm`YQ|9T%*cKOEqtcU#vPrivWlu_+uJ_7Q5r6sU$4wGkNVe#D zn_lySx&g;zpEK)Y!=a2s4)|sY@CCZxzRDD#Wk(o8! z3+zK&R0nKagLRi|wa>B*b1}xFyD}ja9Ank7fm6#b_Y08^ZfcmAHLm3{Ln)e>n(PFTe^L{>gH*a)LBFlnZ{%F z>Fv+I>f_m3C!*;J`0PWMIx;!0Ge;P)DUGOZ;n>MWeEK3gxBL9c&({&T1J=G_3`EuW&QG1(>!lK> z@s_1{xr&VqZ3up2({!e(}kLcsXIRemSW68Ok(<`1O&2kM-P^5AJMWa*#-yaA+Dg{ z-AI-dsNsRe1{GBJ^aEBD7ziNwQ0rPcRfL&<=^lhnEwl=0Ro66FkQHVlx2;Fch=~4v z{v=XZS@Y#sESKEK$cQsj6H0$IIcZF|ozF&0KY)_F{g%-XM3VtYeEbgh2SDIvW@gI% z*(mL?6_GzZ95u4CQrK~WF9#`Cy<4zTX+=d0uzuRlPG%BCcE2oO6oNvDT@+MMMx+_q zeW6Wn3DXMz4Ajt&h|xJx^_{lXOaLRohhJXsy7*+^ z?knFgg}X=F{t*YWu@{q;r2yYsY`w)b*8U!eG=Xdeh$;-$oc2hv@%7Du`k;ujmi|bk zG)6)qTBO2|dmM5-^#j_#9S5>P4LiG1dU9mhGyOMvc*m1;r)_R-YIyFZ=my30_fW`@_HiI06 zYx?5)TCZ*y*s*09B6S)!=h_ZyC`19H)Z2OAJ$Is%K*L#)WvDNgC~$+A@eVaM0^`r8 z{HR^=HnynwD)rf+pliAL>XAd#+H0L}m-^U!da4|jTux}P%TFar-Dn5&ZMF8Q#0@U1 zSP)k>XFs_7#q;uP?uP@?KRr~xWthAJ*6xldUQzghMMY(4uB9Olxc+cCfQ;HOPS zuJLifaVB8?H11d1x?J!Vqrwu%oQQZ9RQ*!<{Wn@<%H=jiTsAZGWK+F`9>U-EM^^>H zp<5E7s4#tLqw9!i70tpz4DRekpYkf6xclN{1}Gqtbbfd z;h%Svzk!t2k`1*X`~*M|#M^z3aFl5JAh78h*trTE7)Vw_K_^)t5wvK*mq-NxhHa;R zch(V_*2T^3CBMf~vdGsRxsg%Wi+tMs9e2FxqRWSa< z7SPRadtRHDIWYTEdt?*`?FN;xrZt9)`BpOSVhNRTpq5trm2xUWJe%w>R$9vH?8G=> zS0!G?=6Kol<=XL~&iE;6`w-ufFr#HE9ZfW4&W7jVNJ`{)amlf)8tGp|R9De_Tpodw z4}Ztr{Y@+nxdR8#8*b8=yykfR)dg_t0Eypj?~YCddp)w*gAi9@XTOEf#N3UWhbK2L z&;I7kq~}0Nw;=}A8!>t9J3e4}L~`SHj-W^;BPAtehE8kb`O0tt3YpxIL}JrvX=y<)Qbc1GYoj5PANm<6JOf*6>oCO7`U$N2z_#Tdn5}yU zlJp!xL5~KAnj&VC`J>F*1`K{+utcDIDGkLOIqF7+1J?RekFyOqQci(-iZ^~t9uu{ z;{DCQroqBv*^wCAvt*En#aPo}wV#rdgl#<)VH7k-*43bO5YwopGPKQ#VT~Gl<4QrJ z!mjGtt=lupkSV`QddSg_nmpW)aG0_#sDEJG`1H)USBEa^CxR~Lq_Qj*ToJv+$Cg?* z4aBhb5{NCgYOjx2kI5_DmKm~*n;Db@@LN z>$djSBwy#A9*>@`0FTaII0}a~G_T)OwI%>1$1-FgWcNp7!y&PaKfEqDl#DH|e;Fa@ z5jq(5DEzaM3xZY(>S}5^IXO~0zkt|B?8S(#+G6GvYvknSrc3+ZZI6Sg`p|4(uaZPQ zd3I9-f3;&-PCeO31!X|T@zITw^FU9-v<=oz=qdsetVu;ZILoC*wrDOPTW4|vJ)+L^ zi=Tb~zXV3hoPq+7hD0MThX5{^b)i?~fnLZLgt}gDGyE$|XGac;QRm7P%dsQS4?KG| zF*Vf-d+D7K+Zd_{&?m@))!NkY&-%;G`-X_;9VE&7!vFG?pkNM(q=DuL#M{iCUe$IR zD8Ens%gH8^YasrlAGekly2rTE%g!&0aTb3aeDeHwzxO_$D|I&4n~-cO-qGHkwO{<0 z@(6-@HJUZN2n$MPPgxd8#u8h+>YhAN!VYwnH!8yO+r5&z6dH`m?=G=dk0?`Vv6qF7 zS>r{H%QSL#gb}nT;EUQWdL8Gaiqb(he@|E+$dx`gG zAKjSf+Fw({RB&FgINTWuej^tPvp*=oM((sXH6DY(r>c#zDhb7tT9cp=8W7_U@euf3_{~ zS;%(r>F(*@`fChq<0s^TCerWYx)GsAE5|lZhoxCOJ`cO3RRoVwIvV zga*us1vBD;H7@1bte;R@wl30Xe{u87?uv8krP*tSOwBs`KUqd69<7@dH8YS&uzwib zd%43aPkT+w&=FPuA_qfM=wI-t6MLa{bCr;I0emV89k{R_N6apPT(th~UpJ&n#dUjD)ON%OtH#Yg6OAg0@EeA zSefyov&T_{<2tQr&b0Sr=(A>hTs5YOmqp4jSwSRk18&h;-nsNMCE;ee?-*!2w`kwhnJ3&#A&5LU{GBAS zfl*nX$a!KMVNmCy@s8D&LQxiu4$OM3{}hY=xB;n!Ht zV4VE7rCHk2de|ZnEw)#j)}C92;*T~}F3nhvu-&ZxB4R_1))gsKC3a})|KJzZ#rEMv z+*eP^YI4Ir8<-exyQrGn8|rHBCok&+*_}1a2}sRfT>>6jYY;SwBdMX^IUw3$5L(zF zNE}7wAP>SmsIrL9xD$JogeR;=AMizK>MOLF+T=U+Ylx!k=A3!}3VO z^LA3ai(pYl|5(vMaC1=$7jlGJ#8;!>EI3fbQ0mJ*IsI|ly<7@Gnv3A?c>6wzG(G(M?xuNLgiCjuv zX*?C~tgYvnozXrSGw9mHW6vP#a8_w!kiBoFEm!^Rwz1uI#BAYCj;Wf1xROY!)xbSb z&XUrrru0!bixr->%~6gI#s*pUnYidB#b11yZb@>`ZT3lNTQw}rvISWVqG*F1{<${= zNkkarK!9GcmjkIzt+Cj|)=O&f-D+87Kn!URbY(()S8mh}^gPFNJAW@24iPpr!o4|r zbf|?Uh7BUN&T_s-Hm>HB|ArLxkdVI-DaRm>p9(u7L*-iy%Y1?6(BL$_gLLO$Q(E-9T@D+_@ z-_-+(7TkpJQQ^>+k@@O@Bz|FP?Bff3S?g+!k(I^CoC9cIQ|jMdNk2QJDx;r~YUVxJ zDEW(LF0G+nAkl245l>Re=!c(UMxHHmdWE)pjG|0YD?FXXH;zQ-ox2jLv2Mb<2PXdK zwMqIWRw$X3Qd4PcJ!v&<&+C?%3Mfx;jW0EK|MYfm7hy_LI~PfUPx^sTXXCz_--E@9 z>EGC*48I&(2HMfdoL>Pmx}V$r_EAm?xRkXBrQG`>ZZzbUT6?48@{K5h8Xf8zGY*KH zz5VouWj8{rL$B-`*Z`z&e?@~k35)}C(0x2PHRU(GJpKOtK(gfA0OSOX4i8NG-GFeS zo2sTT2;BJZI`A2$)6UtspO-8{eoWQSM8oU`9rl*89*f^^flLl!q0HL+Md0*U$aN|} zWc5fO4tQisv#?s4ckbNbB782uo^@F)YtnB$0e3aGp(Ri6@j>r`*4*(31^bmVymb#A zdY|_$x*K;I=y-YZ5G#I=tQ15lyrxQz!j>A8r{FIf;YN$#+R*soF+6HMi!L1tkEA61 zt;(X86-?Zd>p*Hi6V?@#kfDt`q0OG@=hXHykE%%lrD^ZGfzq)?B^po(M`N4CX|GQx zyE>c^6aL2Pvw>+WjsI96WUQN4WzF^_RBIo*QL0 zFwREfOu)&M_Q9p>d%(_vOD9X`J=nkX>F37#>$BD9!HeaT%l?xO=TwC5 zccYd0v)M1CUOkk1^E@B$GWkE_siGhyhMBkaUi?9BSq$2F9+ZwsZYVOEqMOKyt zH$<(z>6Qy(d|Z&sK|KFw`uTfkct&EJa^PkQ7fQH+sLfx0tXVlppag~7au7nfl394n z2{BhyOkh@Run_)7*jU_0?z-w10;1r412!7ueT5WDPMY|d?~^|HgtBrDKP+BYddTSg zG8i*A5?~m#7e&TpxKTm%IRYMCgT_|mntKftC)tc{;u;aG_Ok_lWt-M#U8ro|J@eV;2Xo*Ms;dVm1JMP z#$976?R0L{N#}dj=FCLHNnYnnS-J3pt(LI8AFcbc1q6qbHI)W9_maTZei`Mo~>+{9p^yv+yrx6 z1go{JZAnJJz2Q}e?Bja)7NVWIK}rruAk|MR1y65i8|<+Xd{pxJ!l zy*iZGelpW^Y`$(0#b8HngZAEE8Pq!JSB|9|pPA{00~vBRISWSt3+;yZI>d4bBG~&b zXVy|^lP)pK%6HtA`zv77i>)p%!LRGmNaoW@AlyBmY~8>vMDRVYrt@heA6fOeL;X?p zA$yl%gBd3`=MZ0R|J(D|t34w0MaFPN4N?ubQllplbv4gnu%?bne3B6Y_lZ!N@AMk% zi+D>{SNDb|o<*U?30dUiy1(x!9n*FXY`WNQWGeXPcsiphYct<}xz*MySY%s1k#OzW z68Tfs0pgrpfjK6XrtXxisz(2%J}S!${j-Nt4YbGg1*_uR%j^D0^^}&OhYtUs8d$}) zpp8jwf?_nlYFZX!&LPDzNQeVm(?BtA`f7&gQIo}n2=D0y-k?D!m@^hEd}x9o#xs}y zgVF@=v%h#Qx#uK0V-Vu#7=u_fprEt~&@&|L5XKU)-O(Ejwv@q53^1O(m#>drB1VF? zul-l82UbH)j#|!tm6O+~^?b$je_DXJ&iDY-3?UoJTfUc1d4os9h9(M2H9|7B7>sv~ zjgI^KSVbKBgJdBFvL0=|Exa4Kx@R?ftB=B{M0`swL`IGqV9LBo>QIKPs_)8iIpTW4 zH8e1*m2V#T6=6qx@+j2YblB9_cD+bTCpR`-eA{vQikOu!BD!gF+I#*A@qTV`?q;!> z8pb=PF10@r=WJ$3PwZ;8>{m#*lwCy8NV=iCHn-yts*}k=N_bkOS zlpxL#@z^0o1{-vMAqY(YF-O|Q5OfM?A-HcqH*g2ouwI;=9Kp1EBm3;okD;v=N{b}7 zxK|_BK|W9Mk6}w(Eit?@PZ1I3EAtlcM&@nz2M=sLJ$D-! z`%E5+y52r7{pM8ml${XYlAUr$=`5>4Imv-$(j&G9#WN_pr1bG+Lv*`-XF|@)H(4jt zVO~l`DcdmquHOnZ9&- zv3ucBAJVX*y22u?qWE7YY}8*QOetk|>B+kOT~(PDkDTZeLCSqwc7?Zdod%Z#+&NZz zN7hT*ZGjU5lTb~P**Z*FaGLAB-{~DwsW_rns9^YHr>d%Nn>=cS)<0eJN77~3|?L-l?c5>R4i^tQbtuJtx4X8 zvuB=kAE#7%m9S}+mcC&@oFTB?o1B$y0galF79|ecQ?E$xQ+|k7k*3Oc?c*I~v)y#9 zNSY#o(0RVT_9G4&$vXDwCcNk6(U?UxTzYg@Fxoxr+9^Jty>SAoefelqE{{9|RANa9 zZqpI-e2|tIJbf_oq-6CY6~)4_V{Ig>HDZ(loAal@t;bGT7uy)7ga18{dgA{cNO{6! zGU?(mUa}5%_sr`PKLjlSgmuqngx4$%Hd6Svx{Kyn~~lpHrf) z|9wn}JltrA3qx-tZdA}foJ1l*?-9N)N`X#_Kex`J!i6G#Z{}i(;<313Tq9m%dTS{ZN()}ZL-viqW)JY zN&w;jW3pWDhs(Wm=&=-FhGYeqK7Sqr$rPDdhd$Xf+^u&Srd{(T z;MMz<*r?(8U|Z>_g>7)^SXB8pe7vZh86v*FhU4J^Zsh5MHf_|4eI@&Zn>JAbv*#@b z>h8}~lJGccux#`fYSgbzPL|frkEgdU_c%6Ni|*?wXl!youz3 z!1Zvi^A_q$>=;8uuICFDIiD{ZP4@iukGUVK{$b)nj7E=kF8Zp+OgP+@5o4g<-9JVJ zcTWy|LZR*+*om3o8#4KhV2Fnlt#fPyaDn~=v4l{)o20_N7Z?NQAnY$x|Nq5kA|G<6 z9u~`XBAfeXt`?Dz3GAG`%^VAKHUyd=JK3m{`~f2{ffu)4e_smk_@zwAETxFaEH~&K zTwYkF{ev5dra2@+b!CW!i&t>cyvgS)+-Ahw=_&cd;8~}y_#-jN3!@a4&KljPGI&IX zAL&UylJ_$LE0gA`lBLC1q7yT)vWrd0!lXMXVnmex=qq%pJX_JNDt%&kPDlRwmkCy< ze#;Mq9~KnEiSU4ORiVP3V(k7#z8F4o4wzKSd&Y%UUqmNUS4_AsN~oFnJ+Y9yvQhE= zvg}B;&b<+pB(m9q;aI8&u*K{F3>iNfye0-+uZe(BHkiq}ts(A7c#{qa@K+C-A>jD+ z-`UrzNG|E*4#6Se0Cd@wx|4tD=${AIQQ(yar|+P~A8}J1cVkge)&p5~BO~;cx=) zEuQ|Bm*1TW0w2cm%77G4Og~yyVh8{pZZX`<59~&pvJe`e!&ZdG7D?Dtku-kg?G z)pCZ;9`f97{3zO%s?8G0i~Uw^EcwGrD5Gov5f)N@hjYa&tYP+4G#9=&+_X<O4%`FWAQ7bh-T(_C z)d8?$OTmp7co~ybLd0MMa8L>4?auwk#{bI#^+~}yLGzcpmCNAaLxeTF%K_~_o9o9v zf7S&Z1>y|_;MM^mOaaclIwFZk8_kALKJnkW)cU#k6`qS17TTmD|Fe#{5AEpa2$Oec z2htM(-xwxIu&?fe`+t|`?miF9E&@{L0wC2N!5hIL^Lhc&Q%iwrMNF=)Q#C{Lkn@=i ziYTqo?-#|ET^zSbbqCwV;`V#wL!Ad4eaghHRZLuS6eZ-YXiCz#;^Xa`x41{nVdug< zdChU3EgGv>r_XwImyd|8Le}UOfq6ugJzv%dY4|wq717j3WYpO3wAEr<)hu$)@<_Uv zJR`lgmru^{U(>5r#G6T0-DVkG-Oc9bdWwN-T_typw73c;{of!ChR-dYaX z31We1m#pAy`_2Ind$GZ%NAn8nB0bAqA3uKF`1Z{aWZYoSOb2okVM#zB59pL@(Agy) z@91KB%{!@CvMb3r=zyExVSE+ zd|RjOjQdZc{Y4im%2qnUs_&aw*->H~4|kvRK}O5fPbOhhX;M_bcKp!50KM znM^hVS)*CMD=|3|M$!;PPZJ3$mXIt{yoi60&>xNi`BQm5m+!?GHmEueq+UsbxR`=H z5se{f>b|J-yOB8gx*Tu2$_Z9esQS=&)Y+LRHiKL57?p6aOi%AQpHEZp%2?VEzhb2j zf9`7LS2LnI0rx{@JR=+Zp-Fo-SxH_+H<6Dc;Fs|)n)GGoG&X35IMlRD%My%UdX4JT~ z(O&8eZ{*GeKbm$w30hl+Lt+>0ym!5-RL3jMOIN4@tjY-;;)2re|Y6&<`;`Ztx{+) zCTpv8DH+@1s*y$!IB>$P%W?M%i}D%Br}@I(eCzy`kn?@%**~+>)w^@&X$7%mSykW? zcg&D&IHEV5^CR}|G^M@DzVb`>;(cjXD$kP3fL>rXPzS_ZLohb=xwBW}IMl5QIs>h6rsbbkiZyGz71P1Yu%P8P0=^uXW%CLa>p440;D(j8=ovN&a0$L~1 zv&N0YD)!%axtw)9U30$wMjv+Kez|nB!^J~6Fg_sFLsf$+M&$-azk1A}`#jE0ofWOL zP#l??TtG{U*=Mp_?T3zt{OJH`khnSfEE$5G3XC51MUR;ue5tJ$1(6Tn5u=*FC(W<6 zCn8I-<>!A>gN2>Nmvdg_bV&On(wXF>aCFTizN^)5%YXi8?oPJkkK z4&Kzh${w|dYxJj*yWM7P-^idK@P?W}tMftD0IQ&k@Uj$f_+3tGheZSaG z-OfhmeI;E<QXaG5XsLFk=jdUilB?_LI6`zNDQR~$xm!ZzYFf%UwX%ZbjEO7MdUv-N4wmAL+WZ`Nehk`cz6n?+u3ufI zf*mRpfx*AdK2LQj47rD>p{`nmWE)jp$-i@l*6xj)BbD=Gh!0-?t`})CLEsyJ${-!) zqgdTpQyrMp@41ovZ#jkWiu7U-}B%!^h#p1&k6~EwA7Sk`b8Zd-iDqka8y+X!B%di{y;hH0x{fZb> z%x|9l9BJ7&)Qu!x>VW}bMSgyH!t`2O`v^3hNObV`8{$NQglc?IX<5o+ACB=OvuM=w zA6jFq9;&KKhxM%_zG-VKJdV#Sn|{PT-b9mIqpA^aox)ZcrjN4SBcjx$n7AzeHhBbFKTSM=tGDn#{_w6x?RXzF~(FA))uPN=CtL|Xw+O>nsFad#MWpJ0%3a?j+CDgv?6 zqZ{WSR0WR5W3b6{A-8>0Hx(NJglPQBvI3pTlTbtl<4s}rpDolOFgHaPBedSMY{)!%X#>QsnmPL#P zr#=^3uj0yU*7JRu4R`(Xnzsdc*S{RM7d-Y`jD99zvGHT|Su*s=e)65vh5R$n0U>=E z{$HvqkvS4c0tD6<@`1I+>i_wAktlqa4TW>uF>-Qmk@wv0>knv;6o3rZ3@@xA~BQJ#SM7B0G+&m3rH)sbC4Jk;MK|vA=Py^(w3?RB(Md;B) zIti9pPNfjdGM7uV>4k^OQV>zZpWV7KD|NMQ21f~{DZ19s^70`*kD6jmRdA7UDa+Er zyXWkD*+eL1=kIv#G~``qEN$9ld6kp*=++`76)dhy*;oCdLA$3o9H{6H5{WlSwD7N+ zdD0~|83EQ~yzm`bB3yL%#Yn?hjmRuYLw*7_L-oSKI{%9*+w9TlVFj7-7^|1~)B$X$ z@C=b85jtYxJ@tP5zklD&HBvqg;0qTun8r=N+MSfSP+qJVDqPrK>&4Jm)yAJ2^!No` zMFbXQ+kgfKGf1KuAPWvou8p0YN*KeT-B%deFM7_NJ^!Apwdv9qE^ z%;x6j^*lU;;fv3T&rR0n&&>KEm!UeM|D>c>4y~@R^_E9l?YO@r zXZc()UB-bDRc6ez^`$onH+Z8(Jk{>C6m#-MtsKP+oM$AkvKb-@FOe~p>|+xrs#eak zy=U!@!;`7Tb&e|eu`|6gI+-@+1$Qcj==|@T;Fw_R7sVACK3GwaOpaZy7R8J+6u;#^ zI%8-Y*wedl`UZw@*7g!)m#>AZ{6C()JD$t-ecwv<&dS~^Bb2>YW|WeUQAWxpd#|!G zvPB3j5;8JF_R31g9%UuU=67D6&-d3~J+B`1e&6?Xo#Qx<^EjxVI4#pA)?v>(j42F$ z7hk^qVF(w9T3rKg3j*#wNls6X+Ke~3zDQu9NCpL#k^ptyix)E=a9a&DDIVi&dS-%4 z1M;5#akT#E<~{nV;)C?195PEY3#!*)r00L%Mw7$;GUNh<^FC4a>6`>S7eVn7IHm91 zS8kYjEu4b_tqqtsAl%fHu~7|jSzr=2stcaOs~HcM#~A<$uLj5cbS5KJfi= z;>LM4*9Wxcss?CT7W{^T3;cSE0?m|7%lgT(F8dc{>Mf zNOX|)$l&1eW*l>Un2vpNLNAqsxKz{Q7oY_lCn}L(?a9`By`b@~RM)O{az~nszlegp za*>u9zVS7=Geq6ILBh{ku-VUe;%Yx%jrzn(l$q~8$w#4;l6&n+`JAvxQ0KCkN^T1_(A}cXV8V zEfGCfp?GA9i`_%~SKz)wzN2pq)wFt3xyDc~=)CsjxEbZ+@Sb@w5-*Gqy2qAwOdedX ze;4Bg&p3k`FMhCr6D{ualJkdKp&RO1G{G1l9M=gbqvlkKG2=T zt+gAbNHQ22<$P75H85C5)lW<8C2!7`WpQ7s=Q=!bx1i0RbX;76(Yigta?kGhN;SK2 zsb8Xb+Hr7q>x@enmIduvJUPXSyp#B8+WzKd?ZQG{5|WvBCPCxhHJ9_h$%!>d7Y>$R z9(h(C{HE~g3K;>N%%wFp7=&Yt*m zX_~YtCe=|`tDhk-(2Rf5wrm^o!K!=&#{&AI{OPk)zB;|6?cMolnoDWD^QoE7EqtZg z+Lwv)<_3v$dY$gOJsEgK_e7C4MuA7WSmU@~#D!b^ca2oh&s$CW&K>KC>;kW~*cjan zHE@;ip0u)^?w&xA`KB`t{tHwth)|9M$QN*=`6tmEzA4hN&ESyj)^#RWlG4_FwMKQI5cHKdy4dm6Jhi`)h8<{9U4+(0SKZ&waR)%R# z8y)KZy#(z`84F0r5nZ}Pae_}|5VyVBq<<&^-{CwP4`PnyqVuo@)yp1JNimr+^^x4!QuKHP= zMD}(i@|)^o*bBw6w*B8(VlcVG|IBue5;uV{oX-Vj|1JW0d;4DggFARZD-u;-S>t&k zwsCbh_-IWvwMfNVS#p-oQvLCA3F^}xv2pSYzUgyTa>6Hp{iu2Sc{+!*DaV9#&(P*v zD)D(wuC#@<$*k$__3zK}O$vtKFaj)SRXvgp6#(q%gBeJbb^TWH3g8peKzO=TKBB?f z*FkC_x46Cq2YYuk;OHilDEeN489f)Fxkk1}hnsDnl?{U)G*AQ0VxtCsS!f~i>guwt z^SSphD5Da@_`eRo>8EvWC>d01z?t2x!NKO`1gjqn4m4Ij^*3!iE2kjC)UfCn86y&E zq4DCXk<(sSvQsU&)5}~}{qwIpqaQzzd;Rkf#nZRSISM#Q0a^B96d^oR9n8S~b;7d9 zG89 z3#eh{afjpMa;5Ps8YM+)vzLesUl3k8F{6oUJ<}KQ=9!C^!WiX@5^q$E+>wk0)*H^bQ z@0E>hkzjPPA&XG}PLMSch`_lZ{b1Oc0u!Fia3Jh~#V?RcR0XJ|l}*6M9<08ItK@Wg zLz4vVzqN7pV+@(IiymNM%=EmVsnGs_H&Bes`p+cZ zGE?f)<|_G=nR3i;V-)X)P&2WK>z&2I6nRzc^OtS?`?tqF&x4y<)$k%)8Im7)ysvc? zd6ZVI=qhlYXOm+(gP>V9|J8rvCLW zv)vP$HNVAfQ96xCR@Uf?I7wH?qb!HtR2bd1@&EU_uF`K(?p?{pI753chj;HreeR63 z{rK+fcc1aqyBp3J*U6|}+)EJY4#i;I@=Xw3tMd=Y%v4tDy`qc7O0=@ZPcU|{TK7*b z#Kv#bc*;?^IWQzV+ydOe`w#>N<6sRft+@m(!58J_NzQX$^i52(6%{er)uQpvQd}of z)TiQ*9wkzj}=EXH|3hLzFt1sJ}KK-}&GbD7VBAKgutph2+)({`G_H2X=R zo@xa)@7LO$u?iF_$H@OpOF2KLUZv1_JhfIZ*XZ2+{f01SdTX!)u5euz-Xp@#gF&1a zHT`6^P2$H~Zg=L$G@tU^!?3`?lX&}$(RtFCJtT}M^saAllw&VHt0-^m40UE1OHG2( zuNkcm*&%n_l}dIHS9w648N^IUaHa_J)M6}QL2^c6td;NjA7_}DRhbAvcEa@~;^lOx zJYPAkbMGD}pwpaka?i`l#Xgjk@$&KI=Hvu+34V%(-PFXCl*_9f+~-vFdsUZyoLoV_ z`LKS?A|oSX4=k(4z|S>EAy7-OIuzEm(k3P*FdrE!w}F!J(*8=x64?2!iua6ImOf2S z*SvBCAE47kKoekRB4Ty*fr%i^rHr*;Lf8@r&4E{i+U{rkH(i$dS;chNMMV>l1xR;y zca)UvQ-r_R{M|&v58v@S2}cVbL(obL~mGyvKEX z!>rS3UijUTR7gGY>C2oIc}#606<M+H#;lvqgiD1fql(pKzB=LEQzM%R zu#>%ENL1PXAdUB00TNh7>VP0ur3Nf(*iGurxEyCUSdjHk>}1PuXW={s81F3sLtqJ9 zH`n9ET!gg;^Z#?%AYqp4o;o>xBtR|SV07>QxB!G59UajOk_jM;9SdnaDmOPbYS#58 z9K{<-Bm+tV`{4z@vj+za!e^#NB`1@- zo}-ug_rD{>Nim&GS1e*v949znr^{QOY(Pr%x5ixS^Suco zYwWu{$qxm*j)aE1WsC^8r)lMBEXvq9Xc^bapE0vvoeIBGHamnB{UqeJAiw2tethlr zVj;*=Vte{w<_nY6FzO68=WR`1u?S4Ukzu5Dxa@5~Uc=9My^#EIxFOE&;zG4dxviA5 zF252^3RfoKFn+K5WP?W-R4bQohah@<2#>4PPh=FYjY?cpPC<7xY|nR-`;W9hHklWpA*VMv>*Jca#(U^ggTX z_O3;!lS#3*Suv}Ua`Jhobt(HjjJ)c^k5A@kW7asnyAfP_*i9!JY+rcJK!y$96wl$) z$LK##a4&7VQ_^JAW>3XY*!Dl_=x>O|`1TMlBQa6=FMW-0n(@>2%Zt==E>gkwXyf_J z$?~q<>3dPJeJSH%s9^eUJFd)ECJ zBw?csNAzYSR+}o}%m$oVn`KQZxPXp)wmCeVszPF&m2h<&NL~+UG@V^tuim`Ld-V!G zyYDinds)z4zkVG&KMI0z;lP_-?^n>L3Nxor`ZUSp{%QEW;Wfi&7WXQQ)1)1UvWLak zOmwBrXZ&83R$`JX&c4<-JhxTF!GA8IIh?AA#J}k2i{aNI0bg+bt(Pp2Za97s#wp+( z$gRzPNmQU*E-@z@c7|2_DMi%!(jpyunl$U#sNxY{ew*y!`>ppYD43b`bahD*xpZcc zNIFwo1XMlHZ|*`2Kw}A1^;A^2H!k(NoBI9Wf0FB#hd#2&kP^FodVvV6F&kXF1~t}l zXncZ053{@4ln-=CXM)hoL7Vt+NmMEsn^`GHt;s}317Rc4~)IU^} zWsQVGYFZsHMv{|PAYC+ErI7y{*WaP+M@o{)+Jh>C<-aKcuVvY9tY}p0P2W8`HRBXY zNe{|QX{}K-*UZd%pUe8}9&YvV3JEQNh%&IqrnmO+aIna2*uFiuq!vRT*0mtTKl+mA ziHqD-#naoS7y_d;q^%4~MJ2Gt8I_a}8j4d~_sD>LT(0{fYc!?M$p#Hhw|jj zu%?|Iuc&qR)j^Mlj&^c7?Y@pe7X0E*4+Q46%AAa}-MU_t-BqcScQ>T$H@~YZ73G-P zQJNlCROHYsMjWcTp>L`{fEoUHXbmrekTrOZK=4YX>!{(&=xz>8Tjs}jtg!D9Z^ z+qa-I%D(F>7fJ>>5o`u}z?e+YWa4)UxgnLRBfCjo)%1I&|P)~d%s#v(19 z0h;3pDJhQzlBR#HuHFUaO6|@MA74=70_<4Gq=!XFC=TQ;;{uLG1Bl~Vvh3LtXfBq_ z6#iKjNf`WkB_>We(mdb}_s7wKB(8UBjEBcU_Fj7-LUzvQjaGGoMTvv^tS~5h=D4Qc z=iQ6J(c$hWcHMZEw|hbz&EE7=p(v-xOI@=A!RxD~ZAd+uDekQ4=oKBE1$aC21sM*O z$ilLqz^p&py})jE)3XT3@VlFH$gdVCvTtwjLm_0o*qs*5B)eZrkOME;S^E5u1FDEr ze{zX2%3minfm1|fR`p)G9g;i1UTEw5cV)v-%#$YsGE90UytOcjTZ{`hWCt5aA|b=8 zy(HIy8$+-DQ5>jH;rQZ?Rggq`u`%cg{qx=C2;b7W^gxHU+?B~-T->kYvD(_6r_uf=5qU+nH>Q~$P1n#63P?NDQmkF)v9D{Oz*nXy8vES)}RB*!O4k9 z^TWn$r?0HvAZDG@Oend}M^#^t!6t9YZzeneiW5t>`;%^_qordH;50+AedefDs)d8+H)Jo0}zz&~{nDd|+sFbmsupQ_V^)XKLt3HsIl;J6NnI4ftBkTn7QW z^TOIR1;v;Ytcrtfq3djlJFacRFSUg_6>X`nH%=ce{Pk`YE!rR7cvtzuvN@dC#C_Nh zv1yW?J&TWzch+}%EwsIIjY^e{==A+pN3KnNNz?;<>f}%MjCDRfTC@L0rWT^OF zv*b6%qHMGiIH*`dKJ3Ml<j6jy#<<%gI~X1YR&vnkc^jomco5KC*rv_gK z1{76Q&w!9|VQwx~!xZF=#`_f{V?LUAO-1^7siHO{H{Lu8567Kwc%-dGtt3efk&dyr z2xY1p;NY2|<3|D?j3}>cVB41L~--?QOSEGJSQahnLQuIdjH$&Hm0D#AnOy zR1u?hBV`i?_jqt9dGEK3t#xOcb&klk8CATbe}!LME0)dAy)gIQY}fu<-$xj2BE}1< zI-!syQn0XKMGgj+C*3IFFI7&io3bnwcK>z#ODtm-lKR`~xr}&1$eX&XOtioX2+mtj z`Io`!ga7_uwhPRQUiIOq>L(FqrrV)dgSjmC%hBUr^G3-WYbXmD-fvKg9UK`MvC0^} ze-G-n36JrIh^Ju#ZOi=9Qr^nzi63D+z4y2~?%f-V4aw&;-fha-;_cQ|q)hj&?|YBr zV?p{KGFxohT+`gu8lA7OJMB}8}b_b%El*jb#;Y4 z&;m$mRg(^GrrhG<&rn%^=Wvz=0a{R^&$?854+-5A6sQqw@6hJ%cgkE@N_N^y+B7Pk zUQv@>vKfhB_HTzX<>THfEhU90_i*d2TO!G4?&jI-A3qYhyzC^=-q&XXTDc%-j!`(+ z0eQ>$)1(Mv*V2|VjB(;KGTxYR!)I=uhuS!dA$L{!FR{*yd|}~BqE=pXF8E%=t@OKK z@p3|01D6l&D!wVH?*zY!$9+r6c#0k#mylKR6rlRf)h7CJD_eU-uV2qXVw7MYp`f6k zg~#UX?9Aubq^gKTF&G*+)nZA^)p2b>EaD58GrA@V+@cWQDw)3dFDYZNU)3Thpy1G#-;Kc6%4)_JyhO=%=!$HqDUPW1ZuG6n6V?K2XuE-#w{900MCn5Wvv7Upj*Ib>t!;U|N#|Q|Ckf0PVk% zR+B<1?ihlXe(lL+K-Jc26(<|swSc~1~ZlT-rkBKfq?&OHE9|{I=#n zKuB0>guCNhB8BtB#TtCOxq+-6 zfqRA<+!f_eHG>PmSIW(lxtI%bJP(bep^H8`Rnt2MUUM;*cRTd_Kf|Z}EJ^}chpcQC z{QiT}j8gq(jN{YnZ24};N%w?-)@%g3q;PaUZ|q#Ve!T^o|CR(~fiLpY zgoNM%yI)b|xtUA(Ni%f#G~)-iG?4?~%4i89W?jE=VU)rDKD?B}UN_as%*6f!rSGH}`T%#K~ zrz)9kXlS?z$dSwL@0s5lsMml^1f&>$)6{+V7QgH(cxyPXPR}iw#Gt){m2B_Tm_~O} zwal7=-Juo-2r_0nJ6~J>)c-p6`r6;G06X?Aeti7=5Kg~6_*j%(N;9`g1jq8lfBk4@ zU}&$58G6N9JiAWtFhSDLSc zYh89bE3e+YyN9C8L^UOJ!q8jZ+3UQCI(X`+{V6yL*n>w~oX|%Aq-b#K))+ev00Q^! z-D_!WZS@s^qrK35b!r_RiLNpWSx!w2cqP(n4|2c{ynQS1_Qs0S<+W&hKj##K4}J$8 z$m!^Z*AgQ@L`Yu|r8i(*`lL8hVri z?fv`dT<|PVEI_O#0|Q7nZ9m;!_i>`HmUG&ihp@bCS=!nJv}rL-EXwcB`8HE|OSOzaM zeT8si>%&05c=CBJ2aS6HZX=zwh?Q z@lUq6Lb_q958zF>L#J4Z-GEq3a4^=Ntu1^UEFkj8*%08d7QK3Ps$~yLoh@QI;G)P} zQoql~%gc+VhtP{;fmi{=%bcQWV|c3}aI^Pjql6^<4?RmB4i66li;B2eIQ+#un{YBL zC_iBii#%@7;OSdKK)`O7Cr5h0t9ThGJ!x5S;1XwUzb|Qtc=qfWa8gT0ZLY(`L4Gu) zrTWkEp+cqO5Fdn#eUI$WUmhSL9IPO-aiZoNKvWv00)BAeiV=kXLuV`CxbFn*J?$PH z%k4a>HbL$1PPaFG+NNvw`Z|fQb#I$=}gCeIPO8 zW_G76mxzW{WtZFf!%ATTW6c`Ps;^e#>Q>Xz=Sbh3gMrbbcmuXj7Ew{r*+1BST2E{~(<^()?LxQlt+>$bX;4dYH?p{154i`5zp&t${ z4jy@?h4942p?;p4GX&&>)KuEztkXgU1@5n3zlN8(!)slt+;aW#7V@DmrIIkj!&7b` z;y!^y52(Sbs;VM2KyyIWnts;%Ryxfu~F0w+@}hZ{)mMTPq`Xuqq0H5pK{uFp5F?G%%&G zaCikDeNU^MmB4lMvRK$j=JVu>RE2PC={ySTpu&0FHisG9MQtK@GtRDJDMNv!@#G-j zBrbxEuwcsO^H=eneO_B$hSXtRVf;8Ms@bx% zXcWE}&0Ev~i4vn&8B$;e0hFAE=JJHj<6E}GPGSJgInS!VB&HcaqGm0sP%=1ZsBGQ@ zMZK#A1};CiyczBfNX>`*EK-kJYx9mCT$^rlIW{WcH@J=ay?%Y^iQA7s{BWXkyqpUQ z3$Uu$fG8WXAKw{QJb|Vt1;m5MGJ2?8KZW7cxF5bGGN*SqT<5D5_pyEbU9~6Z0$$#k zsO{XDHl?YJBL`wGH@6ZWc}Bw<_dR?S3>IxET)!lwN@MyMlT+WOPhVHbb-rjM?7IWw z;7Nxzhsx~4lGB3t+5X2uxQ5#-q2^!nuN?l4ke>%v%ta`$aIi34TwSYw7GxNe+mffH zrTyyiWQK$_w;3ue=k>2zs|SR_+aNV-?3rrd6w~8Z2jeNqji2Erm4s5Q~!K2Y~}+}AwM!aZ5%U!uTdq| z>=hN`lBILlnLj}>I)H3M>sJGK0J&V|WybHQsHqK%jA*ar%(#80*>{9&0$3DW&V+;n zTrtxF6LLHp^bi^jCO2#y9MtfdplI%Z9qc{WJ|Qa=FyTuz1h5IQV4#8MPJjOPnS?CfR3l-E>;H zOdM8D+d_u&4IEFJKZb|xNS>)CnVuno#s4HO>m2My%^n`L2YBbQ zWg`aOpYO3%*T8-{v+h;0^IBq$(kof9l9BDOLY?KsxFG$-U3P3EY~pf;<5Wh+kAsU2!mU3ZKy}kJ4la0fA_g)iDcnXB((sVmB4~JlvAJjE@|b-9Hhc5NO~sfm~}rNDrq8E#99R z7e9IOgo%9;5FXG8zG`OX|6MTzSl)0@|NL=53sr!vRaaM=cr84mqM^zD=e-Lu%z!Mm z{$BQZWN->b{ThCQ*hRU5g!qV`c(Ah(SJeZD6XFU?F7}D(RVWF*W%ddH{7NHunLeh+ zIYiS-n%iR&abfKDG0&qS(QODg?2(mI3C(=34; z9KcA7gJRB-rd6}Lrpf){j0zqLg2HuT%;Oq=KUQvD4|wcQp7+Q!U1q_;=Jxyc*$&DG zYbx8IOHy$Ur;X+;(g_w5(+z@DkjX zWUPn62!;$6ym})81D+=#FaM7V(4-|)*8=F-R%F%#yxvF&_~IxvHDs+Cq0%Dzc)JiG zRM~wI@O1&ORA=@};xDekG95kZZo+!r1SnI)Fd|ICwM?lVSj!u?0}s@*FM` z<@J#S*j@^=pcSc*ogebJC6HXS!;!?FSCR=(!81Vff!V^2OAKwqf3R)YRF0yV851A{ zvgp0DncXxsWfW>Sya+I%BtAZVGm}K5n}0@+#|O>e)nB_&SMgidv16SYXx=$T2h)cC+ zc%1*(q7-WVa;NKG5>Mn4eslz{UDrhw51~R)(iBQVl>rBr!PPVSz0c0N|7hRR;^Ona zEzU4${Qi9xOp;rWaaj>2wnUnO1ItT-gRkGecOsoMDE6Q-XW9QSV=^lsL;1BagmU0tJ z;WHWoerx{A+}BNCkp7vEA~l2yMNpbAq;$5g5G&|-bccBlYN2cSY{U!|cbI}J1}$jEvof_V|C z+%+^|vEslqa2njNSXo(T1S&4VurLOh+}}CbA0=NIiCC`b-Tpbqd*Ont)lxAa!-&!I z*Jn@+>6JouX=!UC-K~Ot49GHaCx=@_2UO?IIYY^|EuE;SiO394fkjDau|ThbPsycX zqC4O>5;?vWOYix9e9grB=gXtsX{TxvXjO{F54_)nS+m@=u>quuN~DqHB^uDeO^Aau z#hXO3KNO42H*t#~q{EwjSkg9V5>&Y<9R3M6y4S3WT=(NEtxz?5 z)`88rcavX^_57QQs&^74ED7+WBhAziINYmd+B1fpCG_m;*9vYimV4julwQ{vL;Z*e6XQ3kHG@GO~|l zmS=KiHXTh9vBLH8@;XW`Niz8GXu}N78+nE7ty{P5T3bJlA7iz-AAaS|=z>c%4=-<% zb(t!W<#j_twDeG1y_oJFf(tV#1ZV&7f!Vp~r2VJ1Ipk7=h7sCXQs*RgUh{@t!4t%l zklYr+?ior6Q0VzrIdBxC{2qf7>V7W zWZj%zDmQ$Hb;mXIrFP**!9VoaiWqrzr0Q}fvS&m$*hMM@XeA=ArU7oQDp^^yfF|cO z7$u`!aHs4*tH4zrz8_fGqS%Z^yf8YE)Ve?fjP}_vo9qR6rGC21%$FS)PL4NDdJp%u znzPzP2R6If+s)w&kXPQbu_3*m90ZghSbG<@9|6h&+j=5kP*CI34!$J;bOrWmfN(-y zxl*I+?=J`c0D0x35-R6Q1UwXP93GDFXVmRgcW8?VJYo%9(j?VXH!C~u*~<~pJDq0pCbDY9d^ zS38uG1@zaU)KJDyj~cE}%tp=Ko+(Xh*${-!d7 zsKrSZ)s;@yPRAXDfdzKo*8UYS+C!3pFAEIvS|C}Vj43Zg9oUX7q%&S~fXU!pyT<{%BBMwTHZaRD`%W z2Lc(CiK-KQmZeU^hBqO?35e;e>=*;4g*-Q3zT}+s05CiMN@A`@B8mnHr2~N0&Fbr<0DXmx_86c?5jNgn!98 zeVTmmz5FqxKi5aen4zw;7%X+=jJNa_!sXd~b-1?}oHS3QzjXcNFAJi&G5g_5yzvJN z3q9-_>qi=o?Qj)v$tXCm#Lj+98n~-d#dLv{xr0|!F&LkqrsB;bcGWm$>@qvjnDMF4 zQ_c*$I^IJMucO;3dEYA-RfNEW6S4cs0g>ICle*kUz{F$h9+KZ&gl>&PT>S6bNBK%i zTL(vexI_ioX`f-}+Y7Kd?06pOE-)|eg^{7^0YD`f;b=o5gvPo%U7X)!*gOZ&wu%bz z`%5w~)<#?QFkMV72DQDgKY#udb@6=o@PUV)KT0a5!RKo`v@0sIxi;vuEj=g&_$y7!jsoa1(N%o_1oJ>|4dkIdtEP@IT;R=2!^euP(u0cCzoB8u zDvL1x=gWm}T(KuddPY4r%CBkhW_Gw@ex79$@xh|Dp~jN^W^vh8B4Cx+%QgbK#ZC1UE z&OrHuUh|y$L6!4ds;~uCdVwnO>lT*_HN`u(XMbzM2+ImODX6c+EQ`YdVFNn!xoc+F zfrp2_r<<#QeF1h+vPKdy_jYlGRv@Tl+#~weL&q7%?-8*BRx8tS-d{qifvY8Pq+Mgv?;c#TG<-^R5rdmVQ;yZJTUzImqcSv$%+HH!m^KakDJVYTzuhS z^fF@q&w|9PXh*v5c_mjhffblthO(bqXMKaHg1i zhA*U{t*tWpH!`oa@OV;~2qu20E&I8Hl+idiLZwGxbTXqmrVva5&kHF@98Q{avdGL)GQ20ipXRe~*O zL6CKfawT`rGQn#N`7E6XTgeY3Wp!Agy7oP0Z#`&ut?CKV8T%XKuPBY!O8xQ(X7K#l zzm8*`=+P9Bnv4eL{5eJd6YZ=n&IshPn+NU~$zf0U~(8^z}1l z*s)Y>dco-!45%qcVHaILK0eE);4>yJ9h}QG{$G+gT7J3py|kl;?cW8MIbT8ptO_tt zIK)SNKkNm&YqKwrW&S#W^ffRxMh9aghYi;er5$@X=g^q{9dlq+X&7}ZHTvA=48oZu zYw*)Zdl+aVf2Cb5fRq7+lIn#D;F9S=1)ytbX{p_(1><;h~5Ys+N&|>^A>_6-9VcQ?5pw>F~$3DAg|`vFlB`3gyhR%i;2_9kmZ0I7@~|fg=z!e8XY-_1+>Gsq-f4&+|kb%iz% zROdFqV)%n)D*=G>no#}8Wta5nuK;W8N5F|6oXXUT_XsXeIAH@7gH>L>p)WL=jDhvJ zjMqh=U&4qpB82>fN7mrumE1^RkfR0ZVZ;jMPzMUucUhU98lkhU-(U^@wgY8xTBD8fY0v+g5V zFzkb1{YJX(04^wIV+XUh2-WYh0UMJE`l6TB)spwW)@rmZude3j<_05<2W&xw;!{@b zEoOjRed&%D4J$Qr9Mz!q2q-2rP*8kB-SPWt^b5Ao#s!zK5LScER6b5O$Fih^ zwedHuEs&X&(cdxhjF=70&Hd-Sk! zl6vqfv=lYP*NlwX;kF6uY`t9+0f)@L-oTJ;bf}zjlk&;Dqv_=mDt{fHm4Wp4U{%1m z`Wlx9yUTnp`~4zB1t(fZ#X?79dD!E_nOS>^Ik6PO3&VPo=^y;$dJ%lPh8@3$iAG%8 z{9+CpX`ssPZQF03(uacSzReOUUm*DSSB8iU`f?N7|0aW}VQSufa?FTKha^*BNVp+h zVWN2j^pZB}4+IgE2h@HMcP3?}1Hijr@x$}qn2i^3n(~tWMLn}i2W9OKV1NQ^0AG+Y zF_c*lD1HqT8(Aw8<%f{*@&H1F+HvURvtJbz!50+u`g0eFy#O?_ug&CH(DK2J1^Cci zfH>z0^1@?>V%)vIDOY3dr=!g(SvlNM?V%iRB_m-Qu+n( z-tj-eBkO)Qt)4z<6QGel!#e#i@3w`8+Z%A6h^-vYJNxd<*WB=+5(}ZWkA506t3DW` z9gMv#UQkIT4d$Mr@Xg&~I_N}ZpB&mCWD@ep$kH*~F<`_d#K&V2y`2RbH-Lf}NV3xx zR>sH+BT18!5&#)Pb7?+4KBV{z1rj#FgG>^b8Xy!Hwe|2PpBwG~K&c+ffVA^DxVe`y z8LFw2`k{%Zx#LS-k@p-*4k$-0rEc(We@R>3hWx6n%^@NZ594sN@&5?#V2NGUmx=r0 zBjER@mt_j*O27;6B ztp=e_>p>Vl3tzg(9xpZ*Ue}T+lO)z%b0Nepzz`!;sw6jl9+;Hse@D3b#VU29@DZ)Q z-snbf!&kAhN|oCZ>m9>X6b~EwZaP_q2u}>H`ld|a-?Y?XqA|%AaZ40LPZLvO$DW)Q zPGzI^;9bBI5n~W3RZj(C;z*kUw1+L1GPPlYl$oc`$}e9whap!-dplHvfdDj$Sa;I` z?}nF$2gsD~`tF?pq!_qP*@$TdQ+)14PEzc1^qi!Ip1Z(JY$6TVZDzs@x$0BDJ4*+7b#i2ND}J*yOV8woG=zw-{l zJC@s)9B`KqV+;+mt){u#GNm>!%=K6WkMgXJ*bN8O)aS*4O^wu2P$BRrEB7F#G94F$ zOHifEi<3KDy~@h3hm=n(V5H#U@+VO3H*xbcxTj0YgI+w)aCL?Pg9d-zg()p$-Vi7f zGP}FAp+5NO1~8+rPU2kKsb z)omLL`C>o`H#9tKk#1e3giYj2C0$<5YOL<@K5rIY4`7lzze2-9JEdTQ^vgsqY6VQa z5qlG;bPdBKN}*&nt9xidLqn68nW?R=u3obCA8HD@roFpc=ACQSzD(AuTZDkW0{>T? zC=$vC{oA+4)$_2*8OD`v8%ox!j^s6l$Dlw-`sljPe zYB38GCBRhCBHg~W5zVZV1QPN^<|;Bd?rSfmTVS-?48#m}T%u4iv>UhKzw-(DzNbk^ zR175Qo-v*~y*F6M%wbT3APQJyglLc!$vzJy0#ub~9E5&?cEpQRmxXK^-k>D}Iely) zKqq&1cjcrHOaB}kfzlal^y%Hd&j+POVU|q}AX7m0R9RKQhHxPYBSDkmZ=zk}z}~Sx z6xWF|t||NUf_8zl#{^4J{PtBXKt$+_ z|LaVJQC&9$?OY&8idg`Rc(`gKkJf+?aVn#>*hl>>d`H+F+d*sO3M~Eh0nn2JsiFlk zS0BjlboBMXrUPbFD+QCq`g1X%Axt4&-kF1`6S;uHIp$kcP8Kb(uJGW&NaAoO;N&uN z7&f((d@t{UBFN#t%7&M)5d6zuQc`k$aq(YOY>)?o&R{+?8)(`O7AVd=;-kWk)=dzb z0wn#%R_aOT>FLj(2dpokW$A3NmH`AJE8XqF6)9->VTq4>_8*XD)Z&;J2Umlx1~=Ji z8JxN=wwZY7<0%4FIV>zpzweHzsg9xHaLyee15F6tyTF;0eLy2dLq6>? z?|o=TI^oUv7-sjoBSzdsm-+T$-OHxRzdpP%oqm*^v))K8W`T2dE^L|pnf%x$(=^9P zeTTVV$g~OuaZj42ubN1DE0auA5{}gP?@#9y(wlbFiV{M2@jb_zrWAQ{a&FCOv}c7s zRH0+DBdYrI^y77+o>(g5-}L)Fj$z-2F3>1R6p=4CnwFg%v?V=0>Yg}*nq#q|Q}ndC zs6iKGn%NLej#RYDtb3lkBAR@KsGch&4NT5{&;%?2opcH5C;*vDL{E{_y$aT%px|WZ z|6`^Vgjv zwB`1-*zKVZq8l8vH7aqkar~ICpwGU{c+>mJ=pP&m`RB=Vp5yoN&R$kpAQ9R8$k4V? z{KZ-i3$_?oDwYW-%G%hm8F8dCHp_1e4tS>i`!4HA&)*B$(cwOt}I zw2xd!g$M~9B+RP_uW1#B;O^{iM zZf)@j+G;u6E|ia<6{h9U8B7IaO;2VM2Zp>T^vQmNi-|~*V9@3$0=T-p-&q78>EK({U1FBSuv8wlT#Q!8On+3mx6jBhPFeD?BO(igjlM`MQPHj~nDW=I;A z>$wZw=SqrRVUWyPeSFzckDug*{8J3v`Y+1mHy=hQ>?njw+;ShdrZ-uy=Ugy9*Pb3q zMxw;HJFSh+zkDNfkwZ|3(MC-qcAnwB$02dEpG?yI(v_ltu#Z<7w<`y)*M;N!X$pd-f$vC9e%AN|P&h>Xzk2nm z(-)~2=>Ck2JB9o=31Mxk-Yg4dd89~s0(IHFiB3|mhIb&4XInX54a?BG-$(05Y#z5C zhyLM0GnHS)yRcgb*zM;s8C;i#=>=hwdTI$tNh(*abOc(bhr&DxFlSm3OO@_5NyBAt zH`6$8({Kxwo;I>~F;FN(Gs{1RaofLN6ZYu{oraDJyFOb94`%6fB9=Jm1%C;?7`?mU z;^Jai%rG!jz3-5LLzn5!@^n{9H)rqmC#m_;mx@F;<#G-h+_W!T|M#AoGN`}Y+`N%O z&*Tg3#V3mi5-xn{qKxmuQ!rB*-G|n1U9oXJL$7B02a~%{EKvB!ZDf7^+n1XiX+yE> z_H3Ui;`+DEFRGMJixUtB#V)q9Rm{`NtaH(OG{gu?u~=&54dw@J53#D28ZiHp;^Qk+tt^IO9 zcynFUhRr^tMCzUS2_gb)Pes9C%I<14ovy4608vBt@Y4&N7oOexGaW+uWnwJ$3ryRA z-RBB(mdJ5;Q|OvP0oGPzlursUA1cxZ@TI&>HVV)ffWiI+{vjng!-@kq06fD}z!hdL zkYgfXHNKT02lA5dlJt}XEHS_~^G^Gxr_Z13+S&cgk^ZM!m}p;I9gdnWI1b_1XCX|( zlDA;p_E$y5N7FTN+2spvf z%gZ;oZ`fS?7plGbxY}q=l-%2C%3PZzaNw45^N%>&Vv{&w>BrP9WEg7PYuxqupH0u&~ho!3gepe4Ygwys#+fG-QH{X7xjSM1`@dn!FI*fi3J$$w|Pf0pKK{RlfT_ z3uTi@LX)*1vu5AuXBqlwV`BZ2n z*S3G6bab|~(bLet!*fg8rWuU9A^09h^Foo-6qfB-z%Z{N(ru4>zQ}tE(9j*%L%pfFMch z`kRN2P4Ae}PaRrtkSA7i*M$-vVzuv9##9T;-=l!W6sp7u*x7Xb!FuyBV5Pmo#7ojU zXyqb=`4FR|;aCd%GtY!H1*%3-&wx;`G8~bjKFLcWFE#y`c4ci|u+WZru-f3^Q+7+1 zU6nYv_MWCYq$!QNZ_{b zDqQ=#axP?#G}v$1-Z60O7RhmyrixhUuA>E8?iB|CpJVv8P!hlT)F&q@N(u7~iH*<) z=H?i%!`B8QIe>s4LnZvy^b<9t@F-oTzsa%ZiEsfTW8T}_bKb*+x);d@!{$)TuXHsE zrN%W6w4C=D=A#j&h2v{qkO4RPakV^W?ZmZqpZug0 z;tv2{1gLWm0YdrPQwEwHB|z(d^{3`nfA`76<&2(WCqh*~YGG)iUbm*APc;D?t}1X4 zaQ-#K3-BM>s>{t~^PaTo@tbjya2nLPJoEZZad6ul5UL=(LdfKOYec4}6fR01T8p>u z{es}tQAxXHQ&H&Z>qtMCqBm>K-nMoROpb9;_KPn{oB)PZzG=iQ%U|Ye@_!$(q|hX zT0$V(cA-DGp0H%|0{ffuSa_b_F+y3a{7n|pvDtFBt@;Bam)R)CZca^3zRNHvsi~nq zckbNMM8L+*!6t+zY9!K!EQ?uWnhwCFGYsg+^kBMT_1W;O5*|Ev3)qN&4bb-Y9~D@f zdRxNsUcBZ*%5tzIFbxM>36e9cVWDRWS;^tsys&}JtP*!r{2WvXX!U{H9Zy{)2A<8x-=VbvJ>FEzx{LBKD_|3pgoWG zmYiG9-Q>uOROPaN%=NUxx6{6C%n3LdXlWT35(D$`><4uA*-+|yt8~2c`5)}!&cU2I zaO?ro|6}h>!?|w1ui>vDAv0wtnTtl15@nVQ8B&HqD9I2-BxDv6%1|USBtoJRQpO4m z=0s+t%tWRXo^`tZ_w)XKb-(#Le!uH-bm{9eoaedsUVH7en*WG!gzrq$eam@0ObC5z zQceM^x&g|$&G@R=kJlgrlM4Lq;o{AK`NqeF>W2<7C|35g(9>4wS*o!6RA4J+o*IwYdf8+t`ePxAe%pjS6c3# z_8QH56$*z7nG~<(S-r6;sOWK9M?V{UlU-dw-ekufUM zD-0SkQfGqoYW2b+E*_$#+!CVo;A^!3byf~j^%EZMd}*}FzjfSSe(ds=XDV@)rG z)_<~j{L;a<0`0#91r638d7|HMWX8Zgl{(bh!^Wt_A+D*u-8Z>#eM5f5A~SWUT~Nh0 z)rJR(ie+ho>MBOUDK&bZC_B5$OjK2)`Bxr?kb!|~lDC#b-{cjSk=DcXLKaedoSRCH z8SE`ClxsY8Lc1B%4VTToGP{r654&`@DiP-EzvPeH+sBMJG2R!vvgFu5K#G10l!3*5EPdAsy)&DxhwP&z+8`NGb)SCQTiRAz$qHtL5&|2Y|@ zK~EJ>fid3eXxCyA588lXvQ*!!I2f;`_H=wcug$ZbUE+$b`h7q3Q#aK6-d-%Fw(%R> zDHL$4i8XT-g_lvc_u&T90?B#)+aYFEl#mqKkVqAgK058VWa}7VpSqm5%U{o>#E(8& z?$;f(ZDV2Ql3%@AkIU7H-SMvT<)!+&MVM_svi2QZ1g9~>?6vrm=ySrCS(NF; zL;XQZPkHu?1E(0Y01tuBp{b<><%C^gPtIgDN->+6nL)bKKZfJ$*RRNx4qTY3YqU9k z{w?~ZA?Poy2Y`!hZ(MXyh=@6XSt6ceqEojsN?A|A$@1S~2s4G{!gWLWTf48y?mDPp zb;Z?}D(eBy-5*B6Za?kf%-$iMsx)^iR9^Bpp!#U1^%0Q;cFj+chl-{GUjI7Cf_Evi z%SKVvg?%?)IMEFly?o%{#`?ShOdyjEGQ28e&+S&uZVAP0SORHmRD4@=cEN+%j{>^I zH86UM3NX11%phzst7)e`gD@t&@PgjlKX`LnKn{Z}dS1=PBBvzOpRk>&3w|u6+tfIm* z5hV&6-J=5PM`~-X%54vbUQS^sp`&`VFU~njPio*;OGO}4)yO= ze^KKlhv{SVhZ-%EBp1y&3#K&%ApufalCp)ul0E z;rI=knimcT3u~sLw~hJPy-wK-$|-Dw<4>@v!-8xS$%7K7Qw1xp9Z4nc2EYh`6`eTk zg5UV9cPyepl4<CV z@G&Ac8DVs6`DCLizZoao>E40>l#Ztm%HiL~uJ9=ULwi|3y zuHQa&E2Lyta&rZ&olX6ycjaq|xXH|Z&D?*&oqO$5ths3DQ_9Zn)R|b=zEC&no9Z51 zJM*r0nQmEMUe$h@6b8(Rwm0vZB=q(dnz5UhwcHiWCIrf>OuhS08$TMIJhO^r*|!zPp<{ zj@^ynK$GeU^pA*sIQ#N1)@9$#Nmn@pyyu0e{mSzU=%Xd#1Rn;rj%c@aH#GQy@)zd$ z$@)1jDwu#;>t=_E@$o7UfvzK$?e9z)boLSv{hY7u6|DPQEO4D~?iicSgvTnm837t0 z1*SV2jJuv&*4|(BE{4H|iYb3X`CZ*5?(TG5LFL8B!fI32Z-&h3;*uTDEhN7_*h-Ve zRN2vtxMX>pbIHb4ok}~7B3k!KSygqYBAJT4@sRj#A?dqEh`Ai@hu!T6HkrWk{9!Ur zp@I`N|J0(*ssN?)J*E6T0L>?Xhz#971K^_w0~$Nub6gwxi7g8Ku5U@O@BD>Hmp=4G zX^3%OA10WtJFdSQrnDXfC>SY$?`7mRvUYpTnPIg%&765^Ol(_0G14lO<66r}leS(@hLzYmd@SWeI@(mzsL z|16`HHjq(WEMdRp)Z3zvkR1{$p{*a(CDQp4l$Mr$qOExyk&KhjvkCs3Tv*5*pKjI= zJ*aN`8s!cRaqlD?U?DEXcl@!jvBBs!kWNMX@#xPkyzV<6#xzJVdzT4#mnrZ)*7y>= zjH&N`lQ(@-jf`6Jc7g9eP)*;Si6ob7Cf{FHP8csJ?l~rSl^IOM4Q{6}u#3;ShB@QS zOs8Mu=l7P(b*uRJEEknM?{j}e31s*=YD;hV*qvGJaK$e!>PXWtM(SUM0?N(f%ZDa1 z%T$nv2gnWDcU}uh%q{Q87fG1D*nddSn!P;aH)8Kv$PUI0W^an=PCK+k9^u< z^c9Ww2!-+X-po=}<-FO#iO1r6`4vA};0k0U5M(<(?=qkN&f(<+BiM;DLKE*D+LHtf z0}N4aBUP1HKQRXpx3yr*yhBEPO<{3J_fc+cD>DxUi-{HnS!$?->rS}$=_7@AbCZSw zi9b*qXh+1v#8|+)3Brm&Sy|sp4T$Ea|A~T=FNRkH>0GfKbv+03YS0Y60=R7 za8khOiU62gF?RXp$VQnBERW60Hj>#RT=Tc~hcQIA3*GA=)I^(y4s6wvoMV*onc@p; zTE9bOe_WcRSn638t51&pHKRz;3RL!&j1-DbyDC3YOOX4wk?!QIa-?4h`yoRK3vmlJ zg_Eo3?Du_oqOZ7@^^0}`+jM?q>sl5*rd9l+qLd#8Teqc@-!Y+jXR)4dH9NaD4;?dk zgv#_J>F$}Yl5Y6?Igp4fjmP0YM60?N-60aWCGq&kkUnWegsF+3IQH=HU}L;SaE#Yz zuVBu7KmNppvz4lk;|8I{5eyjmXa`ziWri4pK;N5$t1g=Y1fL%;Fluk&G|W8}j-LGA zM~@$Ga8qCjR*`O_q3pZIZ2jE5iOvzV&6Bbv4>U^{fV4j;$;eoZkW>W~nCecaV6=wm&u;$VAfQ9=DEOSZ~0-EvUlCH3=?4Nv^H%-<`BaGn3YXa5K{C2b$Y z=kRtNudAC5R#~vBN?EYMfnmC!Cj0c;Y<(V!Pul`yo---Vdv3O(+>eN+pV_NyaYb_S zpW9Jo#+l7?5Lvom1YdtH3NoiYh$jTyr z2HfV&_|ef_MBvH^MfjV(i(#1pLbPcy&{`-$SviU5z})eH9aEpbAf!RGz%Vx4$TMN| z$up2*MYo&LI_06e^l{p6n|d4P!j2rg!ZNA5{6o*jWAiPQTt;^PX}7mA$HE?Oxz!l( zK#5jn>UG~*s!$2e6V0*ZaYrSugmaus7N8m1obtyb$ibmpuj`tnk=)eYfLdmIYl%R~ zYnOOcAMh{O^P{$U!BZ?IT&#-Z8okeNXfISk9mp2tLBj8OtU)-aflvl&9@LzY%gUBt zQ_-NzAk#j?Hd7LOPD=EvZI;D0@j-}L$ilWi$svn{g+Fvnf7l^#C`*4`+u4gDy1?*6nE^bB2SPI<~Z+Ak=)N4bgrc5S#Ms3 z7u745pE$@JKg`$TP8VLx)+eKIxvN2?vQUHGrjlA~iMlA1Tbbu^6% z5>y&Z`d+ou!1&o#i`pGZS409fOtK(blYT-#l|bAzQdPo7SHnOH(b_`VDmvDQ+VTnl zyc3Ejh*X`|*VSp>t{=ZUQl0w4B$E50A9qajPMo+7bd?C2VhC;x_YTq&iK}1#>ZGdX zbv!lnt5UEAkt%_5B`z)=^7CxS2G11XYFNsh#`Jz$iD%*XqOALrD8X95S>Kgw6M}_9 z4!@OzHYG)9yiHF$5QnP(XkI8N>hhc-!hOZg(LEm5r#1N&a9HKix8I-lhuQM}DLSQf zW%}3mRG%XknE8O@i@;tVRgS~1t?ZEHwTB`t#JCZ(m?hCgvj67COi#!V|DeZu) zEQX}?n|IP}p-BGbQoUR-Ng6~@a|}K{J_=C& z_tz7TiLpP44wo-+-XYlEky4N{nlpdIPunuxPI9p)#pZrhg|DM<+yE zHXtQCycA*KvsLxTv+DUAp3{RT&s443z2DNUK9DV7EBE{at8gkqHA#`gHa#;C+`4rQ zn#7xl*wDW}^*&V~X$4|Ss79|>)1(?MF*h)XtF^}X5aAHdS8|v7B5sqmlKMp!;;2KM zn*NQumEEAT#Q~289t7UMN9P|VHTgaxgSB)j&P&GxWX?(Qyd@P5VU*0}y`}-|jEc9C zWQJ(d?eby8--QBrAew+ExpzP58u)FALk)tcilKl;n-eicj*`I$`JK9Vc|>gt?? z;lwPwpG*)c7dZAFq-HYxH5 zt`$k6-#N{_JKb#aZNKtfOwDUV9>UG^(v*V`c0d(sDlnM(^XsSJQ>nCb;vih%A6LFk$xpII_YF^ zl7~v*Y_rvVCQIS;LL$pIg$qPP-+7Mpt}*^8d#z`3T_1Cw=~=Pr%ziK zc1(6ncyp6S3}4q2TRzmIL7?=qV?Q0SD;G1USPyD9XtpCdd93`hf#8eXv|k^{MvYzp z24{+uprDT?X(IwN=(oG(I4vc4^1!JEdO%NF-ko;%0ch=q;3G)Z`<-Q!Lbu;JyH7kJ zVEn!ZJ#W~U_P`Yeb!KPdwbZM!YMBS7fA|(|GyR~)QlhX>zkAb&ACq)V9KBz;UV~u! zy<2Shlu&B=RiUQ??VQUKBG)|6rY(3pBq_6oswJZ4W~9E|ttB0o`}36A;3Yct(eU?& z*vk5qgg4Q1G;iQqS9IzW-$`sIHN416ceBG^75UKwxGhzntv5V;M40}H=w+Pw>c zNW>9YVd_0rwg1pd;jH89w6_l4GgL~S=ma@f%rvE^rvv1 zxpMG+(HW#F7r(t_K$1V{W2mgS*Sb960Bg#30VUc4g8dD%G%0(IeetKEXPySOuDB|Y z@xgOiu^ZzXt~&hcdirrOxP)z2oQIy8b-(b>Huj0|^IO>%15XE@&Qs*cVGCuH2rjrr z&+GqlM6vohYs0g-y@3ny$?9VXuNbGl2-rxLUy!)=+-8UH!}ZrgY@997$_NSCb*~n2 zyr!;>?xO7-jU&L{nCMjUtt@s&38Wtr=TdfabE}*!C@63;kA_+YCaJ^J&X_h(C;(WT zU`tArOuw`6QTitSp@$bjII~c9x^CYp#SD8&XzT^F*tN(6l)imWZ_sa)<6r2sxp<`e zTIj_VCJBz?Tj*!cBroo_N?x!0o+gkw<)lTvNl>g*SUTJO>N6g^oF{l{%^hT1^4K`^ z?R=+2+FHYAA58R}A2Vkkm09K2EAyLe(C9S5FfKjubP&li>$>PDBg`^&lN zu?2Dz8Y+j)?=^nHF8o?UiC*2(_sqhNydQORfliVlX-i@xHaR_{VrL%;P5cy*(B?d& z1Sx25wN2&7`k{-0f1*_06fI%v1(18+J*U`f;JuJ5&WHvQ>FuRCh%g+34EU;Z@3t^~b_nDcW!7B?c38_6 zaBM`7T5bjvfSWkapkIy!71Ng^^zV4&c5F~#Yuv&_p=2^8O2(H9Iy=i8EN64yzOT%W zNw?+Uy1;cjisB8yiuJtHFE;uOoZr51@}|;n*P!T)5vMf;))(Aj^Ua|j&&sxxJZovC z7_gi!BpiQ*6r7D>m@$I(8Imw{2^2*Z&%Z`3eD)Dmt=j%jRaci4H1!ZjI;H^hMbc6P zHo7FVfztYuv7QkT-y)E>7)g_<4V3C}NM?%`-hce40{yXq3q4Odyr=p#AcesOJ+AMi z={0|$q&b;5adkDM8limngSsucF< z{tAr)6rsgmF9fQF68ZD2FZjKWWPNADY_u=rR4|{}5y8|yj6p4qJn`464KIiEYi?jV zNze4eG?GtnZ=nAv+LA7Mj{|Ij_JELTlLct$q#~kkafg4h~Z8|@Qh7E8G6a>p|=hUSWCGi_)p*0$yc(>`=&@a z>BG73`ds5quD1%o0%!00tG?kY=(P}cPmSTLJhc8zby#6G+i{*tKa($3$cn3Nj$O;f zuC2fF)(`1wUdI3rQ9P?g8}vc$1Qsf2$MZms{6I$3?WM2AkB|xxk_{^0l1$GaTgOW1 zL;tiAC$i=9uT?G8*ZOsNCi-Qam%#U+0}Ha)gbR0xP{(`f8x0N8;!sQR!+vjqbo@ z0>k`smM>pUBQiHmMIF11;oH~ld%!aL)()pUG~20|n3%${S%qlP{dsp_PkY>o*{E$_RiXwZ@=)3*VZLpMNk3ml^h5Rzy`!YGOPV$ck#UoZ%f z!!3~NT4R*{X~>d6PO}(PcL9vwIO=Qe)Y(Qwzjl)m@0KSE2g778C2RZhr@hJEAj%>b zmM(f)-Kkv8o|*CIlK{O$i#SA>DZ&B>*s;3P~Oj?-@tdk+w>ll8g=y>U#Ay zZy0MQ$a~2aKkHL=-f@I)cfpjeFWV;LOFvUxesEjV+@;ax5j@);f4UpGeoGSfTv+eR z?z2gDE4TO=cqu|`cXzT~aE+ON)7Fi?cC`U|#@qcuY_`PBOYUQ5KN>0#>Q7`m2$4RF zDDFaaiH<_}2jI!+Xd3vfVSnGCz#hfR1u5S74@RZ_m{Zt*2^l5y(9m)uYCXWBL~{fL z|M6}&E9SzGOKX=h)TI5;p-3q%_V|#))ySVKDJbG2pKd-WI9%|WlJi|Kkm;?c+QE7a zk>`I!dqOXHc?!SbkfjNul~z{c?%Z?XZT{Ky6cxj%)#|FM5^w%H?|{W@^q#jQDl>q( zwxz|OwMEJ+s87b+yQm{o*bxqu)>Rc2Y>0yh>Y=HkIsYsJp<@k}`Xq>y8)2*HRK0Lj z9%?+aC~3Q5YHcN7>-bT)I1|LQ^a?(N&X(k+mN@P?(zO0k*pf`primffCrhGS;E?x{b-`grf7fgXm=H#~)41eDjOm`1`AIeKqgj^Sb%i z?sYa$oK`BK1oA%d|C1-v#b(Ie&RW9!V3b$B<%@~It)DN8_E#q<>#^_VP8pqAeap;y zZD8ZxxanI|;H6{+D$xiF^F43VNb9z85{~^|9lG;yY=XzEhwuQ@LP>Yf*4D53I}uuD zWMs4`><|i6KUo$zCoLs4sFOt{C?Z0$!WjBhEqt^=q2aTp_qi*qlnT=$j&sWxn>C{Ey=SEs?=LRW7K^eo+z(R9B;U`)^rwQG5MT7yV69 ztGqXHa;x{mdeP0Mius~-z2j=@Y6gDaTp@Mc@?n8VZc%P0bBzIV%ezCMl#*X%rO%g`d4}?0S(YN z>>SKs(P=k-i@@-x8X7h0c9_P^PIMOeZ@T`RiDeaQAay_q{i-z+LD}c2bR7OLFWj3` zr1eM+VGBH>+q=ozO7ZFvH5IcOy{C|KcCJdJzpI}1HQ%^)4K*}@Tvtw^I0m7MNFfgX z9eq9z;cpyqt8fnn*@k@>@xuFW38Z5Y<{X~Z$*R;{ByspV#=k%#prH`L8>R~+d6?!A zYFTc)i^^Y0iz$vCx&hA%W#lJEHgO^jd}{}W=`=lf6kMse+Mf(OmQ$i(BJ^t>-@T+a zxxB={zj--gR%GH$k2jC-Aj4)sE?>Gd_Y8lg0ev=V{VZRx)x6Dj-k8lS_ijm8ZF!N! z@vtD)N!ZFd*`wY3>v_^5lLE9&gS3C5hNFM}HEi?!E7UB(XQ&Uyd1E_)I$i}z*#oM8 zknZkNWb}_1J)}l=SkdlAHxB?ZghpLEYOOEfJq%gzyZ7|!A7`j_cO@%H@{kwnUt!T6 zK=*y=ABRGbz*kN**=%+yd)LbL$CzIa(p?Js>6Kft@kOQ0R;72L%;NW-bMVi+YV;VCvng?9Q`#x0a;u?og7>MF zQpD*alP2b(QWAoJ)Z%o3$GJzZtvR}TWXt|F1LZc`DS^BG%}L4_X@k!~A$%4oYnW?5 ze7x8}PEHOxLHBnEk!d^l`_rD4wb$541!$72Jq-T8dF}o?Im=G-?*#{kcPRP48S<*~ zFwyZa4K3E^rix?PX(WBpl4xvh4gfs?j@z6C? z3n(S3Zx1?2{SO1}J0|=2p|g=9&X|>*Jqdc?Hw>Eybh&M-IP7eh6vLeU>8rSlp& zURl1`2W2yo{`}RAJq9;#m8}kK5KIe7d3*hV$m$-66lD;cB3Un8<~<^_$KsNaP@xOE zMUCp{&6s0Cu?)y}_Y)+<=J@H&w4fwG|Dw1W0xUq3(_W-migQ&G2n~v#Lb6ZK0|-fhAZ|cLGl_UEpqE49T-0nVA()fl2Htt(y=G`6ruLAYPDUsF zqus1~KdjUn$P82kiD(9i^ZH9Au>rhHw6tk?z@@MM`+C;*GBb78x!$4u*Xs8C74+IZ zXuF?@_OFnNw<6_(#mu$01BQYr6yC=f=0S0lWQppQYtz5ZgLRCUYe_ zd*j=c+@tCfW|-lzQVF3*G&mmQ9zbj7#B(~l!yFCzl2bcT?r=%_&3crqtPhy5#eTf~ zYzYk?=vAY1ckM7Rcn5a&El@~4d*6$Xzc|{qt4nM_nI2??bWy!Ypkf=qqxh6JCP6eL z@U)4I3})pOB)k}e1Ov+<=lkpPFQL{i~OfZPJz zd`J%8_ZXHSb7oo#=Qdoh$~;CzPm^3^@yS_ut391*bF7NyPUTYh=|bu90o&kpqlY~g zy%iUP(tNo%Zc+Em(Do^qY;eE$(?FS1t^Y_iH50F17|SEN)yX_UYIKZ6d0-KxR*{q; z{4N`{`9f2yq@<)T5-VmBUTbQ~;ZpOfL4_3yJ;OC~3MxA|tsi!Po>#uy)~ z;uN(SdP9Jjh;Szu$;sl3j}7hh>%3BMmLrVj-cvvmkF=FA4)PX$3lAzHj;mie#m=zd zZgTvQTQ&Z6cO(ydq_Xk-te5|5HtxIHElP3_4VxYv4WnHVV`WWlY#p1`w@o+@Q&$c| zr}rN|RC4Mb^z->w?V}2X7ED0FsY8nDwCvh->p)wyYJ27c8Y)t@8#YZ?KYWz{@l~R&%tRO9*o)Q%GqE>^cK z;>ZuqpTEP;qQwMAfXH-_1EI1~xf7zqMF?oeiet<);R6<%H*F4V7-BBBr@iJtEJbu? z2q5`6nXTKpYQJIR=D#?9h!P>N1Nm)9xXg*}0S8xTg#kK zs_an#mvjv|b_Y|11AViyfP&(Gw%h+0!!ZM&fWEe!@jWO5Lx$&e(+6fw2x0u^R|XC< zVu=vmKE_}0ygB9aV@SgY9f-;$QL%O3M=K^+Pdu$-BSUIhXoZC@80$EQU{t{R z{Obm@RkyZ?)B_w6=Ypkx#Dobwm4p~rtV9Q{+-mpG##TTY@bF;(GO7L5I3y&b$#3h* zt#bW6F3rz7dyNfs;Y#-lLfoe@QZ+a-@&=zT35ihoq3&@7TD zyDTVodxP3fDRfwgwmQ)ds_nsFc|CYJ?6_GWRth1hVa#t8DU{@_36%rV4kU1YQ@^{(32Aw18=ayJCr0Ja&BjNRdH86MYRG=9wdel#N`$aZ6m7SsC zLNYK9M#&25O3+p|YRuHOLHWKEgOcfwPpVllVe*y8MG!xf!_qc;a4eDw0W;wcoJb4N z`f7}Mn-7s|XY?T)5D= zi;y3nSBV_+Lp4;!!k`hd+tM)rED|!$isVlH<1DkVaH`cxxJ>5~qFwm=capF)2*(>( z;SR8IdiU@gKA_J?k7qRgt=_y+lyV7@fBN1%ho@5jU%_k~1WUuYuazUA2q%R%Hb@!*16N8X){HM?_X_sz%=X1ajJ>&58l zS4d8asO_Fm={E(whwg&p-6IH8Zz3@CM5rOy6fwIWCZX9#^iv6wfe=vr6a~lQ;P&zj z^7z%DG91%a3c!}T{onf#Ltg`ukzk_SwEunOAnE->swgz(Tz2cd5VcK@I47mPIu@RR zL~;=b7b&(U^F0)^Y~zUl7Kvx@8%BOqiH$+^;3(AcogOS!D4_z0-07C3;K@@ut=o7-V?tm|sh&BXtCu}F14RFMoj zD+@t}CAW2rfq|tIUvt3+YM$b1oUo*+M4U)cGcq=h7hwcD@IA-%kdRG#HYtk=r%I@3 zGW<6W1;-|fwo-RA32}fULdbiZ-=cc|=Ncgt-QgF&v7ARg)orD1iz&$)z=?T@4;3nI zfwYI8x@#Ez)NsUPiavY`9c-7aJV+P5p?#`#;_I8X`U)GV$|09ZK*3hKA1-cGtwOlu zJC6g%__~%5EpGcMC~T0_%#R4iV&f%j?~wGc8mnwHw+ZEuunx%TM74=WPul;x&^*cy zgEAWtdMaC9ocPW$i9@yz=05A;)8u}1!V>iwlGuBW7K)nJ!E6%ZVM=$%u2BVp8M+$> z|MW+bmyPPbck;@u#KdsO7JY*Nw#En}Jvk0634nuZIA(^{AlYZgR>>OiuR!C%8Gj+M zfWZ!(W+-Xq{*94C4*+C@9dQ?*vMUp-2un`@KB#>-?})r^qZiUMHVz@)uB4bmpB^4K zh`7zq46fmJ*Z#M+4imCHsYEdmuVzl8U%1^e4XtK^{W5gW_>~79KdOE%RSs7}P&TM~ zM2*`8K#~(|j5IMD$rBt*NQRtW_PWRhho2KJU4w(=eK|>Wabj~Sm&h#+H8eH(|NLp) zwNHo^&`wj-yiT(e@MF*sR)ctnsSi~k+^CVc4rvT6vMH-xq943|{7w;<_UE}MT<2GE zox*%#+Rb@T`viYzxS-?wKFm#KoMrVyE$t0=+i!%=i|;WI@zkbKn9LFY>Rbx^$$3j( z&F7(K)4>yl8`*G8E6@mgcvSts8weGV8HI~io_-*ZlAXPEp&jn- zgabs3gn@u**e+vHBS|WL$&Y?3VTFT%QEnlbc9yyaK_AsFIUI20q1Qt~1fl~CJs!rH zm50$N4)g5p?g*&litsQEZ#;(TAEe3{ZG92lMlX4RQ$C9`mQ*wpaG}ZpYD_-d=_PSi zDWHZ2&QaF^s&^&(p?88VIXmOEpxf0)T8y!8iFOciX;g)YY#df z-)i%>8vwW@6_0W(@5ir9t+hjU-m`U`K*$^X7P$T@agCq1Ad+3}q}y@5gVJ`F>Iy?rl&l!aiGoFDc8 z06vj*C+?D9$CBhO}^82ah-{zp(;75ZQpjplK{_CyVw;vwRSWPC35WZp}M83Qnmm|3*d-%qc)qCs~&~s>TS-v>= zy&bt;Dg#wBSyz@{-@!d2?O`YlI|ke6{`*;VtLr6b#gpM*H<7H^pk_U|{0)Y~t^ON-+s2SXZ=QL9ZL8W&|R{8YOIH7nf;`g5rB%r54MLf00M}#FH8|c&`Jjm zBCyr)aXIjwR53P$UGbMqyC$qjY#9{3HK;H!bpNsL9Vgte86*C~&skc$aEbHJj?>=K z*NK0MKR-H^*H0emA;Bq8h0A{{RU<@puX`ei%lQxP6ppV{)A_JjC9l^s7uVhRxpIsF z+mw8z-5~2{<*R9>Q-}W!e!$9>z#9(FJRz3h7{JG_i~OK0pmwH zbVQ|DNIpWl_r-0`{!q0CCqdUY8gI#tg?FpT)5Ewe5!_aIJ~UTdhngO0IDATwIR=EP z2ol}gV`a-reA14lki(FWXoQ&P&)XNu7B8;~U|v2~TGAaW@bTF+pfj7?lG&ymuSSKN zr8s3TK%#_^E}-3Kd2#Q>UPXR%EH#F7ch}zCa~N`2uZ>QXd3!wwpcC-<_Y~S?Gw7F$ z07U)*EvM*~T5GpYyQ*z)9Z_jiG<1Uyudx5I@g?X@kK|_C-fZ;&aM^w?$t$YqvaN0A z!tB_{?F`+dCy=X?m2jY;6et|grTD7Nqj!;shj0we65i3|@wiQ++%*C1;t}k2JJJd& z`|_YBHR`pY+VnuRLx1i5ZP4ccy;CuqB< zu?}}lhVM_gHzX?!V_x+^2av@3g^4H>Xjt|qYb>h#cHVFp!>lv%k?LtLUeQw8f_61g z$}gV*j^%+qfeU!=*_eC@f3nB2k{14FW>IOs>kjDRp*ZEZj_3n7z(`T^R@U|V9#=Ss z9`h{8Ek?towF8rpFJ>`IAvcIDc(?=tDZ&ZS9;9mRvGY9;Ug!b~U+kzc=6!O*iX${&>T3;0W|D1>$pVhR@VW_q@H0`JA@Fg3`rVlN{rxm0Jbe z`y<00*Vv5gJKvTJA+2{_S#;bZ2UsX$?AStM-MA}PNX5eU!!&9drKsnng!TFvO;GmA z05!UOE5I5P}jV_2@v8>p0N)fDZNaSr79Uf1nfb0+MLC|1i*y*?oyv@_Atu zo%-|ocb)y6(!DK2D7MJ?7&WnyiYQFHbEj&(0fkbq2TQ48ybFuxWSMt~(RfdpPx#xb z*FR?tql9h*wQ2^k%9_r>voI{m`(oK^es%I1(2{Sn28jwuv9U%Yo$~+(Odv4-Vpi;K z`-s;cOjhWa%0#L)x?0Trh%`6~)aA?skOI?&hI?)6B^M@8^K6gR~*OOp}0M_q3Z${g~?sAiZk_fz`fu zzHrocVHq|&F-UyALj$MU-fou7`qjGVnC*qwk{Je{qB;*!K&O3*!XXoMn@zK?XYyPB zW(~txxdF}#ucv^Tj>ru__3RQ2DL(iB#8TZyp)W}v{SBuapnm)uc$BV^_U(^L2WBJG-l|EnWpM+m&_Xu z9Zo->{km7&{Rir*EV62ths{0v=G^jz)e{#YXcRqF8z_pe)`qLiG->U8H+BGQ#M{x) z&B?N^uta$bB6J2adiD1B)fSK;dvX5~Ht-e*Zr5El$8Igbwq%$|ksSxunPt=?rk&mNxUf1OELiYjQ{Yo*Lzza#Fad ziFEwe(myeY0|!jt{&}oqF{>7@|I|4J`K9?Yi$I#g9TnJ7WO@*f&F=AJrGThzYu8A1 zQAb7t6=BrzW(bN|%>B;X2p6 z)g143rY^@DjiF>9%LDcuYSx?qHf=A z>6N{{f2u}gFT64_W~0SzdQs`s`}D5vaeHW_d*F$bOu?V!MfFC{&+c{6y7IiS%e_l~ zCobsU_Z>ckXIhl3bY`QSCsr|#r%k3ln9_5NP`m;etdi9Z@4b>81_K&Nv^aXutk@-U zZbIi$fAFm|oKGHeO$dG@9@RQ0{xmewODV=c3E#lJbMm3Hq_J`Rqe)b3EO%k3j#o8R zatSh`$BHCet3PCCVy!V-P?ht0QO6ygreMDV-gV|9eI{2rb{t!0D7dGBwy+ppyX|4?}&u08L(T)6zD10yUtXt?5aIWTWOQK zhM^3)<7IzTkyp#_wW9i)zix@$ef~oGf|neO=1!Jgb+4R3KK8drdK^&0iGZMx){F)5 z02Jxcb}>r35lw~hd=R@cU{q3i)WLwis-&bYa^g$QJv9A)dOlvLPs0h(jz@Gd)#R5G zG|{t=gB15iX?@09FPZ;#!|9mwFbTc9B=mj(uX^v5U%i`JBh!!pbI;x@1nuse=XLNC)*X0-ODJ7$lG|Bl-SYDpIH4hNu-9xuz+Z7_(EWs}9s9-gEh% z!?B@KN9G6D82H$(U;g02?GKt3x;o61djI*s-G%Iup%o(Q*GM**+nH;wU+)Yz64=b3xzx8YHSN91szC&^UI!9vs?aP`{<;UuQiyC@O7i zBxv>j!9Rc{hTAD5;TbZ$rlpX@VyO&r#`%YpXqwa+bo_>3SARF2w5!eoqs(HVck)O zT$4f_6wzPt**CWU@HX zquMvWul;xtXVVZa*?^>dUNL_CPokXQDLDd0i@~8o*D!hYpz*Hq;afQ$CpfV2C`cV@B z)-v%sUGGQVzyAbXV12XzT7fOLAljr&pEcP7h#M)V>4nli>7qk`aX*v8;>=CCeSQ0& z!}A&h0;$2arw@?Y_j+#Q3(qPlD(Wt~5dOktsHsR|OU{q{W}Rl+jKnYPT@=ds0TjQA zYwjVb4X7XLE_nsb=^OxzrV5MG(cQ2X;fGpFZNsZ7d#$+{R9D03Tir9-xbJ*i9pY|a zeGm5f3+TG-dnwS71s+4NfVXka)!*S@$?wpRMJ|rXTLIg`uFG1F!qX$9h*+N2;rrW6 z3XZ_GUN3fs*|+rjq922p*MBa&&{O2KQV8W9yQeOidTkB29dgqTb#-6RdF?peN=GTz zO@48JLQ(&O6miT!0mo@hTXetXtd{|tO0>1mCTl?Y5`WO&5-X?}iWPgICXi$Hs-SV~ z-@o7W@%x=py%9Y(&1Ag9VB82Y|Je_CYb~e_xTXD*cOcX4zWgkxs0>c_!;llV{uGxo z)SNMb*sP)X$^Z-2>m({SBjU&BiY>T#QMSbk>y~^l6x<$=4G2MGn>%G9H@J5iUcOu~ z_Xl3RQQpiH#lMy6G!z?45;s5)ICU-&C)P2Dc3$i(#gaBgEt6EbFub)kZkLK%r&F*8 z20atZ9Bvgjn*+jaLYAFpI)+>Q^hFDjiI{@p^O-MMp7C zjrh);8c@nI2ryiQZLg#}gFIP}EZ1U$HBk*@U#&1QkwRY18+3HwC6dVQ%V*Lz&ws_T zIDY(i|Lt5Dwb$O0)~&E`_D=lMv%Hjn%Wd9z?(A8z;Erf^v>++p@nkf%=QIQyYMsZj z6jZ96;(zc_d}jl&5A*Y2yx}qM1!khHZRg_Cca+YbM(wH-HI3nM8)(SaHdR;IY@KUC z`6LIdsPZc02r$qjN{p0kiMWmudh6PXLk6>x+0LEqrzM_XVbzuBZ5> zTX;}Y&a3Fi|FODI`}gohchSC-Zw_Y9ChQSz;`c#QS0@qf5BECDP(ZnNAdih(f~~kz zRwCY7$E$iY28#NY9SkISK4T8=+s6CwoB774u{}E!f$E2qb#EEs?DwfHghjD{Oz>Xk zI4}{u!e>=L0v_VRiWcIdC?8Q)(9o{`7y`6CoXli zlDs#j^xB)gqTs(-GCX`JaLh?x1SAX`@`wJ5}{tmk#ITId3%$bzExz#Ei<(B)cm!pkffQfP z(L$NSeCkce4RS$3=-%oBt%+hVb=L1y@IhK4JCJbc+rmK{_WUjlO4`HUp;FfO$_MZ5 zGs4%X_S~c6GSm+t-D+ryb)r6e?pJSxQ!9`B)hju!(RVNIC6Y7=pANr3!7*Gdr(XRea!}{)f@Cg zB&=p4>1rnp!Opnw1$KTjSeRPQ8QiGMHZt=ynHw8FLjG%m!mzs(Y1ZEzGi`d9yKV)L znEK;mtUbsRvfwbdwpj+-VnIe)@@Sv*J7W?P9sS*5;KvWKv+Zn@sh~>weMi$1Z^UG@ub3cINKVqAsPb{E5x-ai7s6*Gw-op-4+&6~2vk+W z_LYq5V1p8vf^kc5Z2=ha$h^#(Pu9gI++#bt6pZ9r$>u;=>(1H>v34VC>-5;;&vy_C zAzYQDuu3nOBPrSIwJ@FbxXOYj=svuoXCNlqO`FmYkag^E)Gq$jlm=%+&GLPNY0*0R zIyw;q0L3|P2S8wEhhO1#8~Zr}*DJG1tNGJ#w(pl1+X%**Y+oSfG$0kNDB@z2i1M}L z?ZF9fHg!Rbx#_()W7Or+tX=9(ds5g&P~+9;vl~Q;i{IzpWcRc`tg8r8?9^-VdxxA9YB_<^$<>JA&wp}=3_@L+WPLqOdJ6cIY-37F{ zg)cd>vdNiQKZ7618&FXi3IKxpTIT__*!=IO7t{>8*+w?XdDwu$8V@jZuBuBHGX|~z zJMED3kvxbiTx~7mwWy}KVt^b|&+S^WiaHs=LA2H1Q@RYYgPnF3Lnn1OW1Zy zIs-hA*L4;;mjP3j2Mohi z19P_XFnuwengj^f3=4#r$~8NshEqyC6gPv9VEa+?%R{?{PvwDHxIX3}5?NcL{<3+(ZqHpM2{lFhmmykm2fvZ?JtDB=E01!N zDJPBXh6aXYmrGK^>oH<{^?&GNgv; z-S$T}g~FFQwrovJ>3Ecynp))AlYK+&K{2r08NhHFQ3yh`iB46#425N=kqSPT4gnW6 zfUC78Sw2QkM75hPG z{kCPSH8+V=Pe^zR4lR5+9%MNPET<72lhLtza-Ics@_)}Gu`fJbPoa$QlWO1@iWyTR zrbSDbpR+^0;-tl8xIkE+g-5znSmn~pE`NO}_UfwzL6-g!gY<4=bEyB3p1^a{t=qS2 z8=1M@HoCv(T!cIdBI}oiT3Pp-lV4_RKJNPE#JM3-~{;d znXrsK0ONjFQzM!tvUpJj`ua_wax|3ub0`i3uY<(niYRsjJ%-!;OHl%VTZVz^nj;iX zZ?uB_Fkd1P*6rP`K{mN_Kh9wXcC-CTNJy|=e^ze@!2Hd7-{lwLr_1H$O?OoIq(GarN^=E>^5ubZH z#<3(;Y85o8x=TsnfF4k{9R(_5Uobu1m3qOFvk`$%{?1ujoYALbkj?;zcDiBuH{nv< zQIyz5-u;n>Pxcqrp2eDV`tft&6&p+N4ED+pexMQm&OwUN5*OO=%I$*o{ev<)+yX_t zP)hL0UoB!eFe-`ssj#Z%Zov~k)dY|ImTpXRD@jFcGTi<9X zzA`Gx=zDi)Bm|^;=#nl0=?;+wX(gmlQo374T0*3e?(R@&q(f4rTM>qyd&b|r_xoKp zKHT+p`N8F4=H)$S$MZaUpR=3jDA0lr?;(AraF0MK6(+vN4sMr1#QLfDfDd~BY0xiY z3G~ZWg9Ru-^3eUKb#$13W6y4b>d!oAYury|1LF(UO)=vS9RXGFM259ZfJhaRNrn)k%X9~zHmsFmhkz5$u`2+~6AriPW0@-&3 z)CpdzK47s2nLr#A5CAK)c#B~2j1I9@<#yIv}naW5;mk1q(J}w>;7-xfJg=MU4T&4*krckf6(T%SXb1$ePLe> z*MAQJ)jnYGr^84V1pN0vsd3r}0QIisQ7h&LlYoAe0%b)w&z&6Pe*_$HXVEu*1r~yO zc0d7nwFOYMD-2fw6HT5wLgF8!s-G$8=T&*u=CPX(nqJfZl+XotV>C;%vH6pd+dv>t zh^m6Do-&BqQy}kq0Qmp)OdBavVmBS0v(t+R7X_ z=6VJws6EJ6XLA=6Q*<+N|t5Vtvo*GE^Mc^70wtAZudW|wSav;u-LvZh#F?*axBoPq1|6#<7R5ghsf5NjUtJKvkS z#UL?%C$MPYy~iMo`2q-o?570IWrlngOai(OGx|cv*};Y`!)yDJT2>}Ac|MW zx{iUWXbG~^48)BIkf4fzqtq$=`HU>vg0dzUJT6@UL=wPp%t0>u02G>BWUiQOr2?(M zl+6{N3R$bS)*rz{pOkPeHR+q1SFHtJKLK^RIe>kB!Hz+ie$vJR$-Vto?6=G;24lAK z0dm^QbgYW;8w9mb2~RNP=kshg*eXx(h&S@t-2h^?h#jO7vE@=E#w*jxS9*{gBj2if0{>TI5UZ+Tq+S_&u(k$gt=S&>ywFiKZQ*mp3aRt_AQe$wR<0Pgw&9}jLp0?M&K zGl)#mqcQ8-$S12{;3BQ264b1pBy=wfi3xI<_ZLX zG2cpXT!7jCI0;$6eF>aj6I`+@cNhc|qqC-roSft3H^1V?L7Y2UXj*ZCJ1jNTgI461 ze>R3RfhQ8Tt$~`rbyK6uN?Z7N5TIKCa}zNsG2grdLl-uYw=}A@0$Vt1`0OqUNPX_T zn@IV0l!8jgl-&ig0Mf%=tLN+QP2`0EfsIVmw7cx>TfBHm!D1!Gjuh(h+j~GV zHUKAssNb;t1{p^9i>Ab3AyVDXFzW{CgG$&T<)8Kfay$1z`xX#>L{~s1rIED`9Hko` z9Ml4Hpc=u2)w?}t1WpsT^dNEX0Urk<&4Xru(JjEZh&Gtyxe-37S}!I&FtDMB1RWpc z;Fu@UY9~SUrW}cmk$vKT-fj4e14ez^-VGoTvPI-C5Zx~6=!NVOcsfq{lvq~&PFGuW z`?^a50ZbX`U0-3eCRUVc5XcP$Z2Ga|Gv|dCPYQ?aj%9HNRGDiq!Wc-ddo)+qAXM3d z@d>Z5Lwj>UOM4>#(O0Zss65v+IQ91P90L`_#@f0Bq;EA)&`CNSVjA~XNY^2A&-ElPJzZ3WamQDpl8+yn3y37N+GJq zT!Ex&AiNoaLsm{6tHIXp|2j%$kwhB&xC6Kac2zGJ%&NZH>P;vlxtU`X!Qla$=;g@ucC<6cM38>jnD>0C|)zu$+6N4TmCqvNTvWFzo&h<>F;9h{8 zvCeNpLr9XcI4tRqiDcmb)&NB_C`%3X8B$yY_NocS>&75MA5zT!*W7ade>OxJ|7?|* zcvy*w?0pJ$rUO6)`R9MV2@3h~{{y{L|MfLs1^(~0tNqs!|NpkcICocfLpU}^g(($g zB*V+vdG{qaQ(6l~XC4pnnY0Ii*9EPKVq#4PC#(^1LD~%)qWUDfhJ_d8is}ZOb`i~k zI6>uqLS282-$NH9(O8qsdrOB?yv?6n$Y?` z-`Xv_{Hp!6c#cb4jOWPF+Erl*hRE z)nr}EtrC=7u&5x^-VFKOe4SaN4OKYcU(ligRNFE;zaLUf#bhL>Q9jPtAZ_}uh4f}o# z1~B@EK*hFBKAwTv9KjI=KP z?+?tyG=b^!Na%1baVFAoBwD2o$EFO%ka6kMsA*c)AE<f00}f`^4o0s!By8 zR1#-p$Zp)EG+8J7`dYL?fCSryop3ZMBdI`!UE+KQ{DWICr9dXul9)z`MC+?wO^z+%I?+WOOvlAZV%9Mmja5AJc3P&e^r>ZGuaM0QmZwz8@4QlK{ zhwL(r{ss35J6MHE?FW^-ikj=%{w(B@#X2RD@+8r6rxA0f(Q>5Ga(nHAnUjwU%FjC@ zLNOcNX+~~)72NGfoRn$o6_mnNLm*>lPH7IyBXu$MY2YA-noFhym`Lx~} z5Kzs0eN8h|yRlG|q-GQG>s`BSM8aDc6&!RMbU6W34gpjKj8UPCQXib+fh6It4}>$3 z7UY#gq+*}y_b-7t^?xYIC#AMdt@Q35ndi`HGB8*aSL6~ItmR~8+rvaXP+4Z1lPUM= z=Kt1h9miS#rCVIJO&HDYaptyGH}qwz2jE z$k(ZnblXr+w@$4#Z1G5~s1uG&sWNErXrzX+Tkwi$tHL%G++NVj!)OLv4#!2~oJH)k z=#L2=ii{TQ<>Zj^q{O~2ulOz&Nt8e|s>9A?kvDn-{H#6a?GcWRhapt>T`C>&A(qrz z3)+i`sETR^9L8;GlYeGo)2g%!XSkOVx$*Bqocw-aJ(zb1!;Js&s0nN%Cl0Q@_Jvja zFC}Q9rSZpq&g8An*O(=7%TSOtIx~(FUbmbZ`n>HP9h$7G2|6|M8Blwg0XK^W5@}gn z-(bE=UWG;mUoaI%=7u@f)~o&)c46H##TeSlT;k2mUu`kF6L84zX)>Yz&87RPL z)2#FdUo##xoAH$A_oxd|aIdj*b!Z!0Dm^33vNkD7n*O+kXr>9pRQ>trXqjBXIA3MR zRTu`0j)#NNL_~maSR2ZsY^$b>p#wa~jRf0vkZ+olZ`uZZ`o-Gr(Va}}U?cL0xn{3u z#Jr4}^jkw^GMY1kX}ObWxjo<0;<={L@}%h3KJzLZ)`^&86#YhNh(z$0 zk)?aC!V(>PF(={PIwtvxHCuMBrp~i;BH&x>y0)opN>J{!zeLD~7-umJaK0zyNc!h5 ziNxIJO_i?_5A14b%JmFdR5Tlm3<@8en=wlmKgdbHUm2MzYw}~Ej!6;x?TW(h|Eolc z9!k4BiPh_YF{d3nAn5p7 z5!4(W%Lx6LhkAFq&z03lD%len8q&z{GV<^;O0)lEvC6pX?A0f|4OK@w5r3-$M&H_Y z0pJeF4REUvKt3e~1GVChr~dH1b*k$tcT6ES2gq|&J>?0A>c${QEwL#oU%F>>2DG5* z41(Muw+uzPPK9!8AlM1$y4e(O4f6>uPu^F8im)(3nJm>JcaL;~XBoi*YQ|pCNSrIJ zrCFIIISn;2KdW;C45eR$%WHDwA_8*!?C=JhBuVNyOZV&Jl_-kR`@Y%mpQ} zBk7JgRuEPks%_CWD%KCAQ|cU%#h%o7TeHS`pjg zZzPEK@n-4ex!mfC@I3oAX(+4hei)@_yAT7U%}Z-*j5wM344|B;v2 z2o5S}-~61G5i?vHXQLduUt&MVr%@{49h#9ZbFeg4v>r%;{Y8nxnuE~olWKaNQwDJD zy!#;hTStlyVtYli_%m-KA9hFs50Mi2yB13_6hi`FR3hynBbUxY2@&C*TD8s&RG1mV zCxHQ(O(8Ui15Ms&Zhq|CKy>`za51+JCDyI|izJB-)X`g2HG(z^~&+u(u1%;2b3@2@`6tNN_I7#NLO>A$R|z9lg7k zQ#vsfuR`?nEc_w?6tT6DVQ5T7&4b|-Z*eAM?qkx*wSs@P^+sx84Xx*Z#nno-Qno5b zaWZIlk$mtfq+-x^aq}pwTfbJ-;4?nX0&ot42&(IZr*;s#tT#H4@3hJ=T7w3E0ABa^ zDxE2cvwHZEIc{JfCHjC<-Kg!70yTBh5=#|@62%+m>s;ak6mL$Y*+Us8kj90WOkmCf zt26ELfANot!hdyDHgiQCExE|`{6mo5_B|?j08CbZx_MbJ*kcKu(c%Ae_|;^))2M3} zs(2Nu0GPg4=}Lh@*{hKuD>`Ha0yfi3?~@}tTi}Gzsq)Wo(kWfs24%DW4EyXR@yN|x zl0w)`GQHNHP@@_s5>YfGS=Gl2X;~GmZe3lp@e)Ta%F8S$OeQaSA)?Q6+e2k)KiH|7 zF2<7h$ss@-{`Bp4|hz6<9#v7#n>Zv|IZimahd5SWp>>%4@Xy zy?T@(Ld`T9aonzyxDV6q!4r>~h)htYH9WtG>e`^|SdCZ}e@z?Eh{s^oX$r)W8}q&f zdPx>>D!?>Vre)vCO6pQ9jDY7z7`Ik420oK+@58^pK>82*_@v%$zCLW-tVv%a$gN}m zE!ZF+E*i92r;fm!R4;`3`K&@hQjB`?1lSWEP&X3(fei%7x#TjW)8R7f!8}y-dzPvE zu|J32*N3<2a;B?+-k!8yJQ)X)2Lr2H5?+tap%y=+W|WcqXtlrdpvH;Iph{c^O7^hk zFj|ii8cplnY{i6mVCT3d24ThkD{vSIFhagf*$2@?yYH<1*l)y0fpH6zZ5d;$q3DLl zs3dXpd%SD))ZkrT*X&f$g{Bt=?`n2xIR*7-xPLN5UlBjr!S+rGCsHAj#e$T?5mWbB z(D_k!pIIr7Y%G{Gm(w*@x`|CrEyxFb&PE(76b*MkG=KSx)VaUNF&4k(Okx zF{JZ+fqilIlZ`df#1>@Pl_rPSaSq%!%!sq7^_1!o740^K=~MajRDT$-xLmi`!ayZ4 zA+p?WZpV??loAb+*}UWQq{$jZKqeHBjhivc&Dux2Me#W?4l0ohL*SyX&rK`0srd6c z1ANvLwhwm_epFdLH-I^)z)^8Sh7<4WE6KlMVwT&j?NN-BPz$vu@9`Q8DBx|EGiVSx zC$zYn>-xjct>@M7xC-XnPH>^wpkdH38qiM9{TeQow(-bi3HHlO8OGF~KqFQgv_|9F zty^^e^cp4i4B4R}lJ|#vgR@(D-mD#y%QU@VwM8~sh%!|+d|03NMF_TFIuFJRALxrA z7BpFSx{2ngLGJ=p&#V1GOFi@n=2cTa_>YyCw2@e6KiAy?o7#-iGl9p0WO2P1Nsv{MAIDQF3 z!4)F#{#Xsn5!eLvKzdedX?zQxrFAylH;`Y!ECv=i%s_Vcuc1!jPyl; z<8sbhCN_h&of%bEO~#RiYp-9Ny*T6Af3&)^$LZS8c~0tA-+9LEeRjGm-M^6aXIP}; z@`Pvz)^@s4Swq9NXS>i`pyTvRavlpOb>9y-}S|K3beeU8)HZ#EFKxN<{ zR8K2i;1?uO&!znH;3buk7K}&>N|?fA*=0!bY->gKDLIQRLy{-o!`wGsi9Plp@`OP$Z8BV%*Ai* zXxZHaX6Cu-n`%|-bKsKA>HeKmI%uVq`}oH)w{qb^*Km~Q5^VTYg%PGdyti9G)!fGI z@$s@z-{VgLGClGV5K9R;aQL;x>^03&09Qo~Gn`0I&A%~z?!}v445mAiSyb%Ty&*>9 zaYjA8rT*^bo(ryi&MiVA#nUspe>kw3L!&%Y0RQF|{3rt9E zC9)FLBhus7bMDi6TrwwLSyp+51+tLozgAsDqZjGjAR{m%3Kjy;6!l|aiAy_Ay?6s}Vby5&i4ZW?IX!J#hA{TT}YOMc`Roy{FC z!+n9|vzYk8`2q}O*~gMwR2y5Ax1p9&S29F_91?Z6OWn0@00G#Yq&cp$DLxibf*r`s#J$}M2t#l;XofN6}3y4bS`Q8+i9LS zrElE5b3dZ_p%>g+QYGV3StUt?*N?+qHta7CR;hY7^@h==F?^G|jQu9>=+2Y2LWWP* zpfNX+Ua+#f?Jy`yj@D}?(v|fTq5dDkpSWM-wX6)Dqo+3KqPAO;48Bdf$dSCkt2t9K z>f^vL-e$wX*{~>g8N9cWQ?RjK$NgO8``L&{;I8Ds9GkGWh3BrQwi2}DiL_KjzQvlR zv9v>SQe%0unfb|+u<@}Sk;2JsR>sbekRv;^0o99_MrOzj?@!4{N}k9<6b~<63Sodd zWWcEhTSCTVK=J2F>fnrCizZ@lPBx&$r3i&3m7|6K1zdm@BPr`C zu#b>Jm@29TRyLcf8Mt~L2F0P~J#41;xfb=C=m8SXUQL0?tB0wTh(g)zPn&1(rja=| z@0N~?Iep}AFsHg)G`xI&U$v3^Odz()hkjLA|!40_HPph!!=7XA^h= zrv!$ptG|e=T%$zyn-L$mhXPKJ?+4U2RHVe}kwQ6~nDM=Im~H+xVvTR#d8u}o6uWrd zq*}6uNpWnd>Ur_!t$xvXpjN|XPq6BNPK%09@gy;6)@0>=ivNm~3a z7kL{G>YukC`o3^k>pQ7kENg9Ps*N2nsn+#la6XfeE)5tt^2~oL6Uz+5L4#eh`+s1R zwwc$?=s(X5>-r;rmC4fnd3QqBz*JQJbI${7lXl%1w4-m58k>B@EPhZ>cA(0bhNk<6`^h8;;6V>xSo9mX*M}r_Sz&V_LLN z71I}Uq)eKXwqA?0;j2!?W;~Igha?apmx<hrvr)>F&T+zV#j4hYj0B!>hY9tj)SL7xttGspj78%_AuTvG!ZByf$D7+Vi(} zTNrqpI9=0i)pv7@u3rSj>35tBoizQ0BAIOt--ME*bD1IRr>?Uyf?AqaeAXQAg8aPc z*6qyJ2ySj4KdoXL+DNxf5<5@tv~^vG!yqH$gp!p@r^bdhq>welGR|8-ys{a66HYNYqA^!ti;mrYi$uDNSGO!KSid0QTwV;DMo9H>7(wK-;@J=8}FpVgTn z!gJ}y|LEy0@0EigE71RQ#ngR5hVZP$%d`v=FJIn-uj6#J?URE>RML}qPcvD8{D~Vb zT56{gGa{lyGUPouwlY;ymp?e*M4n_j8%@i&ilvuSVAfipoN^)zpON%XSzIJ|E}j4t z^1#ZEXeY3t>{JEQWR{?ib>2AL)UolH%?))cEuIvOPQ119=b6qAD^Cpk)jjKZ%!Z0q zvoSRl%yX+(1KIWGT6PX}^kT4gM-{t9ao+W9G)DnV&*A8hC+-M(k1a1qE7r+Sw}`gD zCK?$|-LBoxZ#CBYVU@UUr^aWE0vsg_(U`(~#h&q@P7y)41y&de+0WcfPA+~W;N53e zqx#Xc4Q&@qIbgzmaTmlgb{N7W9pd;)K&Q-W^_9e>DnA?6gp3{lV3~GUp=}Y>KJ5 zme$R?xau`7`e%hW|IzQ|cHwW6ZdDZ9xu)P1Tg#VQ zHljTS>8N?uKbCGu!>>PWkQw2yVItL<>N{(K8;q-wVV z1Ji{3VP&gE^sBXc78JB${--6s38{TL%d8$AzRzvl7q=EvMSmv^n}}f>IpW%R=s>0< zqHiMcl##KF9(lJ^y$0dgE1p(^07q*w;&V|NFYppW*5*9}vkw#n7VxuM zX|1Blv(Bfnnf$tAHTt9-=ck9vg?JGPD+5`X-$r?Dh-m$5%dGyoQ8Oo#Klu8|AZ2A< z04q45U9L_xNH8Y4gz;ZY$gr9ep2z^F>+T_V!hJtFEVPoc;G@{76a}?zE+_pA%4aUH zI_+lIT7pg9u2oSSB&<-c8waA{M2yUw?mRrHXeADlIFb{1^Xfa?I6-xM6fKQCL+!iK z9Mcp+ZY6?{4GZ5Iu7tGt?Bz_0f{pE%AouU^XXRbo^pzK8Cl!}8c#6}MZceyT!F^$t z9_tZh?W`>Z%jwlC3EWxQ&)q7O1ztgzLc$1{3$|#2EOp@M@c{z&q$9mn(ZU*uBa1b2 zOY5!awO`V9SGnsi9^H#LWszUmTr)La?j|oinT)*_9`Z>4>5x`qoC_4e~XcvJZXvJhkaJ+F(HDYL2Z z%-eix0*&@%UraGxA3In?AKHm*{Pm5YeeMPL*Jv+U?u?^l|9UI^MzInYV$l0-fRK>Btq*7-~8?3j3 zoiER8XvQ~FfAo=~S@|kRf{UB)WpdG(oTr$9y!wzFt~z8;Z?JDNWBJ#$0JtDg$nigK zJYF@gN0BsSG9%b!5Lv8vQgQ3=LSm@O^ycrUy?&h*M6-+t+EOSN zG^4}rnLFF)?Y5N%LVi$#f~CRB6lXp*3#uZJqJh_C2Ja2{>P+bAYppB$_TW|={GyR| zulm}L;!fzV@@$_sN(-My(929Od(Zis<^{Zv_1_S-)_oq0$u?Chb>Sr)ePMUm6W2R8 zJMk8#WN&`;oTN^R5sg=mmJb3U5ht~tw(%aa{mHKShFn3ISb2yw&R7B`U-mFR*ZSwL z?IBhO`M7ejo+=5m)TO- zY_Oo4b?`knP0%$go&J`3KM-%o^7Xz7BJY@KZwV7VoR!z?)i>oulxxk`u`h+4Pc+6Q z6;dpAj1Gln!Frmn?~;xPe^1WyH=LR`e0a^J(1|}bB89owc%R8N@b+mNylqvWn_*jZ z0F&Q$y`9hRTa+stO_wY}&`%`KW-L1zzZ+-m==xF33RLOYjc_FmCNhxw*#=ny8qE@Qof>GmI7zgdM16X#roB_5z!N<;qq(o%q`Pm%%-1rD=HE1^aGso zTFR4R3?I4}rKIx3HPS^#ukQUbDVC%IZDqEC{_63;eiPPIiBCq#6FuJpIZU}+b*5Xx zz$~(~_pdG)ZBthFgQ-sVHTx(rK9Pw(*U4#qvD z>!~svc`~}RzRqsh7PzSHk)uaNsNdO3hG?bq&aCIOOL6OyIR*j8fEzVb37aJ>FBdAVAg^k$yt;P38$WcMCHleyK2HD)$1n_$iS^_=0&4LN>m z)v5r&M`7$ZLMN@SdQ@9?>*hm}K(61lo%$9_8>Sc_TV=FVJ55-6tceysRu=F;4`BjE zH0H)^<*qsbC6$Woop&kUeGF3c+i}yxag9DhcP{;*1cEL-?E)Erg_%tcrIL7$cj?|aAB+#`cbK8pk$Lt+#&iDnr@FRczuXE8@kiG`+Dqrw zE$uc>3?cskun4niWWWzBqNCffhI*~Pa~LO8-Q-E77!X2wAKLzQb=;e5wIMB0O!M1; zW$%;GC4gA)<)`H?&L3<2UWm!YDc;l3a{I7~sq20uD@L?nq4!NDZn2$m?pz&US*XAT zmav_rb2pek0Xt`;e3TndlUu7M0I|FA-qCY=*ntz#{Vb_B#)n*5+sguR=gQ>qXMWEy zd%242^h2GqtvLf1ip=Z^CAdxY>fhqvQyh*ks9=6p_Ld_&4ayXzvqPWE<#CjJSY(B^ zSJ(T!w3zCBvML4xrJH+V{Vui=JP7wk@>9bOi)6=6ah)vHi}j@dX_`0$)g2nayxB58 zz=TL!-ky5bb09{(xIxhu@J;`*%lhn7ltD>$39?&K)PX?bJ6V^s9qZ~V%V+V{HPm%} zXv_uFN3%H`rA&E>LtqT*RdUi9*j|tg43^edz+H$4Zp{qH*wM|$=98WTNd|qyMD=+ zz=K$`pFa+>d%-E0K)pzx(`^wn0ZeALXTU>+7>96fWXA}j9)9ziG*s`etFGL`yKkDE zt!8FbGj+ri--NUTYGcm`{-+~iD5xq;cfjSmDnll_@REkYUL6azULqX6dlu!&+8cXu z3Vg(@7WCHZc&Fiy9bzRNQFQhVELV^|>3RIP;JVqh56J%@kKr9&JpNfwZM8nT+x$dB zQPa|HfR@j1Jj~j|;0&t0e$aN`_8Y5_F>lXK$g$>PeBR*)*Pe+6LI_{WFZbwo9U_N* zIVx<;p!VsyFFtp$^4L5k5NFY&klD`Cqbl^}m|4PG9!P6&@A|ok;}y+I69AG5u;?NI z5kkSXGe3u`M7DkUAP(L8y2NfEA=z8ILnSnJiwkMp75uS^;Q78P1UKUoPeX&CPhtZ& z$-1lT^oFai^Bbeo6()e#p@N9xOzZ>sb(P=SIjV2vhCVZ>uo^A#h7=-6j69(S-@^N{ zg1udwN{ml04qNc%U?Y1{U5PYt)!GPil-6#x4AnJZSs^*s3_!RTh%q4oLpW>NvY{m- z82*~u$AUzYIsXBlA=Vz{?xiR_Q+HlQtRw^@hso5xc?E zwPo&PgGcdT&Bs=(SYbQsGXuCu+T0iH*i=3n(dNu|?Z~aqV z{17NH{pXV^Ui%Na0hZi0G z^#46{s@}vVA4NnTU^VRb@nKfX+)QGw!J_|ssxdNhdsXuqlZ#(UQ%Kx|<$KzG^Es)w z&fgMao_@on?KrUX+`hBs1_MKuo0*GpCEfdXvSJpN_cAOz=qdg&xV^Uwd7lRkMd?2NOWgcA8T;TjGEa9ySq%~?PQPQIPh^l8)@-Oy-%=dxl5vm)##E1wF zr@g>qL2k;&(RYR}nT`8mX{NXXedV*-Yu4)!Oz&kWM-;T<$ZM@q#|ZO};?bK+*TNJ| zaR4U)JHdtTnea9Ix<3{W7lyehh(B6Mp5MCPKe0KzuA4}Q*BMAYH92*xT(WIAF;t12 zxM;RR zg{^totv&kN@62~jIc_1j$&%^ry#${Ejy(LaZ%w41!GDie?XxYvM-2b zkuPnxbO8mH2%^a&GPAJSBW1O7Zr_#<;4C@ihl!!q@AoJ3xK9qdx(`g>unL$12Y_08R1Xynddl@X)49`Uogr~=l`C2ukT?2 z#v)xIN4v~e?q2ZavJ+$es8dn&O@I-|dpScAs4E}NPt}~z_Zl<3)ahz579r)3=*z^( z3GSW4ke%tbvoQ8FX?63cBVRp3;B|WT7G5`?UD;<-L2}v8nsU(Jj?dPNb^G^G$=<$o z6jSbe(dDdvxc$uCaM5R_5u4zKF62lV1>!c+`l|B6TD!JO>zgjcqDjE{z%DE0V{Ako zeXh^t01D(>(U6JgarDB;Tt2umPY65v4XPR|rmPU-%&Ent)n=QqdvSgMA&VdgQj>;XhZpmla~Ps6|D$ZxQ?%rU;C-h;%(enwq1Evx>}=tlI_ z!>jUpih{`$5`WRgTyQV|h2vXjQ^SFX$Q`TC>pNv^KZ9ccXu$Q63&6aDm3?UQ2VAf{ zoqM`FYrgK3}pR2{kvwK08zewA22Dzzc zkIhQ9=T$Je*U}_%RDN7$FwQb~PmXu-9J zeuLGeJKt(N-lBg1`^c+6(vysU4V!3b+~t=e>AcwMiDl@*l?W?g0HAV!Yn-y&EOlPZQzVsaTy|i z+BFmuivHoQc#VS^KoiNGi9N`KNhN8i6*zqbEPly^4>j68w&QD3cD}Yvf7NBYfwLcg zP52ezDmTQM*F0K&jbb9;vu*}g+~GdOW&6C-6}4A(Z{75Ys4*!(gcO-rZgbwe(yCMa ztnBFcq*R}VNBn{jKrT$6fb_Q?^O~eo=h~ccPnbxYR@Z&HeVGdIZQXI!$p;@S#@laJ z)>C>P<-G1`wfgm?%*PNEG>=2p^tR0Yp60J8d_x*wMz8zBxnc$*B zXO__bLN`%&v@k`(0{d|Z0H?rS|1b~Wayng&13{b176;%f z#nV63&^_nY=CmizfwTf5nA`BLhV)MIT%X-2?GgbouL~lOjoCS-jAHnnlNbp-a+ER$ zD-Qcuu}c9-(BOVm-PKrZg`RiU7 z>rwn0h99fsXUxdxK=V%lCJwa=pX>s>CH7oyh{XFTg{$Ky=+QUQ&vAGi(OWk380fSc zY4N{g!xHCbCf~GfR>aDNA?4n=%fK^F5z?N)t|-D?KQag(kmHHOf0KMruSR+%gb4Fm z;LThB(eFf2zPVfMp9K1 z3@QK=`+?}b`=}lt5VvjPSP<@)%Lb?Xr4vMd?|xrpzDvf_G?L({yln9WA^@PsyZ&CW zp_6E9**(qe2ON(VPCCZ5(B2&*OsHt65xe+%)eEFI^mgGD->XEk>N<2l zcobCPinn%_T-{g$_7Jt`N9W;2{i2`OYGce5N=wcDosCosmPhL?=j9q+zu)HHiH-uw zFGh@Pv7wl0=gs}C8?JfDCUG$O)iV}mHWG&leOCV15a;F|5k%*7m6~)G@EZA-9;quX zuO8}4IHJEppsE>PGn8a{xI^b*wLrpTP$|9!6~b^rYh4;XPi zhdZ-5xUT-`M6)>88pVd{3s)*0$U6%&`p^D}#U%eiz&-W-oYIlxxXouBS+hKtF||MK zacd_cX#PicgmrMCR==V0EDP;`J)`Mp~F_nQD;>UUj63z$X?e+nuJ0bl*A z8MpjMn=aq?cbyC`dxV9pfvGl5W_CdFOJ{xi1{EhiEGQVbHoj`SA!}pp$mggZO92QH z(+V1Pa%*B+GC`&*x!6T0uai~;W*2DschIp z3__=Qk=z_3*0|2oaP}&Q|j)VI*#>rW)W1NKyOnFVF$f7|-W6cg# zL=vbw@580gvWw7sTk+2$411@1yNoM{25sct(!DXt-r^&!+vV%)M3_zUH;uzfNA4j$ zKC^GNJ2!`b`VJ7b8Y(VLzchWYJ-$S0Lo&hM{Q(rquU~#?-9SbI<4XcUPcn#1A;Ozt z@zkZujTsAK%%9>83pa>R;wHd28!T zs8E;e&<33mJgEJJL{Bs7Vj~K7xilK!ltZ7_*AUsU0}5CcawiMlp6_>{7H?@uwgIn+D`_8>5<9<|^BAV1wLRB; zamK(e-hGof$*$tliXSjW8jk z=Lp99TPcCp)_B8j)J0^4^6ih>XXZgV6&dp*D7^81!$K=Rk|~kfJQ}5W4oOBOQX0T% zJY!6twr}FS(K>qUewWW6iw5#)h5IT>oVTFI7EINoK05xOXW@r(!XUY$f4N}mT)qwH z*z+KhQSEPrJ4VNA3_Gz;GcdgW%8j&N3R6?HHMsFx)1X6&*$Tsw7w?mwU`!zX34I2~m6r&u`8xLW-X=>~T=|40D79&odY zs*%Q-8v%{6mwn4_nn&&rJODmK^1!7VI0NnB7#5+qg8yGnWH}C0YpLC;BlbrzJ8fWX zAI9h3Ffit4^!^j)6IZBbH)D1ifH%ksY<-Vn0m$XFv&_Z)VHeqX#4qP*g|LF1$G=D* zs0!Jnw%=MEUP0g7+*<(3k*t8MS9MhHoPJmQ3d!FzpPB|UboY_3%RA*zG7HHpxzT3) zUMuP9{HTCipv_+;$tWJOHkr<3bSQQnI;M_&PQ;;~UADCkk^t;%{AD6Lh2(`Kpi*3m z)pmjB;lU3eZ;yhoL#^dNDC(W*!2H;lGq4VmfUtngqgyVQ%66>=$cYhvpA-7B?kojK zR?JS@SSS@Jc!gR0z?#)SW61(VH9O;NVexOlU+V9%!hI<+$zHsGY7VgyKE4B>_)i8y zD7yN7L`{P^B-xQxAzK)}SS!$PhC(=!x!TvBCF?4 zFc$Q#Ezm8(Z^$w$#p^?|>J?Zzd4Yv^1xx9v$-su}EG{oLKzd}uH2R`;PUcJ#6=kso z^2_=EUYY54Vl8$O0m(lh>sxqNDRHn--BVVnw;)3SXq5khKqV7CFXRa1+gg>ZPjlB%C<_DMwV7*GrXo$?r5Z^)?&@kD$m1LEy#0LgftL0w0 z$-9VQ6>rS~J*?l(1QpmP!(Nt~vMn*l15dywg)8CioRpbl1)f&+_?WVU?ji40d&@pA ze9Vk>#L1^)-Od2&O0S&-PFQETU0Il#Duei6fv@+aW36{i!p~amMZtp062{jwSl|JV z1s1<0u8)1S+?rj!Zsdla)a&wD$z0xgJ)*5Gk|MkOIp=bDW%-V)%3Wl_$#Jg{{GAZ} z)pV^aD0s-{Ui0s4cp-JT12JkP6GNV0XfrZ`0AgY`_&JJ@dNE|MhaLz8ty_K=!L>(- zl7-dA2Vi%5yDF?Zp*v7YrwJ4Co&(9J+2;vQlx>Sx?-5O!Q2vWdG9aX_3Osc;5g}Eu zJ|Td(ohITEB$iGjCY9d%ako7wblf?}e5-kjASRUg24FfOR=QTXx0E+mI&|v4+9(0we5P<#| z!aX0HUAs%NA{ibPVyR1^KDHbHgnPdS-W&aR3S`w`tSbcyCw0pj?vQ}qQ~)0SbZ3#P zw-uDY+t!h9k|)?K?|P(VVw=_h*CXU#6G2jlnLQ8AT_gIhZS-KTm?ko|b4!0Ah2DrR zdlU#t(h!>CKu-Vn-`gM#f-o)PaZ(gHFI%br_}(*JGfU#ZC);IB_1Hv2vr+%(22crk z?@$xD$v6^f;hF7mK|wUAkS7<`jF{*G(P38&pxIJ-jfexijVrsy*bD$jl?& zfMI)#5e)5JZfpIg5-TlzttKNEeAotu{w#>-bL|dui!DoKL&Kt)s;W#Bh?z|C{IUAd z(msG%V?6cb&_@fu3RDcUYC~y2v)izd?3&DH*cd@|CFD8Lq1LX z_&2ivex~6^YyfVXT0Fv%LXu*bXzArZ(Y^Oqcg3FMNY$AG!{n6iSAc(!|C`uLzCVl! zaqE63=M3uapm45}F1fu?+_@2?8*zD;H2F-#4x|N0y?6awHX~rp;61h1L;%@IH-JZa z{AIq0>LLKe)&3o$#P+qcdWMV+h?}B8<>7mfG$i(A!#*!$X$kzULrD&G5)V~Dxbn9l z@}9K`GUhk){MZTVep1>&4Hj8$;u&og&AS1Ir1SDKVFRCOMpq7tx0#^&Ny`BvcmcM@djg@+!nu07;GxF*kcrb9EGjY~JE8jR z$4G(`9ID|@F?4bda8@GtF{p$$QaWds2sUri4}KfMl2ZVfrpQUWbEpyA^0Yz#a)P+; zgP{q2=C7*?AVZZ7#!$9@L#=;F{Lq(P=HL^HJh9O#YU6&}qtE7$<>8B{snqchtk zgM9)a2O From 44de27f177486617a3c690a169dab820d00fdf62 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Mon, 31 Aug 2020 21:21:37 +0800 Subject: [PATCH 14/22] fix(github): fix 404 not found & init tests --- .mocharc.js | 1 + README.md | 2 +- packages/plugin-github/src/index.ts | 18 ++- .../tests/__snapshots__/index.spec.ts.snap | 141 ------------------ .../check_run.completed.json | 0 .../chect_suite.completed.json | 0 .../commit_comment.created.json | 0 .../issue_comment.created.1.json | 0 .../issue_comment.created.2.json | 0 .../issues.assigned.json | 0 .../issues.closed.json | 0 .../issues.labeled.json | 0 .../issues.opened.json | 0 .../pull_request.opened.json | 0 .../pull_request_review.submitted.1.json | 0 .../pull_request_review.submitted.2.json | 0 .../pull_request_review_comment.created.json | 0 .../push.commit.json | 0 .../push.delete.json | 0 .../{__fixtures__ => fixtures}/push.tag.json | 0 packages/plugin-github/tests/index.snap.js | 45 ++++++ packages/plugin-github/tests/index.spec.ts | 59 ++++++-- 22 files changed, 104 insertions(+), 162 deletions(-) delete mode 100644 packages/plugin-github/tests/__snapshots__/index.spec.ts.snap rename packages/plugin-github/tests/{__fixtures__ => fixtures}/check_run.completed.json (100%) rename packages/plugin-github/tests/{__fixtures__ => fixtures}/chect_suite.completed.json (100%) rename packages/plugin-github/tests/{__fixtures__ => fixtures}/commit_comment.created.json (100%) rename packages/plugin-github/tests/{__fixtures__ => fixtures}/issue_comment.created.1.json (100%) rename packages/plugin-github/tests/{__fixtures__ => fixtures}/issue_comment.created.2.json (100%) rename packages/plugin-github/tests/{__fixtures__ => fixtures}/issues.assigned.json (100%) rename packages/plugin-github/tests/{__fixtures__ => fixtures}/issues.closed.json (100%) rename packages/plugin-github/tests/{__fixtures__ => fixtures}/issues.labeled.json (100%) rename packages/plugin-github/tests/{__fixtures__ => fixtures}/issues.opened.json (100%) rename packages/plugin-github/tests/{__fixtures__ => fixtures}/pull_request.opened.json (100%) rename packages/plugin-github/tests/{__fixtures__ => fixtures}/pull_request_review.submitted.1.json (100%) rename packages/plugin-github/tests/{__fixtures__ => fixtures}/pull_request_review.submitted.2.json (100%) rename packages/plugin-github/tests/{__fixtures__ => fixtures}/pull_request_review_comment.created.json (100%) rename packages/plugin-github/tests/{__fixtures__ => fixtures}/push.commit.json (100%) rename packages/plugin-github/tests/{__fixtures__ => fixtures}/push.delete.json (100%) rename packages/plugin-github/tests/{__fixtures__ => fixtures}/push.tag.json (100%) create mode 100644 packages/plugin-github/tests/index.snap.js diff --git a/.mocharc.js b/.mocharc.js index f8674754a7..7ebf613555 100644 --- a/.mocharc.js +++ b/.mocharc.js @@ -6,6 +6,7 @@ module.exports = { 'packages/koishi-utils/tests/*.spec.ts', 'packages/koishi-test-utils/tests/*.spec.ts', 'packages/plugin-eval/tests/*.spec.ts', + 'packages/plugin-github/tests/*.spec.ts', 'packages/plugin-teach/tests/*.spec.ts', ], require: [ diff --git a/README.md b/README.md index 251888b66c..b89263c561 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ koishi run - [Mrs4s/go-cqhttp](https://github.com/Mrs4s/go-cqhttp) - [yyuueexxiinngg/cqhttp-mirai](https://github.com/yyuueexxiinngg/cqhttp-mirai) -请注意:尽管 Koishi 使用的协议是 MIT,但上面陈述的三种途径的相关框架都使用了基于 [AGPL 3.0](https://choosealicense.com/licenses/agpl-3.0/) 的协议。因此如果你使用 koishi-adapter-cqhttp 运行你的机器人,你将可能受到 AGPL 3.0 协议的限制,**必须将你的代码开源并保持同协议**。Koishi 及其作者对使用上述框架或违反上述限制所可能造成的法律后果不负任何责任。 +请注意:尽管 Koishi 使用的协议是 [MIT](https://choosealicense.com/licenses/mit/),但上面陈述的三种途径的相关框架都使用了基于 [AGPL 3.0](https://choosealicense.com/licenses/agpl-3.0/) 的协议。因此如果你使用 koishi-adapter-cqhttp 运行你的机器人,你将可能受到 AGPL 3.0 协议的限制,**必须将你的代码开源并保持同协议**。Koishi 及其作者对使用上述框架或违反上述限制的行为所可能造成的法律后果概不负责。 ## 数据库支持 diff --git a/packages/plugin-github/src/index.ts b/packages/plugin-github/src/index.ts index 0c354952dd..e4e98fc528 100644 --- a/packages/plugin-github/src/index.ts +++ b/packages/plugin-github/src/index.ts @@ -1,7 +1,7 @@ /* eslint-disable camelcase */ import { Context, Middleware, User } from 'koishi-core' -import { Logger, Time } from 'koishi-utils' +import { defineProperty, Logger, Time } from 'koishi-utils' import { Octokit } from '@octokit/rest' import { Webhooks, EventNames, EventPayloads } from '@octokit/webhooks' import { GetWebhookPayloadTypeFromEvent } from '@octokit/webhooks/dist-types/generated/get-webhook-payload-type-from-event' @@ -16,6 +16,12 @@ type Issue = EventPayloads.WebhookPayloadIssuesIssue | EventPayloads.WebhookPayloadPullRequestReviewPullRequest type ReviewComment = EventPayloads.WebhookPayloadPullRequestReviewCommentComment +declare module 'koishi-core/dist/app' { + interface App { + githubWebhooks?: Webhooks + } +} + declare module 'koishi-core/dist/database' { interface User { githubToken?: string @@ -62,7 +68,11 @@ export function apply(ctx: Context, config: Config = {}) { if (!ctx.router) throw new Error('ctx.router is not defined') config = { ...defaultOptions, ...config } - const webhook = new Webhooks(config) + const webhooks = new Webhooks({ + ...config, + path: config.webhook, + }) + defineProperty(ctx.app, 'githubWebhooks', webhooks) const github = new Octokit({ request: { agent: config.agent, @@ -73,7 +83,7 @@ export function apply(ctx: Context, config: Config = {}) { const { router, database } = ctx router.post(config.webhook, (ctx, next) => { - return webhook.middleware(ctx.req, ctx.res, next) + return webhooks.middleware(ctx.req, ctx.res, next) }) router.get(config.authorize, async (ctx) => { @@ -120,7 +130,7 @@ export function apply(ctx: Context, config: Config = {}) { }) function registerHandler(event: T, handler: (payload: Payload) => [string, Middleware?]) { - webhook.on(event, async (callback) => { + webhooks.on(event, async (callback) => { const { repository } = callback.payload const groupIds = config.repos[repository.full_name] if (!groupIds) return diff --git a/packages/plugin-github/tests/__snapshots__/index.spec.ts.snap b/packages/plugin-github/tests/__snapshots__/index.spec.ts.snap deleted file mode 100644 index 72e82b8f86..0000000000 --- a/packages/plugin-github/tests/__snapshots__/index.spec.ts.snap +++ /dev/null @@ -1,141 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`check_run.completed: check_run.completed 1`] = `undefined`; - -exports[`chect_suite.completed: chect_suite.completed 1`] = `undefined`; - -exports[`commit_comment.created: commit_comment.created 1`] = ` -Array [ - "send_group_msg", - Object { - "auto_escape": false, - "group_id": 123, - "message": "[GitHub] Commit Comment (koishijs/koishi) -User: Shigma -URL: https://github.com/koishijs/koishi/commit/bff469eabe14d42683a4f7c3ccb659daec5e1c00#commitcomment-36878220 -This will introduce a failure in test.", - }, -] -`; - -exports[`issue_comment.created.1: issue_comment.created.1 1`] = ` -Array [ - "send_group_msg", - Object { - "auto_escape": false, - "group_id": 123, - "message": "[GitHub] Issue Comment (koishijs/koishi#19) -User: simon300000 -URL: https://github.com/koishijs/koishi/issues/19#issuecomment-576277946 -Mich würde auch interessieren, was ist „CoolQ“?", - }, -] -`; - -exports[`issue_comment.created.2: issue_comment.created.2 1`] = ` -Array [ - "send_group_msg", - Object { - "auto_escape": false, - "group_id": 123, - "message": "[GitHub] Pull Request Comment (koishijs/koishi#20) -User: Kouchya -URL: https://github.com/koishijs/koishi/pull/20#issuecomment-576291300 -C'est important!", - }, -] -`; - -exports[`issues.assigned: issues.assigned 1`] = `undefined`; - -exports[`issues.closed: issues.closed 1`] = `undefined`; - -exports[`issues.labeled: issues.labeled 1`] = `undefined`; - -exports[`issues.opened: issues.opened 1`] = ` -Array [ - "send_group_msg", - Object { - "auto_escape": false, - "group_id": 123, - "message": "[GitHub] Issue Opened (koishijs/koishi#19) -Title: Wie kann man um das Koishi zu installieren? -User: simon300000 -URL: https://github.com/koishijs/koishi/issues/19 -Ich verstecke Englisch und Chinesisch nicht! Gab es Personen, die mir helfen kann?", - }, -] -`; - -exports[`pull_request.opened: pull_request.opened 1`] = ` -Array [ - "send_group_msg", - Object { - "auto_escape": false, - "group_id": 123, - "message": "[GitHub] Pull Request Opened (koishijs/koishi#364852707) -koishijs:develop <- koishijs:experimental -User: simon300000 -URL: https://github.com/koishijs/koishi/pull/20 -Das ist wichtig!", - }, -] -`; - -exports[`pull_request_review.submitted.1: pull_request_review.submitted.1 1`] = `undefined`; - -exports[`pull_request_review.submitted.2: pull_request_review.submitted.2 1`] = ` -Array [ - "send_group_msg", - Object { - "auto_escape": false, - "group_id": 123, - "message": "[GitHub] Pull Request Review (koishijs/koishi#364852707) -User: Kouchya -URL: https://github.com/koishijs/koishi/pull/20#pullrequestreview-345349537 -LGTM", - }, -] -`; - -exports[`pull_request_review_comment.created: pull_request_review_comment.created 1`] = ` -Array [ - "send_group_msg", - Object { - "auto_escape": false, - "group_id": 123, - "message": "[GitHub] Pull Request Review Comment (koishijs/koishi#364852707) -Path: packages/test-utils/src/mocks.ts -User: Shigma -URL: https://github.com/koishijs/koishi/pull/20#discussion_r368570320 -Naming is so hard......", - }, -] -`; - -exports[`push.commit: push.commit 1`] = ` -Array [ - "send_group_msg", - Object { - "auto_escape": false, - "group_id": 123, - "message": "[GitHub] Push (koishijs/koishi) -Ref: refs/heads/develop -User: Shigma -Compare: https://github.com/koishijs/koishi/compare/976c6e8f09a4...3ae7e7044d06 -chore: adjust -fix(core): create major context at demand", - }, -] -`; - -exports[`push.tag: push.tag 1`] = ` -Array [ - "send_group_msg", - Object { - "auto_escape": false, - "group_id": 123, - "message": "[GitHub] koishijs/koishi published tag 1.5.0", - }, -] -`; diff --git a/packages/plugin-github/tests/__fixtures__/check_run.completed.json b/packages/plugin-github/tests/fixtures/check_run.completed.json similarity index 100% rename from packages/plugin-github/tests/__fixtures__/check_run.completed.json rename to packages/plugin-github/tests/fixtures/check_run.completed.json diff --git a/packages/plugin-github/tests/__fixtures__/chect_suite.completed.json b/packages/plugin-github/tests/fixtures/chect_suite.completed.json similarity index 100% rename from packages/plugin-github/tests/__fixtures__/chect_suite.completed.json rename to packages/plugin-github/tests/fixtures/chect_suite.completed.json diff --git a/packages/plugin-github/tests/__fixtures__/commit_comment.created.json b/packages/plugin-github/tests/fixtures/commit_comment.created.json similarity index 100% rename from packages/plugin-github/tests/__fixtures__/commit_comment.created.json rename to packages/plugin-github/tests/fixtures/commit_comment.created.json diff --git a/packages/plugin-github/tests/__fixtures__/issue_comment.created.1.json b/packages/plugin-github/tests/fixtures/issue_comment.created.1.json similarity index 100% rename from packages/plugin-github/tests/__fixtures__/issue_comment.created.1.json rename to packages/plugin-github/tests/fixtures/issue_comment.created.1.json diff --git a/packages/plugin-github/tests/__fixtures__/issue_comment.created.2.json b/packages/plugin-github/tests/fixtures/issue_comment.created.2.json similarity index 100% rename from packages/plugin-github/tests/__fixtures__/issue_comment.created.2.json rename to packages/plugin-github/tests/fixtures/issue_comment.created.2.json diff --git a/packages/plugin-github/tests/__fixtures__/issues.assigned.json b/packages/plugin-github/tests/fixtures/issues.assigned.json similarity index 100% rename from packages/plugin-github/tests/__fixtures__/issues.assigned.json rename to packages/plugin-github/tests/fixtures/issues.assigned.json diff --git a/packages/plugin-github/tests/__fixtures__/issues.closed.json b/packages/plugin-github/tests/fixtures/issues.closed.json similarity index 100% rename from packages/plugin-github/tests/__fixtures__/issues.closed.json rename to packages/plugin-github/tests/fixtures/issues.closed.json diff --git a/packages/plugin-github/tests/__fixtures__/issues.labeled.json b/packages/plugin-github/tests/fixtures/issues.labeled.json similarity index 100% rename from packages/plugin-github/tests/__fixtures__/issues.labeled.json rename to packages/plugin-github/tests/fixtures/issues.labeled.json diff --git a/packages/plugin-github/tests/__fixtures__/issues.opened.json b/packages/plugin-github/tests/fixtures/issues.opened.json similarity index 100% rename from packages/plugin-github/tests/__fixtures__/issues.opened.json rename to packages/plugin-github/tests/fixtures/issues.opened.json diff --git a/packages/plugin-github/tests/__fixtures__/pull_request.opened.json b/packages/plugin-github/tests/fixtures/pull_request.opened.json similarity index 100% rename from packages/plugin-github/tests/__fixtures__/pull_request.opened.json rename to packages/plugin-github/tests/fixtures/pull_request.opened.json diff --git a/packages/plugin-github/tests/__fixtures__/pull_request_review.submitted.1.json b/packages/plugin-github/tests/fixtures/pull_request_review.submitted.1.json similarity index 100% rename from packages/plugin-github/tests/__fixtures__/pull_request_review.submitted.1.json rename to packages/plugin-github/tests/fixtures/pull_request_review.submitted.1.json diff --git a/packages/plugin-github/tests/__fixtures__/pull_request_review.submitted.2.json b/packages/plugin-github/tests/fixtures/pull_request_review.submitted.2.json similarity index 100% rename from packages/plugin-github/tests/__fixtures__/pull_request_review.submitted.2.json rename to packages/plugin-github/tests/fixtures/pull_request_review.submitted.2.json diff --git a/packages/plugin-github/tests/__fixtures__/pull_request_review_comment.created.json b/packages/plugin-github/tests/fixtures/pull_request_review_comment.created.json similarity index 100% rename from packages/plugin-github/tests/__fixtures__/pull_request_review_comment.created.json rename to packages/plugin-github/tests/fixtures/pull_request_review_comment.created.json diff --git a/packages/plugin-github/tests/__fixtures__/push.commit.json b/packages/plugin-github/tests/fixtures/push.commit.json similarity index 100% rename from packages/plugin-github/tests/__fixtures__/push.commit.json rename to packages/plugin-github/tests/fixtures/push.commit.json diff --git a/packages/plugin-github/tests/__fixtures__/push.delete.json b/packages/plugin-github/tests/fixtures/push.delete.json similarity index 100% rename from packages/plugin-github/tests/__fixtures__/push.delete.json rename to packages/plugin-github/tests/fixtures/push.delete.json diff --git a/packages/plugin-github/tests/__fixtures__/push.tag.json b/packages/plugin-github/tests/fixtures/push.tag.json similarity index 100% rename from packages/plugin-github/tests/__fixtures__/push.tag.json rename to packages/plugin-github/tests/fixtures/push.tag.json diff --git a/packages/plugin-github/tests/index.snap.js b/packages/plugin-github/tests/index.snap.js new file mode 100644 index 0000000000..294333cd36 --- /dev/null +++ b/packages/plugin-github/tests/index.snap.js @@ -0,0 +1,45 @@ +module.exports[`commit_comment.created`] = ` +[GitHub] Shigma commented on commit koishijs/koishi@bff469 +URL: https://github.com/koishijs/koishi/commit/bff469eabe14d42683a4f7c3ccb659daec5e1c00#commitcomment-36878220 +This will introduce a failure in test.` + +module.exports[`issue_comment.created.1`] = ` +[GitHub] simon300000 commented on issue koishijs/koishi#19 +URL: https://github.com/koishijs/koishi/issues/19#issuecomment-576277946 +Mich würde auch interessieren, was ist „CoolQ“?` + +module.exports[`issue_comment.created.2`] = ` +[GitHub] Kouchya commented on pull request koishijs/koishi#20 +URL: https://github.com/koishijs/koishi/pull/20#issuecomment-576291300 +C'est important!` + +module.exports[`issues.opened`] = ` +[GitHub] simon300000 opened an issue koishijs/koishi#19 +URL: https://github.com/koishijs/koishi/issues/19 +Title: Wie kann man um das Koishi zu installieren? +Ich verstecke Englisch und Chinesisch nicht! Gab es Personen, die mir helfen kann?` + +module.exports[`pull_request.opened`] = ` +[GitHub] simon300000 opened a pull request koishijs/koishi#20 (koishijs:develop <- koishijs:experimental) +URL: https://github.com/koishijs/koishi/pull/20 +Das ist wichtig!` + +module.exports[`pull_request_review.submitted.2`] = ` +[GitHub] Kouchya reviewed pull request koishijs/koishi#20 +URL: https://github.com/koishijs/koishi/pull/20#pullrequestreview-345349537 +LGTM` + +module.exports[`pull_request_review_comment.created`] = ` +[GitHub] Shigma commented on pull request review koishijs/koishi#20 +Path: packages/test-utils/src/mocks.ts +URL: https://github.com/koishijs/koishi/pull/20#discussion_r368570320 +Naming is so hard......` + +module.exports[`push.commit`] = ` +[GitHub] Shigma pushed to koishijs/koishi:refs/heads/develop +Compare: https://github.com/koishijs/koishi/compare/976c6e8f09a4...3ae7e7044d06 +[d7ff34] chore: adjust +[3ae7e7] fix(core): create major context at demand` + +module.exports[`push.tag`] = ` +[GitHub] Shigma published tag koishijs/koishi@1.5.0` diff --git a/packages/plugin-github/tests/index.spec.ts b/packages/plugin-github/tests/index.spec.ts index c1ed455eb4..27a376a161 100644 --- a/packages/plugin-github/tests/index.spec.ts +++ b/packages/plugin-github/tests/index.spec.ts @@ -1,27 +1,54 @@ -import { MockedApp } from 'koishi-test-utils' -import { apply, webhooks } from '../src' +import { App, BASE_SELF_ID, memory } from 'koishi-test-utils' +import { Random } from 'koishi-utils' +import { fn, spyOn } from 'jest-mock' +import { expect } from 'chai' import { readdirSync } from 'fs-extra' import { resolve } from 'path' +import * as github from 'koishi-plugin-github' -const app = new MockedApp({ - githubWebhook: { - secret: 'secret', - }, +const secret = Random.uuid() + +const app = new App({ port: 10000 }) + +app.plugin(memory) + +app.plugin(github, { + secret, + repos: { 'koishijs/koishi': [123] }, }) -app.plugin(apply, { - 'koishijs/koishi': [123], +// override listen +const listen = spyOn(app.server, 'listen') +listen.mockReturnValue(Promise.resolve()) + +// spy on sendGroupMsg +const sendGroupMsg = app.bots[0].sendGroupMsg = fn() + +before(async () => { + await app.start() + await app.database.getGroup(123, BASE_SELF_ID) }) -const webhook = webhooks['/secret12140'] +const snapshot = require('./index.snap') -readdirSync(resolve(__dirname, '__fixtures__')).forEach((file) => { - file = file.slice(0, -5) - const [name] = file.split('.', 1) - const payload = require(`./__fixtures__/${file}`) +function check(file: string) { + it(file, async () => { + sendGroupMsg.mockClear() + const payload = require(`./fixtures/${file}`) + const [name] = file.split('.', 1) + await app.githubWebhooks.receive({ id: Random.uuid(), name, payload }) + if (snapshot[file]) { + expect(sendGroupMsg.mock.calls).to.have.length(1) + expect(sendGroupMsg.mock.calls[0][1]).to.equal(snapshot[file].trim()) + } else { + expect(sendGroupMsg.mock.calls).to.have.length(0) + } + }) +} - test(file, async () => { - await webhook.receive({ id: 'id', name, payload }) - app.shouldMatchSnapshot(file) +describe('koishi-plugin-github', () => { + const files = readdirSync(resolve(__dirname, 'fixtures')) + files.forEach(file => { + check(file.slice(0, -5)) }) }) From 601987767283422a4764054d2bc976d139a09979 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Mon, 31 Aug 2020 21:33:54 +0800 Subject: [PATCH 15/22] fix(github): fix 202 still processing workaround @octokit/webhooks for koa --- packages/plugin-github/src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/plugin-github/src/index.ts b/packages/plugin-github/src/index.ts index e4e98fc528..f501c22d38 100644 --- a/packages/plugin-github/src/index.ts +++ b/packages/plugin-github/src/index.ts @@ -83,6 +83,8 @@ export function apply(ctx: Context, config: Config = {}) { const { router, database } = ctx router.post(config.webhook, (ctx, next) => { + // workaround @octokit/webhooks for koa + ctx.req['body'] = ctx.request.body return webhooks.middleware(ctx.req, ctx.res, next) }) From 64e5dcf3ff907a6b9612f4205d28f0494d51b6f0 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Mon, 31 Aug 2020 21:57:33 +0800 Subject: [PATCH 16/22] feat(github): optimize push ref display --- packages/plugin-github/src/index.ts | 9 +- ...opened.json => pull_request.opened.1.json} | 0 .../tests/fixtures/pull_request.opened.2.json | 495 ++++++++++++++++++ packages/plugin-github/tests/index.snap.js | 9 +- 4 files changed, 507 insertions(+), 6 deletions(-) rename packages/plugin-github/tests/fixtures/{pull_request.opened.json => pull_request.opened.1.json} (100%) create mode 100644 packages/plugin-github/tests/fixtures/pull_request.opened.2.json diff --git a/packages/plugin-github/src/index.ts b/packages/plugin-github/src/index.ts index f501c22d38..2bbfe9aded 100644 --- a/packages/plugin-github/src/index.ts +++ b/packages/plugin-github/src/index.ts @@ -232,10 +232,11 @@ export function apply(ctx: Context, config: Config = {}) { }) registerHandler('pull_request.opened', ({ repository, pull_request }) => { + const { user, html_url, base, head, body, number } = pull_request return [[ - `[GitHub] ${pull_request.user.login} opened a pull request ${repository.full_name}#${pull_request.number} (${pull_request.base.label} <- ${pull_request.head.label})`, - `URL: ${pull_request.html_url}`, - formatMarkdown(pull_request.body), + `[GitHub] ${user.login} opened a pull request ${repository.full_name}#${number} (${base.label} <- ${head.label})`, + `URL: ${html_url}`, + formatMarkdown(body), ].join('\n'), createIssueComment(repository, pull_request)] }) @@ -268,7 +269,7 @@ export function apply(ctx: Context, config: Config = {}) { } return [[ - `[GitHub] ${pusher.name} pushed to ${repository.full_name}:${ref}`, + `[GitHub] ${pusher.name} pushed to ${repository.full_name}:${ref.replace(/^refs\/heads\//, '')}`, `Compare: ${compare}`, ...commits.map(c => `[${c.id.slice(0, 6)}] ${formatMarkdown(c.message)}`), ].join('\n')] diff --git a/packages/plugin-github/tests/fixtures/pull_request.opened.json b/packages/plugin-github/tests/fixtures/pull_request.opened.1.json similarity index 100% rename from packages/plugin-github/tests/fixtures/pull_request.opened.json rename to packages/plugin-github/tests/fixtures/pull_request.opened.1.json diff --git a/packages/plugin-github/tests/fixtures/pull_request.opened.2.json b/packages/plugin-github/tests/fixtures/pull_request.opened.2.json new file mode 100644 index 0000000000..3ca357be35 --- /dev/null +++ b/packages/plugin-github/tests/fixtures/pull_request.opened.2.json @@ -0,0 +1,495 @@ +{ + "action": "opened", + "number": 90, + "pull_request": { + "url": "https://api.github.com/repos/koishijs/koishi/pulls/90", + "id": 472904293, + "node_id": "MDExOlB1bGxSZXF1ZXN0NDcyOTA0Mjkz", + "html_url": "https://github.com/koishijs/koishi/pull/90", + "diff_url": "https://github.com/koishijs/koishi/pull/90.diff", + "patch_url": "https://github.com/koishijs/koishi/pull/90.patch", + "issue_url": "https://api.github.com/repos/koishijs/koishi/issues/90", + "number": 90, + "state": "open", + "locked": false, + "title": "test: fix path mapping", + "user": { + "login": "jjyyxx", + "id": 13808089, + "node_id": "MDQ6VXNlcjEzODA4MDg5", + "avatar_url": "https://avatars3.githubusercontent.com/u/13808089?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/jjyyxx", + "html_url": "https://github.com/jjyyxx", + "followers_url": "https://api.github.com/users/jjyyxx/followers", + "following_url": "https://api.github.com/users/jjyyxx/following{/other_user}", + "gists_url": "https://api.github.com/users/jjyyxx/gists{/gist_id}", + "starred_url": "https://api.github.com/users/jjyyxx/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/jjyyxx/subscriptions", + "organizations_url": "https://api.github.com/users/jjyyxx/orgs", + "repos_url": "https://api.github.com/users/jjyyxx/repos", + "events_url": "https://api.github.com/users/jjyyxx/events{/privacy}", + "received_events_url": "https://api.github.com/users/jjyyxx/received_events", + "type": "User", + "site_admin": false + }, + "body": "According to `tsconfig-paths` documentation ...", + "created_at": "2020-08-25T02:57:41Z", + "updated_at": "2020-08-25T02:57:41Z", + "closed_at": null, + "merged_at": null, + "merge_commit_sha": null, + "assignee": null, + "assignees": [ + + ], + "requested_reviewers": [ + + ], + "requested_teams": [ + + ], + "labels": [ + + ], + "milestone": null, + "draft": false, + "commits_url": "https://api.github.com/repos/koishijs/koishi/pulls/90/commits", + "review_comments_url": "https://api.github.com/repos/koishijs/koishi/pulls/90/comments", + "review_comment_url": "https://api.github.com/repos/koishijs/koishi/pulls/comments{/number}", + "comments_url": "https://api.github.com/repos/koishijs/koishi/issues/90/comments", + "statuses_url": "https://api.github.com/repos/koishijs/koishi/statuses/b1ea76d2dfc43e4ba5852b52e1eeb03beeb36541", + "head": { + "label": "jjyyxx:mapping", + "ref": "mapping", + "sha": "b1ea76d2dfc43e4ba5852b52e1eeb03beeb36541", + "user": { + "login": "jjyyxx", + "id": 13808089, + "node_id": "MDQ6VXNlcjEzODA4MDg5", + "avatar_url": "https://avatars3.githubusercontent.com/u/13808089?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/jjyyxx", + "html_url": "https://github.com/jjyyxx", + "followers_url": "https://api.github.com/users/jjyyxx/followers", + "following_url": "https://api.github.com/users/jjyyxx/following{/other_user}", + "gists_url": "https://api.github.com/users/jjyyxx/gists{/gist_id}", + "starred_url": "https://api.github.com/users/jjyyxx/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/jjyyxx/subscriptions", + "organizations_url": "https://api.github.com/users/jjyyxx/orgs", + "repos_url": "https://api.github.com/users/jjyyxx/repos", + "events_url": "https://api.github.com/users/jjyyxx/events{/privacy}", + "received_events_url": "https://api.github.com/users/jjyyxx/received_events", + "type": "User", + "site_admin": false + }, + "repo": { + "id": 290096040, + "node_id": "MDEwOlJlcG9zaXRvcnkyOTAwOTYwNDA=", + "name": "koishi", + "full_name": "jjyyxx/koishi", + "private": false, + "owner": { + "login": "jjyyxx", + "id": 13808089, + "node_id": "MDQ6VXNlcjEzODA4MDg5", + "avatar_url": "https://avatars3.githubusercontent.com/u/13808089?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/jjyyxx", + "html_url": "https://github.com/jjyyxx", + "followers_url": "https://api.github.com/users/jjyyxx/followers", + "following_url": "https://api.github.com/users/jjyyxx/following{/other_user}", + "gists_url": "https://api.github.com/users/jjyyxx/gists{/gist_id}", + "starred_url": "https://api.github.com/users/jjyyxx/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/jjyyxx/subscriptions", + "organizations_url": "https://api.github.com/users/jjyyxx/orgs", + "repos_url": "https://api.github.com/users/jjyyxx/repos", + "events_url": "https://api.github.com/users/jjyyxx/events{/privacy}", + "received_events_url": "https://api.github.com/users/jjyyxx/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/jjyyxx/koishi", + "description": "QQ bot out of the box", + "fork": true, + "url": "https://api.github.com/repos/jjyyxx/koishi", + "forks_url": "https://api.github.com/repos/jjyyxx/koishi/forks", + "keys_url": "https://api.github.com/repos/jjyyxx/koishi/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/jjyyxx/koishi/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/jjyyxx/koishi/teams", + "hooks_url": "https://api.github.com/repos/jjyyxx/koishi/hooks", + "issue_events_url": "https://api.github.com/repos/jjyyxx/koishi/issues/events{/number}", + "events_url": "https://api.github.com/repos/jjyyxx/koishi/events", + "assignees_url": "https://api.github.com/repos/jjyyxx/koishi/assignees{/user}", + "branches_url": "https://api.github.com/repos/jjyyxx/koishi/branches{/branch}", + "tags_url": "https://api.github.com/repos/jjyyxx/koishi/tags", + "blobs_url": "https://api.github.com/repos/jjyyxx/koishi/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/jjyyxx/koishi/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/jjyyxx/koishi/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/jjyyxx/koishi/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/jjyyxx/koishi/statuses/{sha}", + "languages_url": "https://api.github.com/repos/jjyyxx/koishi/languages", + "stargazers_url": "https://api.github.com/repos/jjyyxx/koishi/stargazers", + "contributors_url": "https://api.github.com/repos/jjyyxx/koishi/contributors", + "subscribers_url": "https://api.github.com/repos/jjyyxx/koishi/subscribers", + "subscription_url": "https://api.github.com/repos/jjyyxx/koishi/subscription", + "commits_url": "https://api.github.com/repos/jjyyxx/koishi/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/jjyyxx/koishi/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/jjyyxx/koishi/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/jjyyxx/koishi/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/jjyyxx/koishi/contents/{+path}", + "compare_url": "https://api.github.com/repos/jjyyxx/koishi/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/jjyyxx/koishi/merges", + "archive_url": "https://api.github.com/repos/jjyyxx/koishi/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/jjyyxx/koishi/downloads", + "issues_url": "https://api.github.com/repos/jjyyxx/koishi/issues{/number}", + "pulls_url": "https://api.github.com/repos/jjyyxx/koishi/pulls{/number}", + "milestones_url": "https://api.github.com/repos/jjyyxx/koishi/milestones{/number}", + "notifications_url": "https://api.github.com/repos/jjyyxx/koishi/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/jjyyxx/koishi/labels{/name}", + "releases_url": "https://api.github.com/repos/jjyyxx/koishi/releases{/id}", + "deployments_url": "https://api.github.com/repos/jjyyxx/koishi/deployments", + "created_at": "2020-08-25T02:51:14Z", + "updated_at": "2020-08-25T02:51:16Z", + "pushed_at": "2020-08-25T02:52:54Z", + "git_url": "git://github.com/jjyyxx/koishi.git", + "ssh_url": "git@github.com:jjyyxx/koishi.git", + "clone_url": "https://github.com/jjyyxx/koishi.git", + "svn_url": "https://github.com/jjyyxx/koishi", + "homepage": "https://koishi.js.org", + "size": 2438, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": false, + "has_projects": true, + "has_downloads": true, + "has_wiki": false, + "has_pages": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 0, + "license": { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZTEz" + }, + "forks": 0, + "open_issues": 0, + "watchers": 0, + "default_branch": "master", + "allow_squash_merge": true, + "allow_merge_commit": true, + "allow_rebase_merge": true, + "delete_branch_on_merge": false + } + }, + "base": { + "label": "koishijs:mapping", + "ref": "mapping", + "sha": "d951031c5cc53b9705ba20d23a528a8109e17332", + "user": { + "login": "koishijs", + "id": 58179220, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjU4MTc5MjIw", + "avatar_url": "https://avatars3.githubusercontent.com/u/58179220?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/koishijs", + "html_url": "https://github.com/koishijs", + "followers_url": "https://api.github.com/users/koishijs/followers", + "following_url": "https://api.github.com/users/koishijs/following{/other_user}", + "gists_url": "https://api.github.com/users/koishijs/gists{/gist_id}", + "starred_url": "https://api.github.com/users/koishijs/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/koishijs/subscriptions", + "organizations_url": "https://api.github.com/users/koishijs/orgs", + "repos_url": "https://api.github.com/users/koishijs/repos", + "events_url": "https://api.github.com/users/koishijs/events{/privacy}", + "received_events_url": "https://api.github.com/users/koishijs/received_events", + "type": "Organization", + "site_admin": false + }, + "repo": { + "id": 225572038, + "node_id": "MDEwOlJlcG9zaXRvcnkyMjU1NzIwMzg=", + "name": "koishi", + "full_name": "koishijs/koishi", + "private": false, + "owner": { + "login": "koishijs", + "id": 58179220, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjU4MTc5MjIw", + "avatar_url": "https://avatars3.githubusercontent.com/u/58179220?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/koishijs", + "html_url": "https://github.com/koishijs", + "followers_url": "https://api.github.com/users/koishijs/followers", + "following_url": "https://api.github.com/users/koishijs/following{/other_user}", + "gists_url": "https://api.github.com/users/koishijs/gists{/gist_id}", + "starred_url": "https://api.github.com/users/koishijs/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/koishijs/subscriptions", + "organizations_url": "https://api.github.com/users/koishijs/orgs", + "repos_url": "https://api.github.com/users/koishijs/repos", + "events_url": "https://api.github.com/users/koishijs/events{/privacy}", + "received_events_url": "https://api.github.com/users/koishijs/received_events", + "type": "Organization", + "site_admin": false + }, + "html_url": "https://github.com/koishijs/koishi", + "description": "QQ bot out of the box", + "fork": false, + "url": "https://api.github.com/repos/koishijs/koishi", + "forks_url": "https://api.github.com/repos/koishijs/koishi/forks", + "keys_url": "https://api.github.com/repos/koishijs/koishi/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/koishijs/koishi/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/koishijs/koishi/teams", + "hooks_url": "https://api.github.com/repos/koishijs/koishi/hooks", + "issue_events_url": "https://api.github.com/repos/koishijs/koishi/issues/events{/number}", + "events_url": "https://api.github.com/repos/koishijs/koishi/events", + "assignees_url": "https://api.github.com/repos/koishijs/koishi/assignees{/user}", + "branches_url": "https://api.github.com/repos/koishijs/koishi/branches{/branch}", + "tags_url": "https://api.github.com/repos/koishijs/koishi/tags", + "blobs_url": "https://api.github.com/repos/koishijs/koishi/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/koishijs/koishi/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/koishijs/koishi/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/koishijs/koishi/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/koishijs/koishi/statuses/{sha}", + "languages_url": "https://api.github.com/repos/koishijs/koishi/languages", + "stargazers_url": "https://api.github.com/repos/koishijs/koishi/stargazers", + "contributors_url": "https://api.github.com/repos/koishijs/koishi/contributors", + "subscribers_url": "https://api.github.com/repos/koishijs/koishi/subscribers", + "subscription_url": "https://api.github.com/repos/koishijs/koishi/subscription", + "commits_url": "https://api.github.com/repos/koishijs/koishi/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/koishijs/koishi/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/koishijs/koishi/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/koishijs/koishi/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/koishijs/koishi/contents/{+path}", + "compare_url": "https://api.github.com/repos/koishijs/koishi/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/koishijs/koishi/merges", + "archive_url": "https://api.github.com/repos/koishijs/koishi/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/koishijs/koishi/downloads", + "issues_url": "https://api.github.com/repos/koishijs/koishi/issues{/number}", + "pulls_url": "https://api.github.com/repos/koishijs/koishi/pulls{/number}", + "milestones_url": "https://api.github.com/repos/koishijs/koishi/milestones{/number}", + "notifications_url": "https://api.github.com/repos/koishijs/koishi/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/koishijs/koishi/labels{/name}", + "releases_url": "https://api.github.com/repos/koishijs/koishi/releases{/id}", + "deployments_url": "https://api.github.com/repos/koishijs/koishi/deployments", + "created_at": "2019-12-03T08:47:29Z", + "updated_at": "2020-08-24T19:15:52Z", + "pushed_at": "2020-08-24T18:51:18Z", + "git_url": "git://github.com/koishijs/koishi.git", + "ssh_url": "git@github.com:koishijs/koishi.git", + "clone_url": "https://github.com/koishijs/koishi.git", + "svn_url": "https://github.com/koishijs/koishi", + "homepage": "https://koishi.js.org", + "size": 2438, + "stargazers_count": 186, + "watchers_count": 186, + "language": "TypeScript", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": false, + "has_pages": false, + "forks_count": 12, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 3, + "license": { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZTEz" + }, + "forks": 12, + "open_issues": 3, + "watchers": 186, + "default_branch": "master", + "allow_squash_merge": true, + "allow_merge_commit": true, + "allow_rebase_merge": true, + "delete_branch_on_merge": false + } + }, + "_links": { + "self": { + "href": "https://api.github.com/repos/koishijs/koishi/pulls/90" + }, + "html": { + "href": "https://github.com/koishijs/koishi/pull/90" + }, + "issue": { + "href": "https://api.github.com/repos/koishijs/koishi/issues/90" + }, + "comments": { + "href": "https://api.github.com/repos/koishijs/koishi/issues/90/comments" + }, + "review_comments": { + "href": "https://api.github.com/repos/koishijs/koishi/pulls/90/comments" + }, + "review_comment": { + "href": "https://api.github.com/repos/koishijs/koishi/pulls/comments{/number}" + }, + "commits": { + "href": "https://api.github.com/repos/koishijs/koishi/pulls/90/commits" + }, + "statuses": { + "href": "https://api.github.com/repos/koishijs/koishi/statuses/b1ea76d2dfc43e4ba5852b52e1eeb03beeb36541" + } + }, + "author_association": "NONE", + "active_lock_reason": null, + "merged": false, + "mergeable": null, + "rebaseable": null, + "mergeable_state": "unknown", + "merged_by": null, + "comments": 0, + "review_comments": 0, + "maintainer_can_modify": true, + "commits": 1, + "additions": 6, + "deletions": 1, + "changed_files": 3 + }, + "repository": { + "id": 225572038, + "node_id": "MDEwOlJlcG9zaXRvcnkyMjU1NzIwMzg=", + "name": "koishi", + "full_name": "koishijs/koishi", + "private": false, + "owner": { + "login": "koishijs", + "id": 58179220, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjU4MTc5MjIw", + "avatar_url": "https://avatars3.githubusercontent.com/u/58179220?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/koishijs", + "html_url": "https://github.com/koishijs", + "followers_url": "https://api.github.com/users/koishijs/followers", + "following_url": "https://api.github.com/users/koishijs/following{/other_user}", + "gists_url": "https://api.github.com/users/koishijs/gists{/gist_id}", + "starred_url": "https://api.github.com/users/koishijs/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/koishijs/subscriptions", + "organizations_url": "https://api.github.com/users/koishijs/orgs", + "repos_url": "https://api.github.com/users/koishijs/repos", + "events_url": "https://api.github.com/users/koishijs/events{/privacy}", + "received_events_url": "https://api.github.com/users/koishijs/received_events", + "type": "Organization", + "site_admin": false + }, + "html_url": "https://github.com/koishijs/koishi", + "description": "QQ bot out of the box", + "fork": false, + "url": "https://api.github.com/repos/koishijs/koishi", + "forks_url": "https://api.github.com/repos/koishijs/koishi/forks", + "keys_url": "https://api.github.com/repos/koishijs/koishi/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/koishijs/koishi/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/koishijs/koishi/teams", + "hooks_url": "https://api.github.com/repos/koishijs/koishi/hooks", + "issue_events_url": "https://api.github.com/repos/koishijs/koishi/issues/events{/number}", + "events_url": "https://api.github.com/repos/koishijs/koishi/events", + "assignees_url": "https://api.github.com/repos/koishijs/koishi/assignees{/user}", + "branches_url": "https://api.github.com/repos/koishijs/koishi/branches{/branch}", + "tags_url": "https://api.github.com/repos/koishijs/koishi/tags", + "blobs_url": "https://api.github.com/repos/koishijs/koishi/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/koishijs/koishi/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/koishijs/koishi/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/koishijs/koishi/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/koishijs/koishi/statuses/{sha}", + "languages_url": "https://api.github.com/repos/koishijs/koishi/languages", + "stargazers_url": "https://api.github.com/repos/koishijs/koishi/stargazers", + "contributors_url": "https://api.github.com/repos/koishijs/koishi/contributors", + "subscribers_url": "https://api.github.com/repos/koishijs/koishi/subscribers", + "subscription_url": "https://api.github.com/repos/koishijs/koishi/subscription", + "commits_url": "https://api.github.com/repos/koishijs/koishi/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/koishijs/koishi/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/koishijs/koishi/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/koishijs/koishi/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/koishijs/koishi/contents/{+path}", + "compare_url": "https://api.github.com/repos/koishijs/koishi/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/koishijs/koishi/merges", + "archive_url": "https://api.github.com/repos/koishijs/koishi/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/koishijs/koishi/downloads", + "issues_url": "https://api.github.com/repos/koishijs/koishi/issues{/number}", + "pulls_url": "https://api.github.com/repos/koishijs/koishi/pulls{/number}", + "milestones_url": "https://api.github.com/repos/koishijs/koishi/milestones{/number}", + "notifications_url": "https://api.github.com/repos/koishijs/koishi/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/koishijs/koishi/labels{/name}", + "releases_url": "https://api.github.com/repos/koishijs/koishi/releases{/id}", + "deployments_url": "https://api.github.com/repos/koishijs/koishi/deployments", + "created_at": "2019-12-03T08:47:29Z", + "updated_at": "2020-08-24T19:15:52Z", + "pushed_at": "2020-08-24T18:51:18Z", + "git_url": "git://github.com/koishijs/koishi.git", + "ssh_url": "git@github.com:koishijs/koishi.git", + "clone_url": "https://github.com/koishijs/koishi.git", + "svn_url": "https://github.com/koishijs/koishi", + "homepage": "https://koishi.js.org", + "size": 2438, + "stargazers_count": 186, + "watchers_count": 186, + "language": "TypeScript", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": false, + "has_pages": false, + "forks_count": 12, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 3, + "license": { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZTEz" + }, + "forks": 12, + "open_issues": 3, + "watchers": 186, + "default_branch": "master" + }, + "organization": { + "login": "koishijs", + "id": 58179220, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjU4MTc5MjIw", + "url": "https://api.github.com/orgs/koishijs", + "repos_url": "https://api.github.com/orgs/koishijs/repos", + "events_url": "https://api.github.com/orgs/koishijs/events", + "hooks_url": "https://api.github.com/orgs/koishijs/hooks", + "issues_url": "https://api.github.com/orgs/koishijs/issues", + "members_url": "https://api.github.com/orgs/koishijs/members{/member}", + "public_members_url": "https://api.github.com/orgs/koishijs/public_members{/member}", + "avatar_url": "https://avatars3.githubusercontent.com/u/58179220?v=4", + "description": "" + }, + "sender": { + "login": "jjyyxx", + "id": 13808089, + "node_id": "MDQ6VXNlcjEzODA4MDg5", + "avatar_url": "https://avatars3.githubusercontent.com/u/13808089?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/jjyyxx", + "html_url": "https://github.com/jjyyxx", + "followers_url": "https://api.github.com/users/jjyyxx/followers", + "following_url": "https://api.github.com/users/jjyyxx/following{/other_user}", + "gists_url": "https://api.github.com/users/jjyyxx/gists{/gist_id}", + "starred_url": "https://api.github.com/users/jjyyxx/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/jjyyxx/subscriptions", + "organizations_url": "https://api.github.com/users/jjyyxx/orgs", + "repos_url": "https://api.github.com/users/jjyyxx/repos", + "events_url": "https://api.github.com/users/jjyyxx/events{/privacy}", + "received_events_url": "https://api.github.com/users/jjyyxx/received_events", + "type": "User", + "site_admin": false + } +} \ No newline at end of file diff --git a/packages/plugin-github/tests/index.snap.js b/packages/plugin-github/tests/index.snap.js index 294333cd36..90fb64c3bd 100644 --- a/packages/plugin-github/tests/index.snap.js +++ b/packages/plugin-github/tests/index.snap.js @@ -19,11 +19,16 @@ URL: https://github.com/koishijs/koishi/issues/19 Title: Wie kann man um das Koishi zu installieren? Ich verstecke Englisch und Chinesisch nicht! Gab es Personen, die mir helfen kann?` -module.exports[`pull_request.opened`] = ` +module.exports[`pull_request.opened.1`] = ` [GitHub] simon300000 opened a pull request koishijs/koishi#20 (koishijs:develop <- koishijs:experimental) URL: https://github.com/koishijs/koishi/pull/20 Das ist wichtig!` +module.exports[`pull_request.opened.2`] = ` +[GitHub] jjyyxx opened a pull request koishijs/koishi#90 (koishijs:mapping <- jjyyxx:mapping) +URL: https://github.com/koishijs/koishi/pull/90 +According to \`tsconfig-paths\` documentation ...` + module.exports[`pull_request_review.submitted.2`] = ` [GitHub] Kouchya reviewed pull request koishijs/koishi#20 URL: https://github.com/koishijs/koishi/pull/20#pullrequestreview-345349537 @@ -36,7 +41,7 @@ URL: https://github.com/koishijs/koishi/pull/20#discussion_r368570320 Naming is so hard......` module.exports[`push.commit`] = ` -[GitHub] Shigma pushed to koishijs/koishi:refs/heads/develop +[GitHub] Shigma pushed to koishijs/koishi:develop Compare: https://github.com/koishijs/koishi/compare/976c6e8f09a4...3ae7e7044d06 [d7ff34] chore: adjust [3ae7e7] fix(core): create major context at demand` From feb6698693fc66ec00a04342da7b2a72c6326717 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Mon, 31 Aug 2020 21:58:08 +0800 Subject: [PATCH 17/22] feat(github): support fork event --- packages/plugin-github/src/index.ts | 4 + .../plugin-github/tests/fixtures/fork.json | 237 ++++++++++++++++++ packages/plugin-github/tests/index.snap.js | 3 + 3 files changed, 244 insertions(+) create mode 100644 packages/plugin-github/tests/fixtures/fork.json diff --git a/packages/plugin-github/src/index.ts b/packages/plugin-github/src/index.ts index 2bbfe9aded..9b21b817c3 100644 --- a/packages/plugin-github/src/index.ts +++ b/packages/plugin-github/src/index.ts @@ -274,4 +274,8 @@ export function apply(ctx: Context, config: Config = {}) { ...commits.map(c => `[${c.id.slice(0, 6)}] ${formatMarkdown(c.message)}`), ].join('\n')] }) + + registerHandler('fork', ({ repository, sender, forkee }) => { + return [`[GitHub] ${sender.login} forked ${repository.full_name} to ${forkee.full_name}`] + }) } diff --git a/packages/plugin-github/tests/fixtures/fork.json b/packages/plugin-github/tests/fixtures/fork.json new file mode 100644 index 0000000000..5aba27cde2 --- /dev/null +++ b/packages/plugin-github/tests/fixtures/fork.json @@ -0,0 +1,237 @@ +{ + "forkee": { + "id": 290096040, + "node_id": "MDEwOlJlcG9zaXRvcnkyOTAwOTYwNDA=", + "name": "koishi", + "full_name": "jjyyxx/koishi", + "private": false, + "owner": { + "login": "jjyyxx", + "id": 13808089, + "node_id": "MDQ6VXNlcjEzODA4MDg5", + "avatar_url": "https://avatars3.githubusercontent.com/u/13808089?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/jjyyxx", + "html_url": "https://github.com/jjyyxx", + "followers_url": "https://api.github.com/users/jjyyxx/followers", + "following_url": "https://api.github.com/users/jjyyxx/following{/other_user}", + "gists_url": "https://api.github.com/users/jjyyxx/gists{/gist_id}", + "starred_url": "https://api.github.com/users/jjyyxx/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/jjyyxx/subscriptions", + "organizations_url": "https://api.github.com/users/jjyyxx/orgs", + "repos_url": "https://api.github.com/users/jjyyxx/repos", + "events_url": "https://api.github.com/users/jjyyxx/events{/privacy}", + "received_events_url": "https://api.github.com/users/jjyyxx/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/jjyyxx/koishi", + "description": "QQ bot out of the box", + "fork": true, + "url": "https://api.github.com/repos/jjyyxx/koishi", + "forks_url": "https://api.github.com/repos/jjyyxx/koishi/forks", + "keys_url": "https://api.github.com/repos/jjyyxx/koishi/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/jjyyxx/koishi/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/jjyyxx/koishi/teams", + "hooks_url": "https://api.github.com/repos/jjyyxx/koishi/hooks", + "issue_events_url": "https://api.github.com/repos/jjyyxx/koishi/issues/events{/number}", + "events_url": "https://api.github.com/repos/jjyyxx/koishi/events", + "assignees_url": "https://api.github.com/repos/jjyyxx/koishi/assignees{/user}", + "branches_url": "https://api.github.com/repos/jjyyxx/koishi/branches{/branch}", + "tags_url": "https://api.github.com/repos/jjyyxx/koishi/tags", + "blobs_url": "https://api.github.com/repos/jjyyxx/koishi/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/jjyyxx/koishi/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/jjyyxx/koishi/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/jjyyxx/koishi/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/jjyyxx/koishi/statuses/{sha}", + "languages_url": "https://api.github.com/repos/jjyyxx/koishi/languages", + "stargazers_url": "https://api.github.com/repos/jjyyxx/koishi/stargazers", + "contributors_url": "https://api.github.com/repos/jjyyxx/koishi/contributors", + "subscribers_url": "https://api.github.com/repos/jjyyxx/koishi/subscribers", + "subscription_url": "https://api.github.com/repos/jjyyxx/koishi/subscription", + "commits_url": "https://api.github.com/repos/jjyyxx/koishi/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/jjyyxx/koishi/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/jjyyxx/koishi/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/jjyyxx/koishi/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/jjyyxx/koishi/contents/{+path}", + "compare_url": "https://api.github.com/repos/jjyyxx/koishi/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/jjyyxx/koishi/merges", + "archive_url": "https://api.github.com/repos/jjyyxx/koishi/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/jjyyxx/koishi/downloads", + "issues_url": "https://api.github.com/repos/jjyyxx/koishi/issues{/number}", + "pulls_url": "https://api.github.com/repos/jjyyxx/koishi/pulls{/number}", + "milestones_url": "https://api.github.com/repos/jjyyxx/koishi/milestones{/number}", + "notifications_url": "https://api.github.com/repos/jjyyxx/koishi/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/jjyyxx/koishi/labels{/name}", + "releases_url": "https://api.github.com/repos/jjyyxx/koishi/releases{/id}", + "deployments_url": "https://api.github.com/repos/jjyyxx/koishi/deployments", + "created_at": "2020-08-25T02:51:14Z", + "updated_at": "2020-08-24T19:15:52Z", + "pushed_at": "2020-08-24T18:51:18Z", + "git_url": "git://github.com/jjyyxx/koishi.git", + "ssh_url": "git@github.com:jjyyxx/koishi.git", + "clone_url": "https://github.com/jjyyxx/koishi.git", + "svn_url": "https://github.com/jjyyxx/koishi", + "homepage": "https://koishi.js.org", + "size": 2438, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": false, + "has_projects": true, + "has_downloads": true, + "has_wiki": false, + "has_pages": false, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 0, + "license": { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZTEz" + }, + "forks": 0, + "open_issues": 0, + "watchers": 0, + "default_branch": "master", + "public": true + }, + "repository": { + "id": 225572038, + "node_id": "MDEwOlJlcG9zaXRvcnkyMjU1NzIwMzg=", + "name": "koishi", + "full_name": "koishijs/koishi", + "private": false, + "owner": { + "login": "koishijs", + "id": 58179220, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjU4MTc5MjIw", + "avatar_url": "https://avatars3.githubusercontent.com/u/58179220?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/koishijs", + "html_url": "https://github.com/koishijs", + "followers_url": "https://api.github.com/users/koishijs/followers", + "following_url": "https://api.github.com/users/koishijs/following{/other_user}", + "gists_url": "https://api.github.com/users/koishijs/gists{/gist_id}", + "starred_url": "https://api.github.com/users/koishijs/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/koishijs/subscriptions", + "organizations_url": "https://api.github.com/users/koishijs/orgs", + "repos_url": "https://api.github.com/users/koishijs/repos", + "events_url": "https://api.github.com/users/koishijs/events{/privacy}", + "received_events_url": "https://api.github.com/users/koishijs/received_events", + "type": "Organization", + "site_admin": false + }, + "html_url": "https://github.com/koishijs/koishi", + "description": "QQ bot out of the box", + "fork": false, + "url": "https://api.github.com/repos/koishijs/koishi", + "forks_url": "https://api.github.com/repos/koishijs/koishi/forks", + "keys_url": "https://api.github.com/repos/koishijs/koishi/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/koishijs/koishi/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/koishijs/koishi/teams", + "hooks_url": "https://api.github.com/repos/koishijs/koishi/hooks", + "issue_events_url": "https://api.github.com/repos/koishijs/koishi/issues/events{/number}", + "events_url": "https://api.github.com/repos/koishijs/koishi/events", + "assignees_url": "https://api.github.com/repos/koishijs/koishi/assignees{/user}", + "branches_url": "https://api.github.com/repos/koishijs/koishi/branches{/branch}", + "tags_url": "https://api.github.com/repos/koishijs/koishi/tags", + "blobs_url": "https://api.github.com/repos/koishijs/koishi/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/koishijs/koishi/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/koishijs/koishi/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/koishijs/koishi/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/koishijs/koishi/statuses/{sha}", + "languages_url": "https://api.github.com/repos/koishijs/koishi/languages", + "stargazers_url": "https://api.github.com/repos/koishijs/koishi/stargazers", + "contributors_url": "https://api.github.com/repos/koishijs/koishi/contributors", + "subscribers_url": "https://api.github.com/repos/koishijs/koishi/subscribers", + "subscription_url": "https://api.github.com/repos/koishijs/koishi/subscription", + "commits_url": "https://api.github.com/repos/koishijs/koishi/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/koishijs/koishi/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/koishijs/koishi/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/koishijs/koishi/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/koishijs/koishi/contents/{+path}", + "compare_url": "https://api.github.com/repos/koishijs/koishi/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/koishijs/koishi/merges", + "archive_url": "https://api.github.com/repos/koishijs/koishi/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/koishijs/koishi/downloads", + "issues_url": "https://api.github.com/repos/koishijs/koishi/issues{/number}", + "pulls_url": "https://api.github.com/repos/koishijs/koishi/pulls{/number}", + "milestones_url": "https://api.github.com/repos/koishijs/koishi/milestones{/number}", + "notifications_url": "https://api.github.com/repos/koishijs/koishi/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/koishijs/koishi/labels{/name}", + "releases_url": "https://api.github.com/repos/koishijs/koishi/releases{/id}", + "deployments_url": "https://api.github.com/repos/koishijs/koishi/deployments", + "created_at": "2019-12-03T08:47:29Z", + "updated_at": "2020-08-24T19:15:52Z", + "pushed_at": "2020-08-24T18:51:18Z", + "git_url": "git://github.com/koishijs/koishi.git", + "ssh_url": "git@github.com:koishijs/koishi.git", + "clone_url": "https://github.com/koishijs/koishi.git", + "svn_url": "https://github.com/koishijs/koishi", + "homepage": "https://koishi.js.org", + "size": 2438, + "stargazers_count": 186, + "watchers_count": 186, + "language": "TypeScript", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": false, + "has_pages": false, + "forks_count": 12, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 2, + "license": { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZTEz" + }, + "forks": 12, + "open_issues": 2, + "watchers": 186, + "default_branch": "master" + }, + "organization": { + "login": "koishijs", + "id": 58179220, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjU4MTc5MjIw", + "url": "https://api.github.com/orgs/koishijs", + "repos_url": "https://api.github.com/orgs/koishijs/repos", + "events_url": "https://api.github.com/orgs/koishijs/events", + "hooks_url": "https://api.github.com/orgs/koishijs/hooks", + "issues_url": "https://api.github.com/orgs/koishijs/issues", + "members_url": "https://api.github.com/orgs/koishijs/members{/member}", + "public_members_url": "https://api.github.com/orgs/koishijs/public_members{/member}", + "avatar_url": "https://avatars3.githubusercontent.com/u/58179220?v=4", + "description": "" + }, + "sender": { + "login": "jjyyxx", + "id": 13808089, + "node_id": "MDQ6VXNlcjEzODA4MDg5", + "avatar_url": "https://avatars3.githubusercontent.com/u/13808089?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/jjyyxx", + "html_url": "https://github.com/jjyyxx", + "followers_url": "https://api.github.com/users/jjyyxx/followers", + "following_url": "https://api.github.com/users/jjyyxx/following{/other_user}", + "gists_url": "https://api.github.com/users/jjyyxx/gists{/gist_id}", + "starred_url": "https://api.github.com/users/jjyyxx/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/jjyyxx/subscriptions", + "organizations_url": "https://api.github.com/users/jjyyxx/orgs", + "repos_url": "https://api.github.com/users/jjyyxx/repos", + "events_url": "https://api.github.com/users/jjyyxx/events{/privacy}", + "received_events_url": "https://api.github.com/users/jjyyxx/received_events", + "type": "User", + "site_admin": false + } +} \ No newline at end of file diff --git a/packages/plugin-github/tests/index.snap.js b/packages/plugin-github/tests/index.snap.js index 90fb64c3bd..9c95b5cdc8 100644 --- a/packages/plugin-github/tests/index.snap.js +++ b/packages/plugin-github/tests/index.snap.js @@ -3,6 +3,9 @@ module.exports[`commit_comment.created`] = ` URL: https://github.com/koishijs/koishi/commit/bff469eabe14d42683a4f7c3ccb659daec5e1c00#commitcomment-36878220 This will introduce a failure in test.` +module.exports[`fork`] = ` +[GitHub] jjyyxx forked koishijs/koishi to jjyyxx/koishi` + module.exports[`issue_comment.created.1`] = ` [GitHub] simon300000 commented on issue koishijs/koishi#19 URL: https://github.com/koishijs/koishi/issues/19#issuecomment-576277946 From d5bd9bd73a3d1c1011b4d1d140973dbe132f333d Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Mon, 31 Aug 2020 22:04:39 +0800 Subject: [PATCH 18/22] feat(github): support star.created event --- packages/plugin-github/src/index.ts | 8 +- .../tests/fixtures/star.created.json | 138 ++++++++++++++++++ packages/plugin-github/tests/index.snap.js | 5 +- 3 files changed, 148 insertions(+), 3 deletions(-) create mode 100644 packages/plugin-github/tests/fixtures/star.created.json diff --git a/packages/plugin-github/src/index.ts b/packages/plugin-github/src/index.ts index 9b21b817c3..cf3c49398f 100644 --- a/packages/plugin-github/src/index.ts +++ b/packages/plugin-github/src/index.ts @@ -169,6 +169,10 @@ export function apply(ctx: Context, config: Config = {}) { ].join('\n')] }) + registerHandler('fork', ({ repository, sender, forkee }) => { + return [`[GitHub] ${sender.login} forked ${repository.full_name} to ${forkee.full_name} (total ${repository.forks_count} forks)`] + }) + const checkToken: Middleware = async (session, next) => { const user = await session.$observeUser(['githubToken']) if (!user.githubToken) { @@ -275,7 +279,7 @@ export function apply(ctx: Context, config: Config = {}) { ].join('\n')] }) - registerHandler('fork', ({ repository, sender, forkee }) => { - return [`[GitHub] ${sender.login} forked ${repository.full_name} to ${forkee.full_name}`] + registerHandler('star.created', ({ repository, sender }) => { + return [`[GitHub] ${sender.login} starred ${repository.full_name} (total ${repository.stargazers_count} stargazers)`] }) } diff --git a/packages/plugin-github/tests/fixtures/star.created.json b/packages/plugin-github/tests/fixtures/star.created.json new file mode 100644 index 0000000000..e2a5ab582e --- /dev/null +++ b/packages/plugin-github/tests/fixtures/star.created.json @@ -0,0 +1,138 @@ +{ + "action": "created", + "starred_at": "2020-08-27T10:21:02Z", + "repository": { + "id": 225572038, + "node_id": "MDEwOlJlcG9zaXRvcnkyMjU1NzIwMzg=", + "name": "koishi", + "full_name": "koishijs/koishi", + "private": false, + "owner": { + "login": "koishijs", + "id": 58179220, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjU4MTc5MjIw", + "avatar_url": "https://avatars3.githubusercontent.com/u/58179220?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/koishijs", + "html_url": "https://github.com/koishijs", + "followers_url": "https://api.github.com/users/koishijs/followers", + "following_url": "https://api.github.com/users/koishijs/following{/other_user}", + "gists_url": "https://api.github.com/users/koishijs/gists{/gist_id}", + "starred_url": "https://api.github.com/users/koishijs/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/koishijs/subscriptions", + "organizations_url": "https://api.github.com/users/koishijs/orgs", + "repos_url": "https://api.github.com/users/koishijs/repos", + "events_url": "https://api.github.com/users/koishijs/events{/privacy}", + "received_events_url": "https://api.github.com/users/koishijs/received_events", + "type": "Organization", + "site_admin": false + }, + "html_url": "https://github.com/koishijs/koishi", + "description": "QQ bot out of the box", + "fork": false, + "url": "https://api.github.com/repos/koishijs/koishi", + "forks_url": "https://api.github.com/repos/koishijs/koishi/forks", + "keys_url": "https://api.github.com/repos/koishijs/koishi/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/koishijs/koishi/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/koishijs/koishi/teams", + "hooks_url": "https://api.github.com/repos/koishijs/koishi/hooks", + "issue_events_url": "https://api.github.com/repos/koishijs/koishi/issues/events{/number}", + "events_url": "https://api.github.com/repos/koishijs/koishi/events", + "assignees_url": "https://api.github.com/repos/koishijs/koishi/assignees{/user}", + "branches_url": "https://api.github.com/repos/koishijs/koishi/branches{/branch}", + "tags_url": "https://api.github.com/repos/koishijs/koishi/tags", + "blobs_url": "https://api.github.com/repos/koishijs/koishi/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/koishijs/koishi/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/koishijs/koishi/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/koishijs/koishi/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/koishijs/koishi/statuses/{sha}", + "languages_url": "https://api.github.com/repos/koishijs/koishi/languages", + "stargazers_url": "https://api.github.com/repos/koishijs/koishi/stargazers", + "contributors_url": "https://api.github.com/repos/koishijs/koishi/contributors", + "subscribers_url": "https://api.github.com/repos/koishijs/koishi/subscribers", + "subscription_url": "https://api.github.com/repos/koishijs/koishi/subscription", + "commits_url": "https://api.github.com/repos/koishijs/koishi/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/koishijs/koishi/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/koishijs/koishi/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/koishijs/koishi/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/koishijs/koishi/contents/{+path}", + "compare_url": "https://api.github.com/repos/koishijs/koishi/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/koishijs/koishi/merges", + "archive_url": "https://api.github.com/repos/koishijs/koishi/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/koishijs/koishi/downloads", + "issues_url": "https://api.github.com/repos/koishijs/koishi/issues{/number}", + "pulls_url": "https://api.github.com/repos/koishijs/koishi/pulls{/number}", + "milestones_url": "https://api.github.com/repos/koishijs/koishi/milestones{/number}", + "notifications_url": "https://api.github.com/repos/koishijs/koishi/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/koishijs/koishi/labels{/name}", + "releases_url": "https://api.github.com/repos/koishijs/koishi/releases{/id}", + "deployments_url": "https://api.github.com/repos/koishijs/koishi/deployments", + "created_at": "2019-12-03T08:47:29Z", + "updated_at": "2020-08-27T10:21:02Z", + "pushed_at": "2020-08-27T08:52:42Z", + "git_url": "git://github.com/koishijs/koishi.git", + "ssh_url": "git@github.com:koishijs/koishi.git", + "clone_url": "https://github.com/koishijs/koishi.git", + "svn_url": "https://github.com/koishijs/koishi", + "homepage": "https://koishi.js.org", + "size": 2777, + "stargazers_count": 189, + "watchers_count": 189, + "language": "TypeScript", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": false, + "has_pages": false, + "forks_count": 13, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 5, + "license": { + "key": "mit", + "name": "MIT License", + "spdx_id": "MIT", + "url": "https://api.github.com/licenses/mit", + "node_id": "MDc6TGljZW5zZTEz" + }, + "forks": 13, + "open_issues": 5, + "watchers": 189, + "default_branch": "master" + }, + "organization": { + "login": "koishijs", + "id": 58179220, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjU4MTc5MjIw", + "url": "https://api.github.com/orgs/koishijs", + "repos_url": "https://api.github.com/orgs/koishijs/repos", + "events_url": "https://api.github.com/orgs/koishijs/events", + "hooks_url": "https://api.github.com/orgs/koishijs/hooks", + "issues_url": "https://api.github.com/orgs/koishijs/issues", + "members_url": "https://api.github.com/orgs/koishijs/members{/member}", + "public_members_url": "https://api.github.com/orgs/koishijs/public_members{/member}", + "avatar_url": "https://avatars3.githubusercontent.com/u/58179220?v=4", + "description": "" + }, + "sender": { + "login": "275761919", + "id": 14012127, + "node_id": "MDQ6VXNlcjE0MDEyMTI3", + "avatar_url": "https://avatars0.githubusercontent.com/u/14012127?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/275761919", + "html_url": "https://github.com/275761919", + "followers_url": "https://api.github.com/users/275761919/followers", + "following_url": "https://api.github.com/users/275761919/following{/other_user}", + "gists_url": "https://api.github.com/users/275761919/gists{/gist_id}", + "starred_url": "https://api.github.com/users/275761919/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/275761919/subscriptions", + "organizations_url": "https://api.github.com/users/275761919/orgs", + "repos_url": "https://api.github.com/users/275761919/repos", + "events_url": "https://api.github.com/users/275761919/events{/privacy}", + "received_events_url": "https://api.github.com/users/275761919/received_events", + "type": "User", + "site_admin": false + } +} \ No newline at end of file diff --git a/packages/plugin-github/tests/index.snap.js b/packages/plugin-github/tests/index.snap.js index 9c95b5cdc8..5f210bad61 100644 --- a/packages/plugin-github/tests/index.snap.js +++ b/packages/plugin-github/tests/index.snap.js @@ -4,7 +4,7 @@ URL: https://github.com/koishijs/koishi/commit/bff469eabe14d42683a4f7c3ccb659dae This will introduce a failure in test.` module.exports[`fork`] = ` -[GitHub] jjyyxx forked koishijs/koishi to jjyyxx/koishi` +[GitHub] jjyyxx forked koishijs/koishi to jjyyxx/koishi (total 12 forks)` module.exports[`issue_comment.created.1`] = ` [GitHub] simon300000 commented on issue koishijs/koishi#19 @@ -51,3 +51,6 @@ Compare: https://github.com/koishijs/koishi/compare/976c6e8f09a4...3ae7e7044d06 module.exports[`push.tag`] = ` [GitHub] Shigma published tag koishijs/koishi@1.5.0` + +module.exports[`star.created`] = ` +[GitHub] 275761919 starred koishijs/koishi (total 189 stargazers)` From cda20da6fc6cdb141cd57c144d98c3acf1b10ed3 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Mon, 31 Aug 2020 22:09:55 +0800 Subject: [PATCH 19/22] chore: fix cli database typings break --- packages/koishi/src/init.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/koishi/src/init.ts b/packages/koishi/src/init.ts index 3e6d65e76b..08a7583850 100644 --- a/packages/koishi/src/init.ts +++ b/packages/koishi/src/init.ts @@ -4,8 +4,8 @@ import { resolve, extname, dirname } from 'path' import { AppConfig } from './worker' import { CAC } from 'cac' import prompts, { Choice, PrevCaller, PromptObject } from 'prompts' -import * as mysql from 'koishi-plugin-mysql' -import * as mongo from 'koishi-plugin-mongo' +import * as mysql from 'koishi-plugin-mysql/dist/database' +import * as mongo from 'koishi-plugin-mongo/dist/database' function conditional(type: T, key: string, ...values: string[]): PrevCaller { return (prev, data, prompt) => { From 36562848563530f8452c9caa28ffc398c50155d0 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Mon, 31 Aug 2020 23:12:24 +0800 Subject: [PATCH 20/22] test: core database tests --- packages/koishi-core/tests/command.spec.ts | 2 ++ packages/koishi-core/tests/help.spec.ts | 2 ++ packages/koishi-core/tests/server.spec.ts | 30 +++++++++++++++++++++- packages/plugin-github/src/index.ts | 3 ++- 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/koishi-core/tests/command.spec.ts b/packages/koishi-core/tests/command.spec.ts index 74ed02db3b..0c738dc504 100644 --- a/packages/koishi-core/tests/command.spec.ts +++ b/packages/koishi-core/tests/command.spec.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ + import { App } from 'koishi-test-utils' import { spyOn } from 'jest-mock' import { Logger, noop } from 'koishi-utils' diff --git a/packages/koishi-core/tests/help.spec.ts b/packages/koishi-core/tests/help.spec.ts index 6b3ca58db3..46ad68b941 100644 --- a/packages/koishi-core/tests/help.spec.ts +++ b/packages/koishi-core/tests/help.spec.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ + import { memory, App } from 'koishi-test-utils' import { Time } from 'koishi-utils' import { Message } from 'koishi-core' diff --git a/packages/koishi-core/tests/server.spec.ts b/packages/koishi-core/tests/server.spec.ts index 1277255ab2..fe23ce3ea1 100644 --- a/packages/koishi-core/tests/server.spec.ts +++ b/packages/koishi-core/tests/server.spec.ts @@ -1,7 +1,8 @@ import { App, BASE_SELF_ID, memory } from 'koishi-test-utils' -import { App as RealApp, Group, Session } from 'koishi-core' +import { App as RealApp, extendDatabase, Group, Session } from 'koishi-core' import { expect } from 'chai' import { fn, spyOn } from 'jest-mock' +import { Logger } from 'koishi-utils' describe('Server API', () => { describe('Adaptation API', () => { @@ -96,4 +97,31 @@ describe('Server API', () => { expect(sendGroupMsg.mock.calls).to.deep.equal([[123, 'foo'], [456, 'foo']]) }) }) + + describe('Database API', () => { + const app = new App() + const warn = spyOn(new Logger('app'), 'warn') + + it('override database', () => { + warn.mockClear() + app.database = {} as any + expect(warn.mock.calls).to.have.length(0) + app.database = {} as any + expect(warn.mock.calls).to.have.length(1) + }) + + it('extend database', () => { + const callback = fn() + const id = 'this-module-does-not-exist' + extendDatabase(id, callback) + expect(callback.mock.calls).to.have.length(0) + class CustomDatabase {} + const module = require.cache[require.resolve('koishi-core/src/database.ts')] + const mockedRequire = spyOn(module, 'require') + mockedRequire.mockReturnValue({ default: CustomDatabase }) + extendDatabase(id, callback) + expect(callback.mock.calls).to.deep.equal([[CustomDatabase]]) + mockedRequire.mockRestore() + }) + }) }) diff --git a/packages/plugin-github/src/index.ts b/packages/plugin-github/src/index.ts index cf3c49398f..852b1897f4 100644 --- a/packages/plugin-github/src/index.ts +++ b/packages/plugin-github/src/index.ts @@ -11,7 +11,8 @@ import axios from 'axios' type Payload = GetWebhookPayloadTypeFromEvent['payload'] type Repository = EventPayloads.PayloadRepository -type Issue = EventPayloads.WebhookPayloadIssuesIssue +type Issue = + | EventPayloads.WebhookPayloadIssuesIssue | EventPayloads.WebhookPayloadPullRequestPullRequest | EventPayloads.WebhookPayloadPullRequestReviewPullRequest type ReviewComment = EventPayloads.WebhookPayloadPullRequestReviewCommentComment From 734d7c47a96516e76855d8e915f476df6eeb12ae Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Mon, 31 Aug 2020 23:36:36 +0800 Subject: [PATCH 21/22] feat(github): auto attach user fields --- packages/plugin-github/src/events.ts | 209 +++++++++++++++++++++++++++ packages/plugin-github/src/index.ts | 200 +------------------------ 2 files changed, 217 insertions(+), 192 deletions(-) create mode 100644 packages/plugin-github/src/events.ts diff --git a/packages/plugin-github/src/events.ts b/packages/plugin-github/src/events.ts new file mode 100644 index 0000000000..de00ad0248 --- /dev/null +++ b/packages/plugin-github/src/events.ts @@ -0,0 +1,209 @@ +/* eslint-disable camelcase */ + +import { EventNames, EventPayloads, Webhooks } from '@octokit/webhooks' +import { GetWebhookPayloadTypeFromEvent } from '@octokit/webhooks/dist-types/generated/get-webhook-payload-type-from-event' +import { Octokit } from '@octokit/rest' +import { Context, Middleware, Session } from 'koishi-core' +import { defineProperty } from 'koishi-utils' +import { Config } from '.' + +type Payload = GetWebhookPayloadTypeFromEvent['payload'] +type Repository = EventPayloads.PayloadRepository +type Issue = + | EventPayloads.WebhookPayloadIssuesIssue + | EventPayloads.WebhookPayloadPullRequestPullRequest + | EventPayloads.WebhookPayloadPullRequestReviewPullRequest +type ReviewComment = EventPayloads.WebhookPayloadPullRequestReviewCommentComment + +export default function apply(ctx: Context, config: Config) { + const github = new Octokit({ + request: { + agent: config.agent, + timeout: config.requestTimeout, + }, + }) + + const webhooks = new Webhooks({ + ...config, + path: config.webhook, + }) + + defineProperty(ctx.app, 'githubWebhooks', webhooks) + + const interactions: Record = {} + + ctx.router.post(config.webhook, (ctx, next) => { + // workaround @octokit/webhooks for koa + ctx.req['body'] = ctx.request.body + return webhooks.middleware(ctx.req, ctx.res, next) + }) + + ctx.on('before-attach-user', (session, fields) => { + if (interactions[session.$reply]) { + fields.add('githubToken') + } + }) + + ctx.middleware((session, next) => { + const middleware = interactions[session.$reply] + return middleware ? middleware(session, next) : next() + }) + + function registerHandler(event: T, handler: (payload: Payload) => [string, Middleware?]) { + webhooks.on(event, async (callback) => { + const { repository } = callback.payload + const groupIds = config.repos[repository.full_name] + if (!groupIds) return + + const result = handler(callback.payload) + if (!result) return + + const [message, middleware] = result + const messageIds = await ctx.broadcast(groupIds, message) + if (!middleware) return + + for (const id of messageIds) { + interactions[id] = middleware + } + setTimeout(() => { + for (const id of messageIds) { + delete interactions[id] + } + }, config.replyTimeout) + }) + } + + function formatMarkdown(source: string) { + return source + .replace(/^```(.*)$/gm, '') + .replace(/\n\s*\n/g, '\n') + } + + registerHandler('commit_comment.created', ({ repository, comment }) => { + const { user, html_url, commit_id, body } = comment + return [[ + `[GitHub] ${user.login} commented on commit ${repository.full_name}@${commit_id.slice(0, 6)}`, + `URL: ${html_url}`, + formatMarkdown(body), + ].join('\n')] + }) + + registerHandler('fork', ({ repository, sender, forkee }) => { + const { full_name, forks_count } = repository + return [`[GitHub] ${sender.login} forked ${full_name} to ${forkee.full_name} (total ${forks_count} forks)`] + }) + + const checkToken: Middleware = async (session: Session<'githubToken'>, next) => { + if (!session.$user.githubToken) { + await session.$send('如果想使用此功能,请对机器人进行授权,输入你的 GitHub 用户名。') + const name = await session.$prompt().catch() + if (!name) return + return session.$execute({ command: 'github', args: [name] }) + } + return next() + } + + const createIssueComment = (repo: Repository, issue: Issue): Middleware => async (session, next) => { + const body = session.$parsed + if (!body) return next() + return checkToken(session, async () => { + await github.issues.createComment({ + owner: repo.owner.login, + repo: repo.name, + issue_number: issue.number, + body, + headers: { + Authorization: `token ${session.$user['githubToken']}`, + }, + }) + }) + } + + const createReviewCommentReply = (repo: Repository, issue: Issue, comment: ReviewComment): Middleware => async (session, next) => { + const body = session.$parsed + if (!body) return next() + return checkToken(session, async () => { + await github.pulls.createReplyForReviewComment({ + owner: repo.owner.login, + repo: repo.name, + pull_number: issue.number, + comment_id: comment.id, + body, + headers: { + Authorization: `token ${session.$user['githubToken']}`, + }, + }) + }) + } + + registerHandler('issues.opened', ({ repository, issue }) => { + const { user, html_url, title, body, number } = issue + return [[ + `[GitHub] ${user.login} opened an issue ${repository.full_name}#${number}`, + `URL: ${html_url}`, + `Title: ${title}`, + formatMarkdown(body), + ].join('\n'), createIssueComment(repository, issue)] + }) + + registerHandler('issue_comment.created', ({ comment, issue, repository }) => { + const { user, html_url, body } = comment + const type = issue['pull_request'] ? 'pull request' : 'issue' + return [[ + `[GitHub] ${user.login} commented on ${type} ${repository.full_name}#${issue.number}`, + `URL: ${html_url}`, + formatMarkdown(body), + ].join('\n'), createIssueComment(repository, issue)] + }) + + registerHandler('pull_request.opened', ({ repository, pull_request }) => { + const { user, html_url, base, head, body, number } = pull_request + return [[ + `[GitHub] ${user.login} opened a pull request ${repository.full_name}#${number} (${base.label} <- ${head.label})`, + `URL: ${html_url}`, + formatMarkdown(body), + ].join('\n'), createIssueComment(repository, pull_request)] + }) + + registerHandler('pull_request_review.submitted', ({ repository, review, pull_request }) => { + if (!review.body) return + const { user, html_url, body } = review + return [[ + `[GitHub] ${user.login} reviewed pull request ${repository.full_name}#${pull_request.number}`, + `URL: ${html_url}`, + // @ts-ignore + formatMarkdown(body), + ].join('\n'), createIssueComment(repository, pull_request)] + }) + + registerHandler('pull_request_review_comment.created', ({ repository, comment, pull_request }) => { + const { user, path, html_url, body } = comment + return [[ + `[GitHub] ${user.login} commented on pull request review ${repository.full_name}#${pull_request.number}`, + `Path: ${path}`, + `URL: ${html_url}`, + formatMarkdown(body), + ].join('\n'), createReviewCommentReply(repository, pull_request, comment)] + }) + + registerHandler('push', ({ compare, pusher, commits, repository, ref, after }) => { + // do not show pull request merge + if (/^0+$/.test(after)) return + + // use short form for tag releases + if (ref.startsWith('refs/tags')) { + return [`[GitHub] ${pusher.name} published tag ${repository.full_name}@${ref.slice(10)}`] + } + + return [[ + `[GitHub] ${pusher.name} pushed to ${repository.full_name}:${ref.replace(/^refs\/heads\//, '')}`, + `Compare: ${compare}`, + ...commits.map(c => `[${c.id.slice(0, 6)}] ${formatMarkdown(c.message)}`), + ].join('\n')] + }) + + registerHandler('star.created', ({ repository, sender }) => { + const { full_name, stargazers_count } = repository + return [`[GitHub] ${sender.login} starred ${full_name} (total ${stargazers_count} stargazers)`] + }) +} diff --git a/packages/plugin-github/src/index.ts b/packages/plugin-github/src/index.ts index 852b1897f4..30486d2031 100644 --- a/packages/plugin-github/src/index.ts +++ b/packages/plugin-github/src/index.ts @@ -1,21 +1,12 @@ /* eslint-disable camelcase */ -import { Context, Middleware, User } from 'koishi-core' -import { defineProperty, Logger, Time } from 'koishi-utils' -import { Octokit } from '@octokit/rest' -import { Webhooks, EventNames, EventPayloads } from '@octokit/webhooks' -import { GetWebhookPayloadTypeFromEvent } from '@octokit/webhooks/dist-types/generated/get-webhook-payload-type-from-event' +import { Context, User } from 'koishi-core' +import { Logger, Time } from 'koishi-utils' +import { Webhooks } from '@octokit/webhooks' import { Agent } from 'http' import { encode } from 'querystring' import axios from 'axios' - -type Payload = GetWebhookPayloadTypeFromEvent['payload'] -type Repository = EventPayloads.PayloadRepository -type Issue = - | EventPayloads.WebhookPayloadIssuesIssue - | EventPayloads.WebhookPayloadPullRequestPullRequest - | EventPayloads.WebhookPayloadPullRequestReviewPullRequest -type ReviewComment = EventPayloads.WebhookPayloadPullRequestReviewCommentComment +import events from './events' declare module 'koishi-core/dist/app' { interface App { @@ -63,33 +54,17 @@ const defaultOptions: Config = { repos: {}, } -export const name = 'github-webhook' +export const name = 'github' export function apply(ctx: Context, config: Config = {}) { if (!ctx.router) throw new Error('ctx.router is not defined') config = { ...defaultOptions, ...config } - const webhooks = new Webhooks({ - ...config, - path: config.webhook, - }) - defineProperty(ctx.app, 'githubWebhooks', webhooks) - const github = new Octokit({ - request: { - agent: config.agent, - timeout: config.requestTimeout, - }, - }) + ctx.plugin(events, config) - const { router, database } = ctx + const { database } = ctx - router.post(config.webhook, (ctx, next) => { - // workaround @octokit/webhooks for koa - ctx.req['body'] = ctx.request.body - return webhooks.middleware(ctx.req, ctx.res, next) - }) - - router.get(config.authorize, async (ctx) => { + ctx.router.get(config.authorize, async (ctx) => { const targetId = parseInt(ctx.query.state) if (Number.isNaN(targetId)) throw new Error('Invalid targetId') const { code, state } = ctx.query @@ -124,163 +99,4 @@ export function apply(ctx: Context, config: Config = {}) { }) return '请点击下面的链接继续操作:\n' + url }) - - const interactions: Record = {} - - ctx.middleware((session, next) => { - const middleware = interactions[session.$reply] - return middleware ? middleware(session, next) : next() - }) - - function registerHandler(event: T, handler: (payload: Payload) => [string, Middleware?]) { - webhooks.on(event, async (callback) => { - const { repository } = callback.payload - const groupIds = config.repos[repository.full_name] - if (!groupIds) return - - const result = handler(callback.payload) - if (!result) return - - const [message, middleware] = result - const messageIds = await ctx.broadcast(groupIds, message) - if (!middleware) return - - for (const id of messageIds) { - interactions[id] = middleware - } - setTimeout(() => { - for (const id of messageIds) { - delete interactions[id] - } - }, config.replyTimeout) - }) - } - - function formatMarkdown(source: string) { - return source - .replace(/^```(.*)$/gm, '') - .replace(/\n\s*\n/g, '\n') - } - - registerHandler('commit_comment.created', ({ repository, comment }) => { - return [[ - `[GitHub] ${comment.user.login} commented on commit ${repository.full_name}@${comment.commit_id.slice(0, 6)}`, - `URL: ${comment.html_url}`, - formatMarkdown(comment.body), - ].join('\n')] - }) - - registerHandler('fork', ({ repository, sender, forkee }) => { - return [`[GitHub] ${sender.login} forked ${repository.full_name} to ${forkee.full_name} (total ${repository.forks_count} forks)`] - }) - - const checkToken: Middleware = async (session, next) => { - const user = await session.$observeUser(['githubToken']) - if (!user.githubToken) { - await session.$send('如果想使用此功能,请对机器人进行授权,输入你的 GitHub 用户名。') - const name = await session.$prompt().catch() - if (!name) return - return session.$execute({ command: 'github', args: [name] }) - } - return next() - } - - const createIssueComment = (repo: Repository, issue: Issue): Middleware => async (session, next) => { - const body = session.$parsed - if (!body) return next() - return checkToken(session, async () => { - await github.issues.createComment({ - owner: repo.owner.login, - repo: repo.name, - issue_number: issue.number, - body, - headers: { - Authorization: `token ${session.$user['githubToken']}`, - }, - }) - }) - } - - const createReviewCommentReply = (repo: Repository, issue: Issue, comment: ReviewComment): Middleware => async (session, next) => { - const body = session.$parsed - if (!body) return next() - return checkToken(session, async () => { - await github.pulls.createReplyForReviewComment({ - owner: repo.owner.login, - repo: repo.name, - pull_number: issue.number, - comment_id: comment.id, - body, - headers: { - Authorization: `token ${session.$user['githubToken']}`, - }, - }) - }) - } - - registerHandler('issues.opened', ({ repository, issue }) => { - return [[ - `[GitHub] ${issue.user.login} opened an issue ${repository.full_name}#${issue.number}`, - `URL: ${issue.html_url}`, - `Title: ${issue.title}`, - formatMarkdown(issue.body), - ].join('\n'), createIssueComment(repository, issue)] - }) - - registerHandler('issue_comment.created', ({ comment, issue, repository }) => { - const type = issue['pull_request'] ? 'pull request' : 'issue' - return [[ - `[GitHub] ${comment.user.login} commented on ${type} ${repository.full_name}#${issue.number}`, - `URL: ${comment.html_url}`, - formatMarkdown(comment.body), - ].join('\n'), createIssueComment(repository, issue)] - }) - - registerHandler('pull_request.opened', ({ repository, pull_request }) => { - const { user, html_url, base, head, body, number } = pull_request - return [[ - `[GitHub] ${user.login} opened a pull request ${repository.full_name}#${number} (${base.label} <- ${head.label})`, - `URL: ${html_url}`, - formatMarkdown(body), - ].join('\n'), createIssueComment(repository, pull_request)] - }) - - registerHandler('pull_request_review.submitted', ({ repository, review, pull_request }) => { - if (!review.body) return - return [[ - `[GitHub] ${review.user.login} reviewed pull request ${repository.full_name}#${pull_request.number}`, - `URL: ${review.html_url}`, - // @ts-ignore - formatMarkdown(review.body), - ].join('\n'), createIssueComment(repository, pull_request)] - }) - - registerHandler('pull_request_review_comment.created', ({ repository, comment, pull_request }) => { - return [[ - `[GitHub] ${comment.user.login} commented on pull request review ${repository.full_name}#${pull_request.number}`, - `Path: ${comment.path}`, - `URL: ${comment.html_url}`, - formatMarkdown(comment.body), - ].join('\n'), createReviewCommentReply(repository, pull_request, comment)] - }) - - registerHandler('push', ({ compare, pusher, commits, repository, ref, after }) => { - // do not show pull request merge - if (/^0+$/.test(after)) return - - // use short form for tag releases - if (ref.startsWith('refs/tags')) { - return [`[GitHub] ${pusher.name} published tag ${repository.full_name}@${ref.slice(10)}`] - } - - return [[ - `[GitHub] ${pusher.name} pushed to ${repository.full_name}:${ref.replace(/^refs\/heads\//, '')}`, - `Compare: ${compare}`, - ...commits.map(c => `[${c.id.slice(0, 6)}] ${formatMarkdown(c.message)}`), - ].join('\n')] - }) - - registerHandler('star.created', ({ repository, sender }) => { - return [`[GitHub] ${sender.login} starred ${repository.full_name} (total ${repository.stargazers_count} stargazers)`] - }) } From 5a5e39dcb3419a2684c93ac2e6be3fa7afe10ea7 Mon Sep 17 00:00:00 2001 From: Shigma <1700011071@pku.edu.cn> Date: Tue, 1 Sep 2020 00:31:15 +0800 Subject: [PATCH 22/22] chore: bump versions --- README.md | 6 ++++++ packages/adapter-cqhttp/package.json | 4 ++-- packages/koishi-core/package.json | 4 ++-- packages/koishi-test-utils/package.json | 4 ++-- packages/koishi-utils/package.json | 2 +- packages/koishi/ecosystem.json | 6 +++--- packages/koishi/package.json | 4 ++-- packages/plugin-chess/package.json | 4 ++-- packages/plugin-common/package.json | 4 ++-- packages/plugin-eval-addons/package.json | 6 +++--- packages/plugin-eval/package.json | 4 ++-- packages/plugin-github/package.json | 6 +++--- packages/plugin-image-search/package.json | 4 ++-- packages/plugin-mongo/package.json | 4 ++-- packages/plugin-monitor/package.json | 4 ++-- packages/plugin-mysql/package.json | 4 ++-- packages/plugin-puppeteer/package.json | 4 ++-- packages/plugin-rss/package.json | 4 ++-- packages/plugin-schedule/package.json | 6 +++--- packages/plugin-status/package.json | 6 +++--- packages/plugin-teach/package.json | 6 +++--- packages/plugin-tools/package.json | 4 ++-- 22 files changed, 53 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index b89263c561..ea4c94ed0d 100644 --- a/README.md +++ b/README.md @@ -120,3 +120,9 @@ koishi-plugin-schedule 允许用户设置定时任务并执行。这些计划任 ### [koishi-plugin-teach](./packages/plugin-teach) [![npm](https://img.shields.io/npm/v/koishi-plugin-teach?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-teach) ### [koishi-plugin-tools](./packages/plugin-tools) [![npm](https://img.shields.io/npm/v/koishi-plugin-tools?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-tools) + +## 协议 + +[MIT](./LICENSE) + +Copyright © 2019-present, Shigma diff --git a/packages/adapter-cqhttp/package.json b/packages/adapter-cqhttp/package.json index acbd65c0cb..08f33b16f6 100644 --- a/packages/adapter-cqhttp/package.json +++ b/packages/adapter-cqhttp/package.json @@ -31,7 +31,7 @@ "koishi" ], "peerDependencies": { - "koishi-core": "^2.0.2" + "koishi-core": "^2.1.0" }, "devDependencies": { "@types/ms": "^0.7.31", @@ -43,6 +43,6 @@ "axios": "^0.20.0", "ms": "^2.1.2", "ws": "^7.3.1", - "koishi-utils": "^3.1.2" + "koishi-utils": "^3.1.3" } } diff --git a/packages/koishi-core/package.json b/packages/koishi-core/package.json index 7ca86fdfac..ec5d743e3b 100644 --- a/packages/koishi-core/package.json +++ b/packages/koishi-core/package.json @@ -1,7 +1,7 @@ { "name": "koishi-core", "description": "Core features for Koishi", - "version": "2.0.2", + "version": "2.1.0", "main": "dist/index.js", "typings": "dist/index.d.ts", "engines": { @@ -44,7 +44,7 @@ "koa": "^2.13.0", "koa-bodyparser": "^4.3.0", "koa-router": "^9.4.0", - "koishi-utils": "^3.1.2", + "koishi-utils": "^3.1.3", "leven": "^3.1.0", "lru-cache": "^6.0.0" } diff --git a/packages/koishi-test-utils/package.json b/packages/koishi-test-utils/package.json index 63173017cc..49214cc7a7 100644 --- a/packages/koishi-test-utils/package.json +++ b/packages/koishi-test-utils/package.json @@ -40,8 +40,8 @@ "dependencies": { "chai": "^4.2.0", "chai-as-promised": "^7.1.1", - "koishi-core": "^2.0.2", - "koishi-utils": "^3.1.2" + "koishi-core": "^2.1.0", + "koishi-utils": "^3.1.3" }, "devDependencies": { "@types/chai": "^4.2.12", diff --git a/packages/koishi-utils/package.json b/packages/koishi-utils/package.json index 9e5700e185..06e3325ae0 100644 --- a/packages/koishi-utils/package.json +++ b/packages/koishi-utils/package.json @@ -1,7 +1,7 @@ { "name": "koishi-utils", "description": "Utilities for Koishi", - "version": "3.1.2", + "version": "3.1.3", "main": "dist/index.js", "typings": "dist/index.d.ts", "files": [ diff --git a/packages/koishi/ecosystem.json b/packages/koishi/ecosystem.json index 09d3c1040c..02c045b4cd 100644 --- a/packages/koishi/ecosystem.json +++ b/packages/koishi/ecosystem.json @@ -16,11 +16,11 @@ "description": "Execute JavaScript in Koishi" }, "koishi-plugin-eval-addons": { - "version": "1.0.0-beta.9", + "version": "1.0.0-beta.10", "description": "Execute JavaScript in Koishi" }, "koishi-plugin-github": { - "version": "2.0.0-beta.9", + "version": "2.0.0-beta.10", "description": "GitHub webhook plugin for Koishi" }, "koishi-plugin-image-search": { @@ -28,7 +28,7 @@ "description": "Image searching plugin for Koishi" }, "koishi-plugin-mongo": { - "version": "1.0.1", + "version": "1.0.2", "description": "MongoDB support for Koishi" }, "koishi-plugin-monitor": { diff --git a/packages/koishi/package.json b/packages/koishi/package.json index 4bc743caa2..3946353c7b 100644 --- a/packages/koishi/package.json +++ b/packages/koishi/package.json @@ -1,7 +1,7 @@ { "name": "koishi", "description": "A QQ bot framework based on CQHTTP", - "version": "2.0.2", + "version": "2.1.0", "main": "dist/index.js", "typings": "dist/index.d.ts", "engines": { @@ -41,7 +41,7 @@ "cac": "^6.6.1", "kleur": "^4.1.1", "koishi-adapter-cqhttp": "^1.0.1", - "koishi-core": "^2.0.2", + "koishi-core": "^2.1.0", "koishi-plugin-common": "^3.0.0-beta.15", "prompts": "^2.3.2" } diff --git a/packages/plugin-chess/package.json b/packages/plugin-chess/package.json index 250b4def53..138cff3b2d 100644 --- a/packages/plugin-chess/package.json +++ b/packages/plugin-chess/package.json @@ -33,10 +33,10 @@ "game" ], "peerDependencies": { - "koishi-core": "^2.0.2", + "koishi-core": "^2.1.0", "koishi-plugin-puppeteer": "^1.0.0" }, "dependencies": { - "koishi-utils": "^3.1.2" + "koishi-utils": "^3.1.3" } } diff --git a/packages/plugin-common/package.json b/packages/plugin-common/package.json index e5a3a12a8f..00a293c261 100644 --- a/packages/plugin-common/package.json +++ b/packages/plugin-common/package.json @@ -31,12 +31,12 @@ "plugin" ], "peerDependencies": { - "koishi-core": "^2.0.2" + "koishi-core": "^2.1.0" }, "devDependencies": { "koishi-test-utils": "^4.0.0" }, "dependencies": { - "koishi-utils": "^3.1.2" + "koishi-utils": "^3.1.3" } } diff --git a/packages/plugin-eval-addons/package.json b/packages/plugin-eval-addons/package.json index 4379a5c068..9d5e728a14 100644 --- a/packages/plugin-eval-addons/package.json +++ b/packages/plugin-eval-addons/package.json @@ -1,6 +1,6 @@ { "name": "koishi-plugin-eval-addons", - "version": "1.0.0-beta.9", + "version": "1.0.0-beta.10", "description": "Execute JavaScript in Koishi", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -36,13 +36,13 @@ "code" ], "peerDependencies": { - "koishi-core": "^2.0.2", + "koishi-core": "^2.1.0", "koishi-plugin-eval": "^2.0.0" }, "dependencies": { "js-yaml": "^3.14.0", "json5": "^2.1.3", - "koishi-utils": "^3.1.2", + "koishi-utils": "^3.1.3", "simple-git": "^2.20.1", "typescript": "^4.0.2" }, diff --git a/packages/plugin-eval/package.json b/packages/plugin-eval/package.json index a92f6f3429..38307bdb52 100644 --- a/packages/plugin-eval/package.json +++ b/packages/plugin-eval/package.json @@ -37,12 +37,12 @@ "code" ], "peerDependencies": { - "koishi-core": "^2.0.2" + "koishi-core": "^2.1.0" }, "devDependencies": { "koishi-test-utils": "^4.0.0" }, "dependencies": { - "koishi-utils": "^3.1.2" + "koishi-utils": "^3.1.3" } } diff --git a/packages/plugin-github/package.json b/packages/plugin-github/package.json index dbd6f16aec..bd4511b603 100644 --- a/packages/plugin-github/package.json +++ b/packages/plugin-github/package.json @@ -1,7 +1,7 @@ { "name": "koishi-plugin-github", "description": "GitHub webhook plugin for Koishi", - "version": "2.0.0-beta.9", + "version": "2.0.0-beta.10", "main": "dist/index.js", "typings": "dist/index.d.ts", "files": [ @@ -36,12 +36,12 @@ "koishi-test-utils": "^4.0.0" }, "peerDependencies": { - "koishi-core": "^2.0.2" + "koishi-core": "^2.1.0" }, "dependencies": { "@octokit/rest": "^18.0.4", "@octokit/webhooks": "^7.11.2", "axios": "^0.20.0", - "koishi-utils": "^3.1.2" + "koishi-utils": "^3.1.3" } } diff --git a/packages/plugin-image-search/package.json b/packages/plugin-image-search/package.json index d716f64cc5..f310d3fb98 100644 --- a/packages/plugin-image-search/package.json +++ b/packages/plugin-image-search/package.json @@ -37,12 +37,12 @@ "pixiv" ], "peerDependencies": { - "koishi-core": "^2.0.2" + "koishi-core": "^2.1.0" }, "dependencies": { "axios": "^0.20.0", "cheerio": "^1.0.0-rc.3", - "koishi-utils": "^3.1.2", + "koishi-utils": "^3.1.3", "nhentai-api": "^3.0.2" } } diff --git a/packages/plugin-mongo/package.json b/packages/plugin-mongo/package.json index ca93eee932..aa986c1bc9 100644 --- a/packages/plugin-mongo/package.json +++ b/packages/plugin-mongo/package.json @@ -1,7 +1,7 @@ { "name": "koishi-plugin-mongo", "description": "MongoDB support for Koishi", - "version": "1.0.1", + "version": "1.0.2", "main": "dist/index.js", "typings": "dist/index.d.ts", "files": [ @@ -39,7 +39,7 @@ "@types/mongodb": "^3.5.26" }, "peerDependencies": { - "koishi-core": "^2.0.2" + "koishi-core": "^2.1.0" }, "dependencies": { "mongodb": "^3.6.0" diff --git a/packages/plugin-monitor/package.json b/packages/plugin-monitor/package.json index 252e5ff96c..9f983ce346 100644 --- a/packages/plugin-monitor/package.json +++ b/packages/plugin-monitor/package.json @@ -21,9 +21,9 @@ }, "homepage": "https://github.com/koishijs/koishi#readme", "peerDependencies": { - "koishi-core": "^2.0.2" + "koishi-core": "^2.1.0" }, "dependencies": { - "koishi-utils": "^3.1.2" + "koishi-utils": "^3.1.3" } } diff --git a/packages/plugin-mysql/package.json b/packages/plugin-mysql/package.json index bcd6603d1c..7fdf4d54fc 100644 --- a/packages/plugin-mysql/package.json +++ b/packages/plugin-mysql/package.json @@ -36,10 +36,10 @@ "@types/mysql": "^2.15.15" }, "peerDependencies": { - "koishi-core": "^2.0.2" + "koishi-core": "^2.1.0" }, "dependencies": { - "koishi-utils": "^3.1.2", + "koishi-utils": "^3.1.3", "mysql": "^2.18.1" } } diff --git a/packages/plugin-puppeteer/package.json b/packages/plugin-puppeteer/package.json index cb9822fbcd..0f25216ba1 100644 --- a/packages/plugin-puppeteer/package.json +++ b/packages/plugin-puppeteer/package.json @@ -39,12 +39,12 @@ "koishi-test-utils": "^4.0.0" }, "peerDependencies": { - "koishi-core": "^2.0.2" + "koishi-core": "^2.1.0" }, "dependencies": { "chrome-finder": "^1.0.7", "pngjs": "^5.0.0", "puppeteer-core": "^5.2.1", - "koishi-utils": "^3.1.2" + "koishi-utils": "^3.1.3" } } diff --git a/packages/plugin-rss/package.json b/packages/plugin-rss/package.json index 204520bfc2..6c621bd174 100644 --- a/packages/plugin-rss/package.json +++ b/packages/plugin-rss/package.json @@ -36,13 +36,13 @@ "rss" ], "peerDependencies": { - "koishi-core": "^2.0.2" + "koishi-core": "^2.1.0" }, "devDependencies": { "koishi-test-utils": "^4.0.0" }, "dependencies": { "rss-feed-emitter": "^3.2.2", - "koishi-utils": "^3.1.2" + "koishi-utils": "^3.1.3" } } diff --git a/packages/plugin-schedule/package.json b/packages/plugin-schedule/package.json index e33b7e14ef..ef9add8ff4 100644 --- a/packages/plugin-schedule/package.json +++ b/packages/plugin-schedule/package.json @@ -34,14 +34,14 @@ "task" ], "devDependencies": { - "koishi-plugin-mongo": "^1.0.1", + "koishi-plugin-mongo": "^1.0.2", "koishi-plugin-mysql": "^2.0.0", "koishi-test-utils": "^4.0.0" }, "peerDependencies": { - "koishi-core": "^2.0.2" + "koishi-core": "^2.1.0" }, "dependencies": { - "koishi-utils": "^3.1.2" + "koishi-utils": "^3.1.3" } } diff --git a/packages/plugin-status/package.json b/packages/plugin-status/package.json index 683a1d3e8e..189eaae487 100644 --- a/packages/plugin-status/package.json +++ b/packages/plugin-status/package.json @@ -32,16 +32,16 @@ "status" ], "peerDependencies": { - "koishi-core": "^2.0.2" + "koishi-core": "^2.1.0" }, "devDependencies": { "@types/cross-spawn": "^6.0.2", - "koishi-plugin-mongo": "^1.0.1", + "koishi-plugin-mongo": "^1.0.2", "koishi-plugin-mysql": "^2.0.0", "koishi-test-utils": "^4.0.0" }, "dependencies": { "cross-spawn": "^7.0.3", - "koishi-utils": "^3.1.2" + "koishi-utils": "^3.1.3" } } diff --git a/packages/plugin-teach/package.json b/packages/plugin-teach/package.json index ddac5a519c..7682312706 100644 --- a/packages/plugin-teach/package.json +++ b/packages/plugin-teach/package.json @@ -38,15 +38,15 @@ "conversation" ], "peerDependencies": { - "koishi-core": "^2.0.2" + "koishi-core": "^2.1.0" }, "devDependencies": { - "koishi-plugin-mongo": "^1.0.1", + "koishi-plugin-mongo": "^1.0.2", "koishi-plugin-mysql": "^2.0.0", "koishi-test-utils": "^4.0.0" }, "dependencies": { - "koishi-utils": "^3.1.2", + "koishi-utils": "^3.1.3", "leven": "^3.1.0", "regexpp": "^3.1.0" } diff --git a/packages/plugin-tools/package.json b/packages/plugin-tools/package.json index 9b9f9b1a8b..931efdba23 100644 --- a/packages/plugin-tools/package.json +++ b/packages/plugin-tools/package.json @@ -25,13 +25,13 @@ "@types/qrcode": "^1.3.5" }, "peerDependencies": { - "koishi-core": "^2.0.2" + "koishi-core": "^2.1.0" }, "dependencies": { "axios": "^0.20.0", "cheerio": "^1.0.0-rc.3", "qrcode": "^1.4.4", "xml-js": "^1.6.11", - "koishi-utils": "^3.1.2" + "koishi-utils": "^3.1.3" } }