diff --git a/README.md b/README.md index 3e4ff3d1..526747f3 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,10 @@ await ctx.start() ### Specifying protocol ```ts +import { Context } from '@satorijs/satori' +import router from '@satorijs/router' +import telegram from '@satorijs/adapter-telegram' + // your application will be listening http://localhost:8080 // and be available at https://example.com const ctx = new Context({ @@ -62,6 +66,9 @@ const ctx = new Context({ selfUrl: 'https://example.com', }) +// you need a router plugin to handle http requests +ctx.plugin(router) + ctx.plugin(telegram, { // telegram supports two ways of connection: server and polling protocol: 'server', diff --git a/adapters/dingtalk/src/http.ts b/adapters/dingtalk/src/http.ts index 57c58893..a838f386 100644 --- a/adapters/dingtalk/src/http.ts +++ b/adapters/dingtalk/src/http.ts @@ -1,4 +1,5 @@ import { Adapter, Context, Logger } from '@satorijs/satori' +import {} from '@satorijs/router' import { DingtalkBot } from './bot' import crypto from 'node:crypto' import { Message } from './types' diff --git a/adapters/kook/src/http.ts b/adapters/kook/src/http.ts index 7eec70f7..b4bad148 100644 --- a/adapters/kook/src/http.ts +++ b/adapters/kook/src/http.ts @@ -1,4 +1,5 @@ import { Adapter, Context, Logger, sanitize, Schema } from '@satorijs/satori' +import {} from '@satorijs/router' import { KookBot } from './bot' import { adaptSession } from './utils' diff --git a/adapters/lark/src/http.ts b/adapters/lark/src/http.ts index 177cf718..a2bcb88d 100644 --- a/adapters/lark/src/http.ts +++ b/adapters/lark/src/http.ts @@ -1,6 +1,6 @@ import internal from 'stream' - import { Adapter, Context, Logger, Schema } from '@satorijs/satori' +import {} from '@satorijs/router' import { FeishuBot } from './bot' import { AllEvents } from './types' diff --git a/adapters/line/src/http.ts b/adapters/line/src/http.ts index fcfe2231..ed9f0d11 100644 --- a/adapters/line/src/http.ts +++ b/adapters/line/src/http.ts @@ -1,6 +1,7 @@ import { Adapter, Context, Logger } from '@satorijs/satori' -import { LineBot } from './bot' +import {} from '@satorijs/router' import crypto from 'node:crypto' +import { LineBot } from './bot' import { WebhookRequestBody } from './types' import { adaptSessions } from './utils' import internal from 'stream' @@ -50,7 +51,7 @@ export class HttpServer extends Adapter extends Adapter { diff --git a/adapters/wechat-official/src/http.ts b/adapters/wechat-official/src/http.ts index 66fd057e..7d5bfe2f 100644 --- a/adapters/wechat-official/src/http.ts +++ b/adapters/wechat-official/src/http.ts @@ -1,4 +1,5 @@ import { Adapter, Context } from '@satorijs/satori' +import {} from '@satorijs/router' import { WechatOfficialBot } from './bot' import xml2js from 'xml2js' import { Message } from './types' diff --git a/adapters/wecom/src/http.ts b/adapters/wecom/src/http.ts index 71ab5caa..115be795 100644 --- a/adapters/wecom/src/http.ts +++ b/adapters/wecom/src/http.ts @@ -1,4 +1,5 @@ import { Adapter, Context } from '@satorijs/satori' +import {} from '@satorijs/router' import { WecomBot } from './bot' import xml2js from 'xml2js' import { Message } from './types' diff --git a/adapters/whatsapp/src/adapter.ts b/adapters/whatsapp/src/adapter.ts index 097249a7..3620bfc1 100644 --- a/adapters/whatsapp/src/adapter.ts +++ b/adapters/whatsapp/src/adapter.ts @@ -1,4 +1,5 @@ import { Adapter, Context, Logger, Quester, remove, Schema } from '@satorijs/satori' +import {} from '@satorijs/router' import { Internal } from './internal' import { WhatsAppBot } from './bot' import { WebhookBody } from './types' diff --git a/packages/router/.npmignore b/packages/router/.npmignore new file mode 100644 index 00000000..7e5fcbc1 --- /dev/null +++ b/packages/router/.npmignore @@ -0,0 +1,2 @@ +.DS_Store +tsconfig.tsbuildinfo diff --git a/packages/router/package.json b/packages/router/package.json new file mode 100644 index 00000000..29510cdb --- /dev/null +++ b/packages/router/package.json @@ -0,0 +1,56 @@ +{ + "name": "@satorijs/router", + "description": "Router plugin for cordis", + "version": "1.0.1", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "exports": { + ".": { + "node": "./lib/index.js", + "browser": "./lib/index.mjs", + "types": "./lib/index.d.ts" + }, + "./package.json": "./package.json" + }, + "files": [ + "lib", + "src" + ], + "author": "Shigma ", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/satorijs/satori.git", + "directory": "packages/router" + }, + "bugs": { + "url": "https://github.com/satorijs/satori/issues" + }, + "homepage": "https://github.com/satorijs/satori/tree/master/packages/router", + "keywords": [ + "cordis", + "router", + "http", + "ws", + "websocket", + "server", + "service" + ], + "devDependencies": { + "@types/parseurl": "^1.3.1" + }, + "dependencies": { + "@koa/router": "^10.1.1", + "@types/koa": "*", + "@types/koa__router": "*", + "@types/ws": "^8.5.6", + "koa": "^2.14.2", + "koa-bodyparser": "^4.4.1", + "parseurl": "^1.3.3", + "path-to-regexp": "^6.2.1", + "ws": "^8.14.2" + }, + "peerDependencies": { + "@satorijs/satori": "^3.0.2" + } +} diff --git a/packages/satori/src/router.ts b/packages/router/src/index.ts similarity index 65% rename from packages/satori/src/router.ts rename to packages/router/src/index.ts index 4d1f6b2e..717e2254 100644 --- a/packages/satori/src/router.ts +++ b/packages/router/src/index.ts @@ -1,4 +1,4 @@ -import { Context, Logger } from '@satorijs/core' +import { Context, Logger, Schema } from '@satorijs/core' import { MaybeArray, remove, trimSlash } from 'cosmokit' import { createServer, IncomingMessage, Server } from 'http' import { pathToRegexp } from 'path-to-regexp' @@ -35,7 +35,7 @@ export class WebSocketLayer { } accept(socket: WebSocket, request: IncomingMessage) { - if (!this.regexp.test(parseUrl(request).pathname)) return + if (!this.regexp.test(parseUrl(request)!.pathname!)) return this.clients.add(socket) socket.addEventListener('close', () => { this.clients.delete(socket) @@ -53,14 +53,14 @@ export class WebSocketLayer { } export class Router extends KoaRouter { - public _http?: Server - public _ws?: WebSocket.Server + public _http: Server + public _ws: WebSocket.Server public wsStack: WebSocketLayer[] = [] - public host: string - public port: number + public host!: string + public port!: number - constructor(ctx: Context) { + constructor(ctx: Context, public config: Router.Config) { super() // create server @@ -83,25 +83,26 @@ export class Router extends KoaRouter { socket.close() }) - ctx.root.decline(['selfUrl', 'host', 'port', 'maxPort']) + ctx.decline(['selfUrl', 'host', 'port', 'maxPort']) - if (ctx.root.config.selfUrl) { - ctx.root.config.selfUrl = trimSlash(ctx.root.config.selfUrl) + if (config.selfUrl) { + config.selfUrl = trimSlash(config.selfUrl) } ctx.on('ready', async () => { - const { host, port } = ctx.root.config + const { host = '127.0.0.1', port } = config if (!port) return - ctx.router.host = host - ctx.router.port = await listen(ctx.root.config) - ctx.router._http.listen(ctx.router.port, host) - logger.info('server listening at %c', ctx.router.selfUrl) - ctx.on('dispose', () => { - logger.info('http server closing') - ctx.router._ws?.close() - ctx.router._http?.close() - }) + this.host = host + this.port = await listen(config) + this._http.listen(this.port, host) + logger.info('server listening at %c', this.selfUrl) }, true) + + ctx.on('dispose', () => { + logger.info('http server closing') + this._ws?.close() + this._http?.close() + }) } get selfUrl() { @@ -131,4 +132,20 @@ export class Router extends KoaRouter { } } -Context.service('router', Router) +export namespace Router { + export interface Config { + host: string + port: number + maxPort?: number + selfUrl?: string + } + + export const Config: Schema = Schema.object({ + host: Schema.string().default('127.0.0.1').description('要监听的 IP 地址。如果将此设置为 `0.0.0.0` 将监听所有地址,包括局域网和公网地址。'), + port: Schema.natural().max(65535).description('要监听的初始端口号。'), + maxPort: Schema.natural().max(65535).description('允许监听的最大端口号。'), + selfUrl: Schema.string().role('link').description('应用暴露在公网的地址。'), + }) +} + +export default Router diff --git a/packages/satori/src/listen.ts b/packages/router/src/listen.ts similarity index 96% rename from packages/satori/src/listen.ts rename to packages/router/src/listen.ts index 0b0f2c7d..8026f8fb 100644 --- a/packages/satori/src/listen.ts +++ b/packages/router/src/listen.ts @@ -1,8 +1,8 @@ import net from 'net' export interface ListenOptions { - host?: string - port?: number + host: string + port: number maxPort?: number } diff --git a/packages/router/tsconfig.json b/packages/router/tsconfig.json new file mode 100644 index 00000000..6f11f324 --- /dev/null +++ b/packages/router/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib", + "strict": true, + "noImplicitAny": false, + }, + "include": [ + "src", + ], +} diff --git a/packages/satori/src/axios.ts b/packages/satori/src/axios.ts deleted file mode 100644 index afc0ead8..00000000 --- a/packages/satori/src/axios.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Quester, Schema } from '@satorijs/core' -import { base64ToArrayBuffer, defineProperty } from 'cosmokit' -import { ClientRequestArgs } from 'http' -import { WebSocket } from 'ws' -import { basename } from 'path' -import { promises as fs } from 'fs' -import { fileURLToPath } from 'url' -import FileType from 'file-type' -import createHttpProxyAgent from 'http-proxy-agent' -import createHttpsProxyAgent from 'https-proxy-agent' -import createSocksProxyAgent from 'socks-proxy-agent' - -const oldFile = Quester.prototype.file -Quester.prototype.file = async function file(this: Quester, url: string) { - // for backward compatibility - if (url.startsWith('base64://')) { - const data = base64ToArrayBuffer(url.slice(9)) - const result = await FileType.fromBuffer(data) - const filename = 'file' + (result ? '.' + result.ext : '') - return { mime: result?.mime, filename, data } - } - if (url.startsWith('file://')) { - const data = await fs.readFile(fileURLToPath(url)) - const result = await FileType.fromBuffer(data) - return { mime: result?.mime, filename: basename(url), data } - } - return oldFile.call(this, url) -} - -Quester.prototype.ws = function ws(this: Quester, url: string, options: ClientRequestArgs = {}) { - return new WebSocket(this.resolve(url), { - agent: this.agent(this.config.proxyAgent), - handshakeTimeout: this.config.timeout, - ...options, - headers: { - ...this.config.headers, - ...options.headers, - }, - }) -} - -const _prepare = Quester.prototype.prepare -Quester.prototype.prepare = function prepare(this: Quester) { - const options = _prepare.call(this) - options.httpAgent = this.agent(this.config.proxyAgent) - options.httpsAgent = this.agent(this.config.proxyAgent) - return options -} - -defineProperty(Quester, 'Config', Schema.object({ - ...Quester.Config.dict, - proxyAgent: Schema.string().description('使用的代理服务器地址。'), -}).description('请求设置')) - -Quester.defineAgent(['http'], createHttpProxyAgent) -Quester.defineAgent(['https'], createHttpsProxyAgent) -Quester.defineAgent(['socks', 'socks4', 'socks4a', 'socks5', 'socks5h'], createSocksProxyAgent) diff --git a/packages/satori/src/index.ts b/packages/satori/src/index.ts index 3ce8210b..e5722e25 100644 --- a/packages/satori/src/index.ts +++ b/packages/satori/src/index.ts @@ -1,38 +1,60 @@ -import { Context, Quester, Schema } from '@satorijs/core' -import { defineProperty } from 'cosmokit' +import { Quester, Schema } from '@satorijs/core' +import { base64ToArrayBuffer, defineProperty } from 'cosmokit' +import { ClientRequestArgs } from 'http' +import { WebSocket } from 'ws' +import { basename } from 'path' +import { promises as fs } from 'fs' +import { fileURLToPath } from 'url' +import FileType from 'file-type' +import createHttpProxyAgent from 'http-proxy-agent' +import createHttpsProxyAgent from 'https-proxy-agent' +import createSocksProxyAgent from 'socks-proxy-agent' export * from '@satorijs/core' export * from 'cosmokit' -export * from './axios' -export * from './router' -declare module '@satorijs/core' { - namespace Context { - interface Config extends Config.Network {} +const oldFile = Quester.prototype.file +Quester.prototype.file = async function file(this: Quester, url: string) { + // for backward compatibility + if (url.startsWith('base64://')) { + const data = base64ToArrayBuffer(url.slice(9)) + const result = await FileType.fromBuffer(data) + const filename = 'file' + (result ? '.' + result.ext : '') + return { mime: result?.mime, filename, data } + } + if (url.startsWith('file://')) { + const data = await fs.readFile(fileURLToPath(url)) + const result = await FileType.fromBuffer(data) + return { mime: result?.mime, filename: basename(url), data } + } + return oldFile.call(this, url) +} - namespace Config { - interface Network { - host?: string - port?: number - maxPort?: number - selfUrl?: string - } +Quester.prototype.ws = function ws(this: Quester, url: string, options: ClientRequestArgs = {}) { + return new WebSocket(this.resolve(url), { + agent: this.agent(this.config.proxyAgent), + handshakeTimeout: this.config.timeout, + ...options, + headers: { + ...this.config.headers, + ...options.headers, + }, + }) +} - interface Static extends Schema { - Network: Schema - } - } - } +const _prepare = Quester.prototype.prepare +Quester.prototype.prepare = function prepare(this: Quester) { + const options = _prepare.call(this) + options.httpAgent = this.agent(this.config.proxyAgent) + options.httpsAgent = this.agent(this.config.proxyAgent) + return options } -defineProperty(Context.Config, 'Network', Schema.object({ - host: Schema.string().default('127.0.0.1').description('要监听的 IP 地址。如果将此设置为 `0.0.0.0` 将监听所有地址,包括局域网和公网地址。'), - port: Schema.natural().max(65535).description('要监听的初始端口号。'), - maxPort: Schema.natural().max(65535).description('允许监听的最大端口号。'), - selfUrl: Schema.string().role('link').description('应用暴露在公网的地址。'), -}).description('网络设置')) +defineProperty(Quester, 'Config', Schema.object({ + ...Quester.Config.dict, + proxyAgent: Schema.string().description('使用的代理服务器地址。'), +}).description('请求设置')) -Context.Config.list.unshift(Context.Config.Network) -Context.Config.list.push(Schema.object({ - request: Quester.Config, -})) +Quester.defineAgent(['http'], createHttpProxyAgent) +Quester.defineAgent(['https'], createHttpsProxyAgent) +Quester.defineAgent(['socks', 'socks4', 'socks4a', 'socks5', 'socks5h'], createSocksProxyAgent) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 8ac85848..d822a33e 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1,4 +1,5 @@ import { camelCase, Context, Logger, Schema, Session, snakeCase, Time, Universal } from '@satorijs/satori' +import {} from '@satorijs/router' import WebSocket from 'ws' const logger = new Logger('server')