diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7570cbe3..93ced083 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1239,6 +1239,19 @@ importers: styled-components: 5.3.6_react@18.2.0 zustand: 4.1.5_react@18.2.0 + server/plugins/com.msgbyte.getui: + specifiers: + '@types/lodash': ^4.14.191 + got: ^11.8.3 + lodash: ^4.17.21 + tailchat-server-sdk: '*' + dependencies: + got: 11.8.3 + lodash: 4.17.21 + tailchat-server-sdk: link:../../packages/sdk + devDependencies: + '@types/lodash': 4.14.191 + server/plugins/com.msgbyte.github: specifiers: '@octokit/webhooks-types': ^5.4.0 @@ -17740,7 +17753,7 @@ packages: dependencies: clone-response: 1.0.3 get-stream: 5.2.0 - http-cache-semantics: 4.1.0 + http-cache-semantics: 4.1.1 keyv: 4.5.2 lowercase-keys: 2.0.0 normalize-url: 6.1.0 @@ -23441,6 +23454,23 @@ packages: responselike: 3.0.0 dev: true + /got/12.6.0: + resolution: {integrity: sha512-WTcaQ963xV97MN3x0/CbAriXFZcXCfgxVp91I+Ze6pawQOa7SgzwSx2zIJJsX+kTajMnVs0xcFD1TxZKFqhdnQ==} + engines: {node: '>=14.16'} + dependencies: + '@sindresorhus/is': 5.3.0 + '@szmarczak/http-timer': 5.0.1 + cacheable-lookup: 7.0.0 + cacheable-request: 10.2.8 + decompress-response: 6.0.0 + form-data-encoder: 2.1.4 + get-stream: 6.0.1 + http2-wrapper: 2.2.0 + lowercase-keys: 3.0.0 + p-cancelable: 3.0.0 + responselike: 3.0.0 + dev: true + /got/8.3.2: resolution: {integrity: sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw==} engines: {node: '>=4'} @@ -24027,9 +24057,6 @@ packages: resolution: {integrity: sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==} dev: true - /http-cache-semantics/4.1.0: - resolution: {integrity: sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==} - /http-cache-semantics/4.1.1: resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} @@ -29952,7 +29979,7 @@ packages: resolution: {integrity: sha512-hySwcV8RAWeAfPsXb9/HGSPn8lwDnv6fabH+obUZKX169QknRkRhPxd1yMubpKDskLFATkl3jHpNtVtDPFA0Wg==} engines: {node: '>=14.16'} dependencies: - got: 12.5.3 + got: 12.6.0 registry-auth-token: 5.0.1 registry-url: 6.0.1 semver: 7.3.8 diff --git a/server/.env.example b/server/.env.example index b9be86f4..f31c7b57 100644 --- a/server/.env.example +++ b/server/.env.example @@ -28,3 +28,8 @@ TAILCHAT_MEETING_URL= # Admin 后台密码 ADMIN_PASS=com.msgbyte.tailchat + +# GETUI Push +GETUI_APPID= +GETUI_APPKEY= +GETUI_MASTERSECRET= diff --git a/server/plugins/com.msgbyte.getui/lib/GetuiClient.ts b/server/plugins/com.msgbyte.getui/lib/GetuiClient.ts new file mode 100644 index 00000000..3a85c7b9 --- /dev/null +++ b/server/plugins/com.msgbyte.getui/lib/GetuiClient.ts @@ -0,0 +1,167 @@ +import got from 'got'; +import crypto from 'crypto'; +import _ from 'lodash'; + +function sha256(data: string) { + return crypto.createHash('sha256').update(data).digest('hex'); +} + +interface AuthResPayload { + msg: string; + code: number; + data: { + expire_time: string; + token: string; + }; +} + +interface SinglePushResPayload { + msg: string; + code: number; + data: { + [taskid: string]: { + [cid: string]: + | 'successed_offline' + | 'successed_online' + | 'successed_ignore'; + }; + }; +} + +interface AllPushResPayload { + msg: string; + code: number; + data: { + [taskid: string]: string; + }; +} + +export class GetuiClient { + token: string; + expireTime: number; + + constructor( + public appId: string, + public appkey: string, + public mastersecret: string + ) {} + + get baseUrl() { + return `https://restapi.getui.com/v2/${this.appId}`; + } + + /** + * Generate Request ID with fixed prefix and timestamp and random number + */ + generateRequestId(): string { + return ( + 'tailchat' + + String(Date.now()) + + _.padStart(String(Math.floor(Math.random() * 1e8)), 8, '0') + ); + } + + signBody() { + const timestamp = String(Date.now()); + + return { + sign: sha256(this.appkey + String(Date.now()) + this.mastersecret), + timestamp, + appkey: this.appkey, + }; + } + + private async fetchToken() { + try { + const res = await got(`${this.baseUrl}/auth`, { + method: 'POST', + headers: { + 'content-type': 'application/json;charset=utf-8', + }, + json: { + ...this.signBody(), + }, + }).json(); + + if (res.code === 0) { + this.token = res.data.token; + this.expireTime = Number(res.data.expire_time) - 60 * 1000; // 提前60s过期 + } + } catch (err) { + console.error(err); + } + } + + async getToken(): Promise { + if (!this.token || Date.now() > this.expireTime) { + // 如果token不存在或者token过期 + await this.fetchToken(); + } + + return this.token; + } + + async singlePush( + userId: string, + title: string, + body: string, + payload: {} + ): Promise { + const token = await this.getToken(); + const requestId = this.generateRequestId(); + const res = await got(`${this.baseUrl}/push/single/alias`, { + method: 'POST', + headers: { + 'content-type': 'application/json;charset=utf-8', + token, + }, + json: { + request_id: requestId, + audience: { + alias: [userId], + }, + push_message: { + notification: { + title: title, + body: body, + click_type: 'payload', + payload: JSON.stringify(payload), + }, + }, + }, + }).json(); + + return res; + } + + async allPush( + title: string, + body: string, + payload: {} + ): Promise { + const token = await this.getToken(); + console.log('token', token); + const requestId = this.generateRequestId(); + const res = await got(`${this.baseUrl}/push/all`, { + method: 'POST', + headers: { + 'content-type': 'application/json;charset=utf-8', + token, + }, + json: { + request_id: requestId, + audience: 'all', + push_message: { + notification: { + title: title, + body: body, + click_type: 'payload', + payload: JSON.stringify(payload), + }, + }, + }, + }).json(); + + return res; + } +} diff --git a/server/plugins/com.msgbyte.getui/models/log.ts b/server/plugins/com.msgbyte.getui/models/log.ts new file mode 100644 index 00000000..100d21a1 --- /dev/null +++ b/server/plugins/com.msgbyte.getui/models/log.ts @@ -0,0 +1,20 @@ +import { db } from 'tailchat-server-sdk'; +const { getModelForClass, prop, modelOptions, TimeStamps } = db; + +@modelOptions({ + options: { + customName: 'p_getui_log', + }, +}) +export class GetuiLog extends TimeStamps implements db.Base { + _id: db.Types.ObjectId; + id: string; +} + +export type GetuiLogDocument = db.DocumentType; + +const model = getModelForClass(GetuiLog); + +export type GetuiLogModel = typeof model; + +export default model; diff --git a/server/plugins/com.msgbyte.getui/package.json b/server/plugins/com.msgbyte.getui/package.json new file mode 100644 index 00000000..43aa3f20 --- /dev/null +++ b/server/plugins/com.msgbyte.getui/package.json @@ -0,0 +1,18 @@ +{ + "name": "tailchat-plugin-getui", + "version": "1.0.0", + "main": "index.js", + "author": "moonrailgun", + "description": "Support Getui Notify in chinese mainland", + "license": "MIT", + "private": true, + "scripts": {}, + "dependencies": { + "got": "^11.8.3", + "lodash": "^4.17.21", + "tailchat-server-sdk": "*" + }, + "devDependencies": { + "@types/lodash": "^4.14.191" + } +} diff --git a/server/plugins/com.msgbyte.getui/services/getui.service.ts b/server/plugins/com.msgbyte.getui/services/getui.service.ts new file mode 100644 index 00000000..6f8592d4 --- /dev/null +++ b/server/plugins/com.msgbyte.getui/services/getui.service.ts @@ -0,0 +1,20 @@ +import { TcService, TcDbService } from 'tailchat-server-sdk'; +import type { GetuiLogDocument, GetuiLogModel } from '../models/log'; + +/** + * Support Getui Notify in chinese mainland + */ +interface GetuiService + extends TcService, + TcDbService {} +class GetuiService extends TcService { + get serviceName() { + return 'plugin:com.msgbyte.getui'; + } + + onInit() { + this.registerLocalDb(require('../models/log').default); + } +} + +export default GetuiService; diff --git a/server/test/demo/getui/index.ts b/server/test/demo/getui/index.ts new file mode 100644 index 00000000..e81e8afc --- /dev/null +++ b/server/test/demo/getui/index.ts @@ -0,0 +1,13 @@ +import * as dotenv from 'dotenv'; +dotenv.config(); +import { GetuiClient } from '../../../plugins/com.msgbyte.getui/lib/GetuiClient'; + +const client = new GetuiClient( + process.env.GETUI_APPID, + process.env.GETUI_APPKEY, + process.env.GETUI_MASTERSECRET +); + +client.allPush('title', 'body', {}).then((res) => { + console.log('res', res); +});