From 041a4374531cef296a68a9715adf78ecc80d6321 Mon Sep 17 00:00:00 2001 From: whyour Date: Sun, 13 Sep 2026 01:08:14 +0800 Subject: [PATCH] fix: support WPUSH notifications over gRPC --- back/config/const.ts | 1 + back/protos/api.proto | 7 ++- back/protos/api.ts | 62 +++++++++++++++++- back/protos/health.ts | 2 +- test/back/wpush-grpc.test.cjs | 115 ++++++++++++++++++++++++++++++++++ 5 files changed, 184 insertions(+), 3 deletions(-) create mode 100644 test/back/wpush-grpc.test.cjs diff --git a/back/config/const.ts b/back/config/const.ts index 8d65a877..3179af5e 100644 --- a/back/config/const.ts +++ b/back/config/const.ts @@ -51,6 +51,7 @@ export const NotificationModeStringMap = { 19: 'ntfy', 20: 'wxPusherBot', 21: 'wxPusherSpt', + 22: 'wpush', } as const; export const LINUX_DEPENDENCE_COMMAND: Record< diff --git a/back/protos/api.proto b/back/protos/api.proto index d440b3f3..4c3269eb 100644 --- a/back/protos/api.proto +++ b/back/protos/api.proto @@ -152,6 +152,7 @@ enum NotificationMode { ntfy = 19; wxPusherBot = 20; wxPusherSpt = 21; + wpush = 22; } message NotificationInfo { @@ -247,6 +248,10 @@ message NotificationInfo { optional string wxPusherBotUids = 68; optional string wxPusherSptList = 70; + + optional string wpushApiKey = 71; + optional string wpushChannel = 72; + optional string wpushTopicCode = 73; } message SystemNotifyRequest { @@ -275,4 +280,4 @@ service Api { rpc EnableCrons(EnableCronsRequest) returns (Response) {} rpc DisableCrons(DisableCronsRequest) returns (Response) {} rpc RunCrons(RunCronsRequest) returns (Response) {} -} \ No newline at end of file +} diff --git a/back/protos/api.ts b/back/protos/api.ts index 649ff335..babc07b1 100644 --- a/back/protos/api.ts +++ b/back/protos/api.ts @@ -1,7 +1,7 @@ // Code generated by protoc-gen-ts_proto. DO NOT EDIT. // versions: // protoc-gen-ts_proto v2.6.1 -// protoc v3.21.12 +// protoc v3.17.3 // source: back/protos/api.proto /* eslint-disable */ @@ -44,6 +44,7 @@ export enum NotificationMode { ntfy = 19, wxPusherBot = 20, wxPusherSpt = 21, + wpush = 22, UNRECOGNIZED = -1, } @@ -115,6 +116,9 @@ export function notificationModeFromJSON(object: any): NotificationMode { case 21: case "wxPusherSpt": return NotificationMode.wxPusherSpt; + case 22: + case "wpush": + return NotificationMode.wpush; case -1: case "UNRECOGNIZED": default: @@ -168,6 +172,8 @@ export function notificationModeToJSON(object: NotificationMode): string { return "wxPusherBot"; case NotificationMode.wxPusherSpt: return "wxPusherSpt"; + case NotificationMode.wpush: + return "wpush"; case NotificationMode.UNRECOGNIZED: default: return "UNRECOGNIZED"; @@ -400,6 +406,9 @@ export interface NotificationInfo { wxPusherBotTopicIds?: string | undefined; wxPusherBotUids?: string | undefined; wxPusherSptList?: string | undefined; + wpushApiKey?: string | undefined; + wpushChannel?: string | undefined; + wpushTopicCode?: string | undefined; } export interface SystemNotifyRequest { @@ -2967,6 +2976,9 @@ function createBaseNotificationInfo(): NotificationInfo { wxPusherBotTopicIds: undefined, wxPusherBotUids: undefined, wxPusherSptList: undefined, + wpushApiKey: undefined, + wpushChannel: undefined, + wpushTopicCode: undefined, }; } @@ -3182,6 +3194,15 @@ export const NotificationInfo: MessageFns = { if (message.wxPusherSptList !== undefined) { writer.uint32(562).string(message.wxPusherSptList); } + if (message.wpushApiKey !== undefined) { + writer.uint32(570).string(message.wpushApiKey); + } + if (message.wpushChannel !== undefined) { + writer.uint32(578).string(message.wpushChannel); + } + if (message.wpushTopicCode !== undefined) { + writer.uint32(586).string(message.wpushTopicCode); + } return writer; }, @@ -3752,6 +3773,30 @@ export const NotificationInfo: MessageFns = { message.wxPusherSptList = reader.string(); continue; } + case 71: { + if (tag !== 570) { + break; + } + + message.wpushApiKey = reader.string(); + continue; + } + case 72: { + if (tag !== 578) { + break; + } + + message.wpushChannel = reader.string(); + continue; + } + case 73: { + if (tag !== 586) { + break; + } + + message.wpushTopicCode = reader.string(); + continue; + } } if ((tag & 7) === 4 || tag === 0) { break; @@ -3845,6 +3890,9 @@ export const NotificationInfo: MessageFns = { : undefined, wxPusherBotUids: isSet(object.wxPusherBotUids) ? globalThis.String(object.wxPusherBotUids) : undefined, wxPusherSptList: isSet(object.wxPusherSptList) ? globalThis.String(object.wxPusherSptList) : undefined, + wpushApiKey: isSet(object.wpushApiKey) ? globalThis.String(object.wpushApiKey) : undefined, + wpushChannel: isSet(object.wpushChannel) ? globalThis.String(object.wpushChannel) : undefined, + wpushTopicCode: isSet(object.wpushTopicCode) ? globalThis.String(object.wpushTopicCode) : undefined, }; }, @@ -4060,6 +4108,15 @@ export const NotificationInfo: MessageFns = { if (message.wxPusherSptList !== undefined) { obj.wxPusherSptList = message.wxPusherSptList; } + if (message.wpushApiKey !== undefined) { + obj.wpushApiKey = message.wpushApiKey; + } + if (message.wpushChannel !== undefined) { + obj.wpushChannel = message.wpushChannel; + } + if (message.wpushTopicCode !== undefined) { + obj.wpushTopicCode = message.wpushTopicCode; + } return obj; }, @@ -4138,6 +4195,9 @@ export const NotificationInfo: MessageFns = { message.wxPusherBotTopicIds = object.wxPusherBotTopicIds ?? undefined; message.wxPusherBotUids = object.wxPusherBotUids ?? undefined; message.wxPusherSptList = object.wxPusherSptList ?? undefined; + message.wpushApiKey = object.wpushApiKey ?? undefined; + message.wpushChannel = object.wpushChannel ?? undefined; + message.wpushTopicCode = object.wpushTopicCode ?? undefined; return message; }, }; diff --git a/back/protos/health.ts b/back/protos/health.ts index b88f2a9c..adaf9761 100644 --- a/back/protos/health.ts +++ b/back/protos/health.ts @@ -1,7 +1,7 @@ // Code generated by protoc-gen-ts_proto. DO NOT EDIT. // versions: // protoc-gen-ts_proto v2.6.1 -// protoc v3.21.12 +// protoc v3.17.3 // source: back/protos/health.proto /* eslint-disable */ diff --git a/test/back/wpush-grpc.test.cjs b/test/back/wpush-grpc.test.cjs new file mode 100644 index 00000000..c75679af --- /dev/null +++ b/test/back/wpush-grpc.test.cjs @@ -0,0 +1,115 @@ +const assert = require('node:assert/strict'); +const path = require('node:path'); +const test = require('node:test'); +const load = require('../helpers/load-security-module.cjs'); + +const back = (file) => path.join(__dirname, '../../back', file); +const protocol = load(back('protos/api.ts')); +const constants = load(back('config/const.ts'), { + './container': { maybeSudo: (x) => x }, +}); + +test('WPUSH survives protobuf and JSON round trips without changing existing modes', () => { + assert.equal(protocol.NotificationMode.wpush, 22); + for (let value = 0; value <= 22; value++) { + const name = protocol.notificationModeToJSON(value); + assert.equal(protocol.notificationModeFromJSON(name), value); + assert.equal(constants.NotificationModeStringMap[value], name); + } + const request = protocol.SystemNotifyRequest.create({ + title: 'title', + content: 'content', + notificationInfo: { + type: 22, + wpushApiKey: 'test-key', + wpushChannel: 'feishu', + wpushTopicCode: 'test-topic', + }, + }); + const decoded = protocol.SystemNotifyRequest.decode( + protocol.SystemNotifyRequest.encode(request).finish(), + ); + assert.equal(decoded.notificationInfo.wpushApiKey, 'test-key'); + assert.equal(decoded.notificationInfo.wpushChannel, 'feishu'); + assert.equal(decoded.notificationInfo.wpushTopicCode, 'test-topic'); + assert.deepEqual(decoded, request); + assert.deepEqual( + protocol.SystemNotifyRequest.fromJSON( + protocol.SystemNotifyRequest.toJSON(request), + ), + request, + ); +}); + +test('decoded SystemNotify requests reach WPUSH with explicit or saved configuration', async () => { + const calls = []; + const decorators = { Service: () => (x) => x, Inject: () => () => {} }; + const NotificationService = load(back('services/notify.ts'), { + typedi: decorators, + './user': {}, + '../config/util': {}, + '../shared/i18n': { t: (x) => x }, + '../config/http': { + httpClient: { + post: async (url, options) => { + calls.push({ url, body: options.json }); + return { code: 0 }; + }, + }, + }, + }).default; + const SystemService = load(back('services/system.ts'), { + typedi: decorators, + '../config': {}, + '../config/const': constants, + '../config/util': {}, + '../data/dependence': {}, + '../data/system': {}, + '../shared/pLimit': {}, + '../shared/i18n': { t: (x) => x }, + './notify': {}, + './schedule': {}, + './sock': {}, + }).default; + const notification = new NotificationService(); + notification.userService = { + getNotificationMode: async () => ({ + type: 'wpush', + wpushApiKey: 'saved-key', + }), + }; + const system = new SystemService(); + system.notificationService = notification; + + for (const info of [ + { + type: 22, + wpushApiKey: 'explicit-key', + wpushChannel: 'feishu', + wpushTopicCode: 'topic', + }, + { type: 22, wpushApiKey: 'explicit-key' }, + undefined, + ]) { + const decoded = protocol.SystemNotifyRequest.decode( + protocol.SystemNotifyRequest.encode( + protocol.SystemNotifyRequest.create({ + title: 'title', + content: 'content', + notificationInfo: info, + }), + ).finish(), + ); + assert.equal((await system.notify(decoded)).code, 200); + assert.deepEqual(calls.at(-1), { + url: 'https://api.wpush.cn/api/v1/send', + body: { + apikey: info ? 'explicit-key' : 'saved-key', + title: 'title', + content: 'content', + channel: info?.wpushChannel || 'wechat', + ...(info?.wpushTopicCode ? { topic_code: 'topic' } : {}), + }, + }); + } +});