diff --git a/libs/dingtalk-bot/.gitignore b/libs/dingtalk-bot/.gitignore new file mode 100644 index 00000000..a65b4177 --- /dev/null +++ b/libs/dingtalk-bot/.gitignore @@ -0,0 +1 @@ +lib diff --git a/libs/dingtalk-bot/package.json b/libs/dingtalk-bot/package.json new file mode 100644 index 00000000..54b412e6 --- /dev/null +++ b/libs/dingtalk-bot/package.json @@ -0,0 +1,23 @@ +{ + "name": "@opensumi/dingtalk-bot", + "version": "1.0.3", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "test": "jest", + "prepublishOnly": "npm run build" + }, + "dependencies": { + "@opensumi/ide-utils": "^2.23.1", + "@opensumi/workers-utils": "workspace:^", + "dotenv": "^16.3.1", + "magic-string": "^0.30.3", + "mri": "^1.2.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + } +} diff --git a/libs/dingtalk-bot/src/index.ts b/libs/dingtalk-bot/src/index.ts new file mode 100644 index 00000000..b4e26231 --- /dev/null +++ b/libs/dingtalk-bot/src/index.ts @@ -0,0 +1,57 @@ +import { doSign } from './utils'; + +function validateTimestamp(timestamp: string) { + try { + const _tsNumber = parseInt(timestamp, 10); + const now = Date.now(); + const diff = Math.abs(now - _tsNumber); + if (diff <= 60 * 60 * 1000) { + return true; + } + return false; + } catch (err) { + return false; + } +} + +/** + * 验证接收到的钉钉消息的签名是否合法 + */ +async function checkSign( + timestamp: string, + sign: string, + token: string, +): Promise { + if (!token) { + return; + } + + // 开发者需对header中的timestamp和sign进行验证 + // 以判断是否是来自钉钉的合法请求,避免其他仿冒钉钉调用开发者的HTTPS服务传送数据,具体验证逻辑如下: + // 1. timestamp 与系统当前时间戳如果相差1小时以上,则认为是非法的请求。 + const tsValid = validateTimestamp(timestamp); + if (!tsValid) { + return 'timestamp is invalid'; + } + const calculatedSign = await doSign(token, timestamp + '\n' + token); + // 2. sign 与开发者自己计算的结果不一致,则认为是非法的请求。 + if (calculatedSign !== sign) { + return 'sign is invalid'; + } + return; +} + +export async function verifyMessage(headers: Headers, token: string) { + console.log(`verifyMessage ~ headers`, headers); + const timestamp = headers.get('timestamp') as string; + const sign = headers.get('sign') as string; + if (timestamp && sign) { + const errMessage = await checkSign(timestamp, sign, token); + if (errMessage) { + return errMessage; + } + } else { + // it seem that dingtalk will not send timestamp and sign in header + // return 'not valid ding msg, missing validation field'; + } +} diff --git a/libs/dingtalk-bot/src/types.ts b/libs/dingtalk-bot/src/types.ts new file mode 100644 index 00000000..107b0128 --- /dev/null +++ b/libs/dingtalk-bot/src/types.ts @@ -0,0 +1,140 @@ +export interface TextMessage { + content: string; +} + +export interface AtUsersItem { + dingtalkId: string; + staffId?: string; +} + +export enum ConversationType { + private = '1', + group = '2', +} + +export interface Message { + msgtype: string; + text: TextMessage; + msgId: string; + createAt: number; + conversationType: ConversationType; + conversationId: string; + senderId: string; + senderNick: string; + senderCorpId?: string; + sessionWebhook: string; + sessionWebhookExpiredTime: number; + isAdmin: boolean; +} + +export interface PrivateMessage extends Message { + chatbotCorpId: string; + senderStaffId?: string; + conversationType: ConversationType.private; +} + +export interface GroupMessage extends Message { + atUsers: AtUsersItem[]; + conversationType: ConversationType.group; + conversationTitle: string; + isInAtList: boolean; +} + +export function isPrivateMessage(message: any): message is PrivateMessage { + return ( + message && + message.conversationType && + message.conversationType === ConversationType.private + ); +} + +export function isGroupMessage(message: any): message is GroupMessage { + return ( + message && + message.conversationType && + message.conversationType === ConversationType.group + ); +} + +export interface At { + at: { + atDingtalkIds: string[]; + }; +} + +export type Base = Record & { msgtype: T }; + +export type Image = Base< + 'image', + { + picURL: string; + } +>; + +export type Markdown = Base< + 'markdown', + { + title: string; + text: string; + } +>; +export type Text = Base< + 'text', + { + content: string; + } +>; + +export type SendMessage = Image | Markdown | Text; + +export function atDingtalkIds(...atDingtalkIds: string[]): At { + return { + at: { + atDingtalkIds, + }, + }; +} + +export function text(content: string): Text { + return { + msgtype: 'text', + text: { + content: content ? content.toString().trim() : '', + }, + }; +} + +export function compose(...objects: any[]) { + return Object.assign({}, ...objects); +} + +export function markdown(title: string, text: string): Markdown { + return { + msgtype: 'markdown', + markdown: { + title, + text, + }, + }; +} + +export function image(url: string): Image { + return { + msgtype: 'image', + image: { picURL: url }, + }; +} + +export function extension(extension: Record) { + return { + msgtype: 'extension', + extension, + }; +} + +export function code(language: string, code: string) { + return { + ...extension({ text_type: 'code_snippet', code_language: language }), + ...text(code), + }; +} diff --git a/src/im/utils.ts b/libs/dingtalk-bot/src/utils.ts similarity index 95% rename from src/im/utils.ts rename to libs/dingtalk-bot/src/utils.ts index 15bbafa6..6863ed19 100644 --- a/src/im/utils.ts +++ b/libs/dingtalk-bot/src/utils.ts @@ -1,4 +1,4 @@ -import { sign } from '@/runtime/cfworker/crypto'; +import { sign } from '@opensumi/workers-utils/lib/crypto'; export async function doSign(secret: string, content: string): Promise { const mac = await sign(secret, content); diff --git a/libs/dingtalk-bot/tsconfig.json b/libs/dingtalk-bot/tsconfig.json new file mode 100644 index 00000000..fe487d82 --- /dev/null +++ b/libs/dingtalk-bot/tsconfig.json @@ -0,0 +1,109 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + "baseUrl": "./src" /* Specify the base directory to resolve non-relative module names. */, + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./lib" /* Specify an output folder for all emitted files. */, + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/libs/workers-utils/.gitignore b/libs/workers-utils/.gitignore new file mode 100644 index 00000000..a65b4177 --- /dev/null +++ b/libs/workers-utils/.gitignore @@ -0,0 +1 @@ +lib diff --git a/libs/workers-utils/package.json b/libs/workers-utils/package.json new file mode 100644 index 00000000..b8e452a9 --- /dev/null +++ b/libs/workers-utils/package.json @@ -0,0 +1,22 @@ +{ + "name": "@opensumi/workers-utils", + "version": "1.0.3", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "test": "jest", + "prepublishOnly": "npm run build" + }, + "dependencies": { + "@opensumi/ide-utils": "^2.23.1", + "dotenv": "^16.3.1", + "magic-string": "^0.30.3", + "mri": "^1.2.0" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + } +} diff --git a/src/runtime/cfworker/crypto.ts b/libs/workers-utils/src/crypto.ts similarity index 100% rename from src/runtime/cfworker/crypto.ts rename to libs/workers-utils/src/crypto.ts diff --git a/libs/workers-utils/src/index.ts b/libs/workers-utils/src/index.ts new file mode 100644 index 00000000..cb0ff5c3 --- /dev/null +++ b/libs/workers-utils/src/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/libs/workers-utils/tsconfig.json b/libs/workers-utils/tsconfig.json new file mode 100644 index 00000000..fe487d82 --- /dev/null +++ b/libs/workers-utils/tsconfig.json @@ -0,0 +1,109 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + "baseUrl": "./src" /* Specify the base directory to resolve non-relative module names. */, + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./lib" /* Specify an output folder for all emitted files. */, + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/package.json b/package.json index 6894fd18..251d9687 100644 --- a/package.json +++ b/package.json @@ -69,9 +69,11 @@ "@octokit/webhooks": "^12.0.3", "@opensumi/bot-commander": "workspace:^", "@opensumi/cfworker-builder": "workspace:^", + "@opensumi/dingtalk-bot": "workspace:^", "@opensumi/ide-utils": "^2.23.1", "@opensumi/octo-service": "workspace:^", "@opensumi/workers-kv": "workspace:^", + "@opensumi/workers-utils": "workspace:^", "chatgpt": "^5.0.9", "eventemitter3": "^5.0.1", "hono": "^3.6.0", diff --git a/src/ai/conversation/kvManager.ts b/src/ai/conversation/kvManager.ts index 46dabc31..d179959e 100644 --- a/src/ai/conversation/kvManager.ts +++ b/src/ai/conversation/kvManager.ts @@ -1,7 +1,7 @@ import { ChatMessage } from 'chatgpt'; -import { Message } from '@/im/types'; import { KVManager, DingConversation } from '@/kv'; +import { Message } from '@opensumi/dingtalk-bot/lib/types'; import { IConversationSetting } from './types'; diff --git a/src/api/controllers/ding.ts b/src/api/controllers/ding.ts index 0b695697..f383560c 100644 --- a/src/api/controllers/ding.ts +++ b/src/api/controllers/ding.ts @@ -1,6 +1,7 @@ import { Session } from '@/im/bot'; -import { DingBotAdapter, verifyMessage } from '@/im/ding/bot'; +import { DingBotAdapter } from '@/im/ding/bot'; import { DingKVManager } from '@/kv/ding'; +import { verifyMessage } from '@opensumi/dingtalk-bot/lib'; export function route(hono: THono) { hono.post('/ding/:id', async (c) => { diff --git a/src/github/utils.ts b/src/github/utils.ts index 330b432b..36520486 100644 --- a/src/github/utils.ts +++ b/src/github/utils.ts @@ -12,8 +12,8 @@ import { toMarkdown } from 'mdast-util-to-markdown'; import { gfm } from 'micromark-extension-gfm'; import { markdown } from '@/im/message'; -import { send } from '@/im/utils'; import { ISetting } from '@/kv/types'; +import { send } from '@opensumi/dingtalk-bot/lib/utils'; import { MarkdownContent } from './types'; diff --git a/src/im/commands/common.ts b/src/im/commands/common.ts index 86c09588..e9063df4 100644 --- a/src/im/commands/common.ts +++ b/src/im/commands/common.ts @@ -1,8 +1,7 @@ import { IDingInfo } from '@/kv/types'; import { StringBuilder } from '@/utils'; import { startsWith } from '@opensumi/bot-commander'; - -import { code } from '../message'; +import { code } from '@opensumi/dingtalk-bot/lib/types'; import { IMCommandCenter } from './types'; diff --git a/src/im/commands/github.ts b/src/im/commands/github.ts index e027a4fa..69259960 100644 --- a/src/im/commands/github.ts +++ b/src/im/commands/github.ts @@ -2,8 +2,9 @@ import { App } from '@/github/app'; import { render } from '@/github/render'; import { contentToMarkdown, parseGitHubUrl } from '@/github/utils'; import { startsWith } from '@opensumi/bot-commander'; +import { code } from '@opensumi/dingtalk-bot/lib/types'; -import { code, markdown } from '../message'; +import { markdown } from '../message'; import { IBotAdapter } from '../types'; import { ISSUE_REGEX, REPO_REGEX } from './constants'; diff --git a/src/im/commands/types.ts b/src/im/commands/types.ts index 591ed23a..ea00a333 100644 --- a/src/im/commands/types.ts +++ b/src/im/commands/types.ts @@ -1,8 +1,8 @@ import { App } from '@/github/app'; import { CommandCenter, IArgv } from '@opensumi/bot-commander'; +import { Message } from '@opensumi/dingtalk-bot/lib/types'; -import { IBotAdapter, Message } from '../types'; - +import { IBotAdapter } from '../types'; export interface Context { message: Message; /** diff --git a/src/im/ding/bot.ts b/src/im/ding/bot.ts index f8223c6f..fd518593 100644 --- a/src/im/ding/bot.ts +++ b/src/im/ding/bot.ts @@ -3,76 +3,20 @@ import { Context } from 'hono'; import { ConversationKVManager } from '@/ai/conversation/kvManager'; import Environment from '@/env'; import { initApp, App } from '@/github/app'; -import { doSign, send } from '@/im/utils'; import { DingKVManager } from '@/kv/ding'; import { GitHubKVManager } from '@/kv/github'; import { IDingBotSetting } from '@/kv/types'; +import * as types from '@opensumi/dingtalk-bot/lib/types'; +import { Message } from '@opensumi/dingtalk-bot/lib/types'; +import { send } from '@opensumi/dingtalk-bot/lib/utils'; import { cc } from '../commands'; -import { SendMessage, compose, text as textWrapper } from '../message'; -import { IBotAdapter, Message } from '../types'; +import { IBotAdapter } from '../types'; function prepare(s: string) { return s.toString().trim(); } -function validateTimestamp(timestamp: string) { - try { - const _tsNumber = parseInt(timestamp, 10); - const now = Date.now(); - const diff = Math.abs(now - _tsNumber); - if (diff <= 60 * 60 * 1000) { - return true; - } - return false; - } catch (err) { - return false; - } -} - -/** - * 验证接收到的钉钉消息的签名是否合法 - */ -async function checkSign( - timestamp: string, - sign: string, - token: string, -): Promise { - if (!token) { - return; - } - - // 开发者需对header中的timestamp和sign进行验证 - // 以判断是否是来自钉钉的合法请求,避免其他仿冒钉钉调用开发者的HTTPS服务传送数据,具体验证逻辑如下: - // 1. timestamp 与系统当前时间戳如果相差1小时以上,则认为是非法的请求。 - const tsValid = validateTimestamp(timestamp); - if (!tsValid) { - return 'timestamp is invalid'; - } - const calculatedSign = await doSign(token, timestamp + '\n' + token); - // 2. sign 与开发者自己计算的结果不一致,则认为是非法的请求。 - if (calculatedSign !== sign) { - return 'sign is invalid'; - } - return; -} - -export async function verifyMessage(headers: Headers, token: string) { - console.log(`verifyMessage ~ headers`, headers); - const timestamp = headers.get('timestamp') as string; - const sign = headers.get('sign') as string; - if (timestamp && sign) { - const errMessage = await checkSign(timestamp, sign, token); - if (errMessage) { - return errMessage; - } - } else { - // 好像企业内部的这个机器人发的 headers 不带这俩字段了,这里的逻辑可以暂时不用了 - console.error('skip msg verify, missing validation field'); - // return error(403, 'not valid ding msg, missing validation field'); - } -} - export class DingBotAdapter implements IBotAdapter { githubKVManager: GitHubKVManager; conversationKVManager: ConversationKVManager; @@ -127,10 +71,10 @@ export class DingBotAdapter implements IBotAdapter { } async replyText(text: string, contentExtra: Record = {}) { - await this.reply(compose(textWrapper(text), contentExtra)); + await this.reply(types.compose(types.text(text), contentExtra)); } - async reply(content: SendMessage) { + async reply(content: types.SendMessage) { console.log(`DingBot ~ reply:`, JSON.stringify(content)); await send(content, this.msg.sessionWebhook); } diff --git a/src/im/index.ts b/src/im/index.ts index 6d5a6ef4..2a345aac 100644 --- a/src/im/index.ts +++ b/src/im/index.ts @@ -1,2 +1,2 @@ export * from './types'; -export * from './utils'; +export * from '../../libs/dingtalk-bot/src/utils'; diff --git a/src/im/message.ts b/src/im/message.ts index df52b2a5..32c64e6c 100644 --- a/src/im/message.ts +++ b/src/im/message.ts @@ -1,56 +1,7 @@ import { standardizeMarkdown } from '@/github/utils'; +import { Markdown } from '@opensumi/dingtalk-bot/lib/types'; -export interface At { - at: { - atDingtalkIds: string[]; - }; -} - -export type Base = Record & { msgtype: T }; - -export type Image = Base< - 'image', - { - picURL: string; - } ->; - -export type Markdown = Base< - 'markdown', - { - title: string; - text: string; - } ->; -export type Text = Base< - 'text', - { - content: string; - } ->; - -export type SendMessage = Image | Markdown | Text; - -export function atDingtalkIds(...atDingtalkIds: string[]): At { - return { - at: { - atDingtalkIds, - }, - }; -} - -export function text(content: string): Text { - return { - msgtype: 'text', - text: { - content: content ? content.toString().trim() : '', - }, - }; -} - -export function compose(...objects: any[]) { - return Object.assign({}, ...objects); -} +// export * from '@opensumi/dingtalk-bot/lib/types'; export function markdown(title: string, text: string): Markdown { return { @@ -61,24 +12,3 @@ export function markdown(title: string, text: string): Markdown { }, }; } - -export function image(url: string): Image { - return { - msgtype: 'image', - image: { picURL: url }, - }; -} - -export function extension(extension: Record) { - return { - msgtype: 'extension', - extension, - }; -} - -export function code(language: string, code: string) { - return { - ...extension({ text_type: 'code_snippet', code_language: language }), - ...text(code), - }; -} diff --git a/src/im/types.ts b/src/im/types.ts index 41a0e260..23472300 100644 --- a/src/im/types.ts +++ b/src/im/types.ts @@ -1,65 +1,6 @@ import type { ConversationKVManager } from '@/ai/conversation/kvManager'; import type { DingKVManager } from '@/kv/ding'; - -import { SendMessage } from './message'; - -export interface TextMessage { - content: string; -} - -export interface AtUsersItem { - dingtalkId: string; - staffId?: string; -} - -export enum ConversationType { - private = '1', - group = '2', -} - -export interface Message { - msgtype: string; - text: TextMessage; - msgId: string; - createAt: number; - conversationType: ConversationType; - conversationId: string; - senderId: string; - senderNick: string; - senderCorpId?: string; - sessionWebhook: string; - sessionWebhookExpiredTime: number; - isAdmin: boolean; -} - -export interface PrivateMessage extends Message { - chatbotCorpId: string; - senderStaffId?: string; - conversationType: ConversationType.private; -} - -export interface GroupMessage extends Message { - atUsers: AtUsersItem[]; - conversationType: ConversationType.group; - conversationTitle: string; - isInAtList: boolean; -} - -export function isPrivateMessage(message: any): message is PrivateMessage { - return ( - message && - message.conversationType && - message.conversationType === ConversationType.private - ); -} - -export function isGroupMessage(message: any): message is GroupMessage { - return ( - message && - message.conversationType && - message.conversationType === ConversationType.group - ); -} +import { SendMessage } from '@opensumi/dingtalk-bot/lib/types'; export interface IBotAdapter { id: string; diff --git a/tsconfig.json b/tsconfig.json index 1f407130..50d27a2c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,7 +23,8 @@ "./test/**/*.ts", "./scripts/**/*.ts", "./typings/**/*.d.ts", - "libs/octo-service/src/types.ts" + "libs/octo-service/src/types.ts", + "libs/dingtalk-bot/src/utils.ts" ], "exclude": ["node_modules"] } diff --git a/yarn.lock b/yarn.lock index e0910af8..bd76ac66 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2150,6 +2150,18 @@ __metadata: languageName: unknown linkType: soft +"@opensumi/dingtalk-bot@workspace:^, @opensumi/dingtalk-bot@workspace:libs/dingtalk-bot": + version: 0.0.0-use.local + resolution: "@opensumi/dingtalk-bot@workspace:libs/dingtalk-bot" + dependencies: + "@opensumi/ide-utils": ^2.23.1 + "@opensumi/workers-utils": "workspace:^" + dotenv: ^16.3.1 + magic-string: ^0.30.3 + mri: ^1.2.0 + languageName: unknown + linkType: soft + "@opensumi/ide-utils@npm:^2.23.1": version: 2.26.2 resolution: "@opensumi/ide-utils@npm:2.26.2" @@ -2185,6 +2197,17 @@ __metadata: languageName: unknown linkType: soft +"@opensumi/workers-utils@workspace:^, @opensumi/workers-utils@workspace:libs/workers-utils": + version: 0.0.0-use.local + resolution: "@opensumi/workers-utils@workspace:libs/workers-utils" + dependencies: + "@opensumi/ide-utils": ^2.23.1 + dotenv: ^16.3.1 + magic-string: ^0.30.3 + mri: ^1.2.0 + languageName: unknown + linkType: soft + "@parcel/watcher@npm:2.0.4": version: 2.0.4 resolution: "@parcel/watcher@npm:2.0.4" @@ -5724,9 +5747,11 @@ __metadata: "@octokit/webhooks-examples": ^7.3.0 "@opensumi/bot-commander": "workspace:^" "@opensumi/cfworker-builder": "workspace:^" + "@opensumi/dingtalk-bot": "workspace:^" "@opensumi/ide-utils": ^2.23.1 "@opensumi/octo-service": "workspace:^" "@opensumi/workers-kv": "workspace:^" + "@opensumi/workers-utils": "workspace:^" "@types/jest": ^29.4.0 "@types/lodash": ^4.14.191 "@types/node": ^18.11.18