koishi-plugin-subscription 0.0.6 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/config.d.ts +16 -0
- package/lib/index.d.ts +6 -64
- package/lib/index.js +562 -373
- package/lib/render.d.ts +14 -0
- package/lib/service.d.ts +60 -0
- package/package.json +1 -1
package/lib/config.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Dict, Schema } from 'koishi';
|
|
2
|
+
export interface AppConfig {
|
|
3
|
+
alias: string[];
|
|
4
|
+
themeColor?: string;
|
|
5
|
+
allowedAccounts: {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
alias: string;
|
|
9
|
+
}[];
|
|
10
|
+
}
|
|
11
|
+
export interface Config {
|
|
12
|
+
apps: Dict<AppConfig>;
|
|
13
|
+
minAuthority: number;
|
|
14
|
+
mode: 'noAdmin' | 'and' | 'or';
|
|
15
|
+
}
|
|
16
|
+
export declare const Config: Schema<Config>;
|
package/lib/index.d.ts
CHANGED
|
@@ -1,69 +1,11 @@
|
|
|
1
|
-
import { Context
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
groupId: string;
|
|
7
|
-
createdAt: Date;
|
|
8
|
-
}
|
|
9
|
-
interface AppConfig {
|
|
10
|
-
alias: string[];
|
|
11
|
-
allowedAccounts: {
|
|
12
|
-
id: string;
|
|
13
|
-
name: string;
|
|
14
|
-
alias: string;
|
|
15
|
-
}[];
|
|
16
|
-
}
|
|
17
|
-
export interface Config {
|
|
18
|
-
apps: Dict<AppConfig>;
|
|
19
|
-
minAuthority: number;
|
|
20
|
-
mode: "noAdmin" | "and" | "or";
|
|
21
|
-
}
|
|
22
|
-
export declare const Config: Schema<Config>;
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
|
+
import { Config } from './config';
|
|
3
|
+
import type { Config as SubscriptionConfig } from './config';
|
|
4
|
+
export { Config };
|
|
5
|
+
export { SubscriptionService } from './service';
|
|
23
6
|
export declare const name = "subscription";
|
|
24
7
|
export declare const inject: {
|
|
25
8
|
required: string[];
|
|
26
9
|
optional: string[];
|
|
27
10
|
};
|
|
28
|
-
declare
|
|
29
|
-
interface Context {
|
|
30
|
-
subscription: SubscriptionService;
|
|
31
|
-
}
|
|
32
|
-
interface Tables {
|
|
33
|
-
subscription_service: SubscriptionItem;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
declare class SubscriptionService extends Service {
|
|
37
|
-
readonly tableName = "subscription_service";
|
|
38
|
-
subsConfig: Config;
|
|
39
|
-
appMap: Map<string, string>;
|
|
40
|
-
accountMap: Map<string, string>;
|
|
41
|
-
constructor(ctx: Context, config: Config);
|
|
42
|
-
initMap(): void;
|
|
43
|
-
private getApp;
|
|
44
|
-
getAccount(app: string, account: string): string;
|
|
45
|
-
getName(app: string, account: string): string;
|
|
46
|
-
getAvailableAccounts(app: string): string[];
|
|
47
|
-
getSubscribedGroups(app: string, account: string): Promise<string[]>;
|
|
48
|
-
broadcast(app: string, account: string, content: Fragment): Promise<void>;
|
|
49
|
-
private getSelfIds;
|
|
50
|
-
private getAssignedChannels;
|
|
51
|
-
broadcastForward(app: string, account: string, content: Fragment): Promise<any[]>;
|
|
52
|
-
addSubscription(app: string, account: string, groupId: string): Promise<{
|
|
53
|
-
status: boolean;
|
|
54
|
-
msg: string;
|
|
55
|
-
}>;
|
|
56
|
-
removeSubscription(app: string, account: string, groupId: string): Promise<boolean>;
|
|
57
|
-
getGroupSubscriptions(groupId: string): Promise<SubscriptionItem[]>;
|
|
58
|
-
getAllSubscriptions(): Promise<SubscriptionItem[]>;
|
|
59
|
-
getSubscriptionStats(): Promise<{
|
|
60
|
-
app: string;
|
|
61
|
-
account: string;
|
|
62
|
-
count: number;
|
|
63
|
-
}[]>;
|
|
64
|
-
private authCheck;
|
|
65
|
-
private getChannelId;
|
|
66
|
-
private registerCommands;
|
|
67
|
-
}
|
|
68
|
-
export declare function apply(ctx: Context, config: Config): void;
|
|
69
|
-
export {};
|
|
11
|
+
export declare function apply(ctx: Context, config: SubscriptionConfig): void;
|
package/lib/index.js
CHANGED
|
@@ -21,33 +21,21 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var src_exports = {};
|
|
22
22
|
__export(src_exports, {
|
|
23
23
|
Config: () => Config,
|
|
24
|
+
SubscriptionService: () => SubscriptionService,
|
|
24
25
|
apply: () => apply,
|
|
25
26
|
inject: () => inject,
|
|
26
27
|
name: () => name
|
|
27
28
|
});
|
|
28
29
|
module.exports = __toCommonJS(src_exports);
|
|
29
|
-
var import_koishi = require("koishi");
|
|
30
|
-
|
|
31
|
-
// src/retry.ts
|
|
32
|
-
async function retry(retries, fn, delay = 500) {
|
|
33
|
-
try {
|
|
34
|
-
return await fn();
|
|
35
|
-
} catch (err) {
|
|
36
|
-
console.log(`剩余${retries}次尝试`, err);
|
|
37
|
-
if (retries <= 1) throw err;
|
|
38
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
39
|
-
return retry(retries - 1, fn, delay * 2);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
__name(retry, "retry");
|
|
43
30
|
|
|
44
|
-
// src/
|
|
45
|
-
var
|
|
31
|
+
// src/config.ts
|
|
32
|
+
var import_koishi = require("koishi");
|
|
46
33
|
var Config = import_koishi.Schema.intersect([
|
|
47
34
|
import_koishi.Schema.object({
|
|
48
35
|
apps: import_koishi.Schema.dict(
|
|
49
36
|
import_koishi.Schema.object({
|
|
50
37
|
alias: import_koishi.Schema.array(import_koishi.Schema.string()).role("table").description("应用别名,如果重复,后面会覆盖前面的"),
|
|
38
|
+
themeColor: import_koishi.Schema.string().role("color").description("应用主题色,用于订阅列表展示,建议使用十六进制颜色,如 #1d9bf0"),
|
|
51
39
|
allowedAccounts: import_koishi.Schema.array(
|
|
52
40
|
import_koishi.Schema.object({
|
|
53
41
|
id: import_koishi.Schema.string().required().description("账号id"),
|
|
@@ -67,17 +55,388 @@ var Config = import_koishi.Schema.intersect([
|
|
|
67
55
|
]).default("or").role("radio").description("权限模式")
|
|
68
56
|
}).description("权限配置")
|
|
69
57
|
]);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
58
|
+
|
|
59
|
+
// src/service.tsx
|
|
60
|
+
var import_koishi2 = require("koishi");
|
|
61
|
+
|
|
62
|
+
// src/render.ts
|
|
63
|
+
function generateSubscriptionHTML(groupId, availableAccounts, subscribedByApp, appThemeColors = {}, options = {}) {
|
|
64
|
+
const apps = createAppViews(availableAccounts, subscribedByApp, appThemeColors);
|
|
65
|
+
const showTitle = options.showTitle ?? true;
|
|
66
|
+
return `
|
|
67
|
+
<!DOCTYPE html>
|
|
68
|
+
<html lang="zh-CN">
|
|
69
|
+
<head>
|
|
70
|
+
<meta charset="UTF-8">
|
|
71
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
72
|
+
<title>订阅状态总览</title>
|
|
73
|
+
<style>
|
|
74
|
+
:root {
|
|
75
|
+
--bg: #f5f7fb;
|
|
76
|
+
--panel: #ffffff;
|
|
77
|
+
--text: #1f2328;
|
|
78
|
+
--muted: #69727f;
|
|
79
|
+
--soft: #f8fafc;
|
|
80
|
+
--line: #d9e0e8;
|
|
81
|
+
--line-soft: #e8edf3;
|
|
82
|
+
--ok: #157347;
|
|
83
|
+
--ok-bg: #e6f4ec;
|
|
84
|
+
--ok-line: #a8d8bd;
|
|
85
|
+
--off: #8a95a3;
|
|
86
|
+
--off-bg: #f2f4f7;
|
|
87
|
+
--accent: #315f8a;
|
|
88
|
+
--app-color: var(--accent);
|
|
89
|
+
--app-color-soft: rgba(49, 95, 138, 0.22);
|
|
90
|
+
--app-color-faint: rgba(49, 95, 138, 0.11);
|
|
91
|
+
--app-color-wash: rgba(49, 95, 138, 0.06);
|
|
92
|
+
--alias-bg: #fff5dc;
|
|
93
|
+
--alias-text: #745416;
|
|
94
|
+
--id-bg: #eef2f7;
|
|
95
|
+
--shadow: rgba(18, 27, 38, 0.05);
|
|
96
|
+
}
|
|
97
|
+
* {
|
|
98
|
+
box-sizing: border-box;
|
|
99
|
+
}
|
|
100
|
+
html,
|
|
101
|
+
body {
|
|
102
|
+
margin: 0;
|
|
103
|
+
min-height: 100%;
|
|
104
|
+
background: var(--bg);
|
|
105
|
+
color: var(--text);
|
|
106
|
+
font-family: Inter, "Segoe UI", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
|
|
107
|
+
}
|
|
108
|
+
body {
|
|
109
|
+
padding: 24px;
|
|
110
|
+
}
|
|
111
|
+
.page {
|
|
112
|
+
width: 1392px;
|
|
113
|
+
margin: 0 auto;
|
|
114
|
+
}
|
|
115
|
+
.title {
|
|
116
|
+
margin: 0 0 18px;
|
|
117
|
+
color: #17202b;
|
|
118
|
+
font-size: 32px;
|
|
119
|
+
line-height: 1.2;
|
|
120
|
+
font-weight: 820;
|
|
121
|
+
letter-spacing: 0;
|
|
122
|
+
}
|
|
123
|
+
.app {
|
|
124
|
+
margin-bottom: 22px;
|
|
125
|
+
overflow: hidden;
|
|
126
|
+
}
|
|
127
|
+
.app-head {
|
|
128
|
+
display: flex;
|
|
129
|
+
align-items: center;
|
|
130
|
+
min-height: 34px;
|
|
131
|
+
padding: 0 4px;
|
|
132
|
+
}
|
|
133
|
+
.app-name {
|
|
134
|
+
display: inline-flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
gap: 10px;
|
|
137
|
+
color: var(--app-color);
|
|
138
|
+
font-size: 26px;
|
|
139
|
+
line-height: 1.2;
|
|
140
|
+
font-weight: 820;
|
|
141
|
+
}
|
|
142
|
+
.app-name::before {
|
|
143
|
+
content: "";
|
|
144
|
+
width: 10px;
|
|
145
|
+
height: 10px;
|
|
146
|
+
border-radius: 999px;
|
|
147
|
+
background: var(--app-color);
|
|
148
|
+
box-shadow: 0 0 0 5px var(--app-color-faint);
|
|
149
|
+
}
|
|
150
|
+
.account-grid {
|
|
151
|
+
display: grid;
|
|
152
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
153
|
+
gap: 9px;
|
|
154
|
+
padding: 10px 0 0;
|
|
155
|
+
}
|
|
156
|
+
.account {
|
|
157
|
+
position: relative;
|
|
158
|
+
min-width: 0;
|
|
159
|
+
min-height: 78px;
|
|
160
|
+
padding: 10px 11px 9px 22px;
|
|
161
|
+
border-radius: 13px;
|
|
162
|
+
background: #ffffff;
|
|
163
|
+
display: grid;
|
|
164
|
+
grid-template-rows: auto auto;
|
|
165
|
+
gap: 6px;
|
|
166
|
+
box-shadow: 0 1px 1px var(--shadow);
|
|
167
|
+
}
|
|
168
|
+
.account::before {
|
|
169
|
+
content: "";
|
|
170
|
+
position: absolute;
|
|
171
|
+
inset: 10px auto 10px 8px;
|
|
172
|
+
width: 5px;
|
|
173
|
+
border-radius: 999px;
|
|
174
|
+
background: var(--off);
|
|
175
|
+
}
|
|
176
|
+
.account.subscribed::before {
|
|
177
|
+
background: var(--app-color);
|
|
178
|
+
}
|
|
179
|
+
.account.subscribed {
|
|
180
|
+
background: var(--app-color-wash);
|
|
181
|
+
}
|
|
182
|
+
.account.unsubscribed {
|
|
183
|
+
background: #ffffff;
|
|
184
|
+
color: #687280;
|
|
185
|
+
box-shadow: none;
|
|
186
|
+
}
|
|
187
|
+
.main-line {
|
|
188
|
+
min-width: 0;
|
|
189
|
+
display: grid;
|
|
190
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
191
|
+
gap: 10px;
|
|
192
|
+
align-items: start;
|
|
193
|
+
}
|
|
194
|
+
.sub-line {
|
|
195
|
+
min-width: 0;
|
|
196
|
+
display: flex;
|
|
197
|
+
flex-wrap: wrap;
|
|
198
|
+
gap: 5px;
|
|
199
|
+
align-items: center;
|
|
200
|
+
}
|
|
201
|
+
.status {
|
|
202
|
+
display: inline-flex;
|
|
203
|
+
align-items: center;
|
|
204
|
+
justify-content: center;
|
|
205
|
+
width: 22px;
|
|
206
|
+
height: 22px;
|
|
207
|
+
}
|
|
208
|
+
.dot {
|
|
209
|
+
width: 8px;
|
|
210
|
+
height: 8px;
|
|
211
|
+
border-radius: 50%;
|
|
212
|
+
background: var(--off);
|
|
213
|
+
}
|
|
214
|
+
.subscribed .dot {
|
|
215
|
+
background: var(--app-color);
|
|
216
|
+
box-shadow: 0 0 0 3px var(--app-color-soft);
|
|
217
|
+
}
|
|
218
|
+
.subscribed .status {
|
|
219
|
+
color: var(--ok);
|
|
220
|
+
}
|
|
221
|
+
.unsubscribed .status {
|
|
222
|
+
color: var(--off);
|
|
223
|
+
}
|
|
224
|
+
.name {
|
|
225
|
+
min-width: 0;
|
|
226
|
+
overflow-wrap: anywhere;
|
|
227
|
+
display: -webkit-box;
|
|
228
|
+
overflow: hidden;
|
|
229
|
+
-webkit-line-clamp: 2;
|
|
230
|
+
-webkit-box-orient: vertical;
|
|
231
|
+
color: var(--text);
|
|
232
|
+
font-size: 18px;
|
|
233
|
+
line-height: 1.2;
|
|
234
|
+
font-weight: 780;
|
|
235
|
+
}
|
|
236
|
+
.unsubscribed .name {
|
|
237
|
+
color: #4f5b68;
|
|
238
|
+
font-weight: 700;
|
|
239
|
+
}
|
|
240
|
+
.id {
|
|
241
|
+
display: inline;
|
|
242
|
+
overflow-wrap: anywhere;
|
|
243
|
+
padding: 0;
|
|
244
|
+
border-radius: 0;
|
|
245
|
+
background: transparent;
|
|
246
|
+
color: #3f4b59;
|
|
247
|
+
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
|
248
|
+
font-size: 14px;
|
|
249
|
+
line-height: 1.3;
|
|
250
|
+
}
|
|
251
|
+
.id::before {
|
|
252
|
+
content: "@";
|
|
253
|
+
color: var(--app-color);
|
|
254
|
+
font-family: Inter, "Segoe UI", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
|
|
255
|
+
font-weight: 760;
|
|
256
|
+
}
|
|
257
|
+
.aliases {
|
|
258
|
+
display: flex;
|
|
259
|
+
flex-wrap: wrap;
|
|
260
|
+
gap: 5px;
|
|
261
|
+
}
|
|
262
|
+
.alias {
|
|
263
|
+
max-width: 100%;
|
|
264
|
+
overflow-wrap: anywhere;
|
|
265
|
+
padding: 0;
|
|
266
|
+
border-radius: 0;
|
|
267
|
+
background: transparent;
|
|
268
|
+
color: var(--app-color);
|
|
269
|
+
font-size: 13px;
|
|
270
|
+
line-height: 1.25;
|
|
271
|
+
font-weight: 720;
|
|
272
|
+
}
|
|
273
|
+
.alias::before {
|
|
274
|
+
content: "#";
|
|
275
|
+
opacity: 1;
|
|
276
|
+
}
|
|
277
|
+
.alias.empty {
|
|
278
|
+
background: transparent;
|
|
279
|
+
color: #9aa3af;
|
|
280
|
+
padding-left: 0;
|
|
281
|
+
font-weight: 520;
|
|
282
|
+
}
|
|
283
|
+
.alias.empty::before {
|
|
284
|
+
content: "";
|
|
285
|
+
}
|
|
286
|
+
.empty-row {
|
|
287
|
+
padding: 18px 4px;
|
|
288
|
+
color: var(--muted);
|
|
289
|
+
font-size: 18px;
|
|
290
|
+
}
|
|
291
|
+
</style>
|
|
292
|
+
</head>
|
|
293
|
+
<body>
|
|
294
|
+
<main class="page">
|
|
295
|
+
${showTitle ? '<h1 class="title">当前群订阅</h1>' : ""}
|
|
296
|
+
${apps.map(renderApp).join("")}
|
|
297
|
+
</main>
|
|
298
|
+
</body>
|
|
299
|
+
</html>
|
|
300
|
+
`;
|
|
301
|
+
}
|
|
302
|
+
__name(generateSubscriptionHTML, "generateSubscriptionHTML");
|
|
303
|
+
function createAppViews(availableAccounts, subscribedByApp, appThemeColors) {
|
|
304
|
+
return Object.entries(availableAccounts).map(([app, accounts]) => {
|
|
305
|
+
const subscribed = subscribedByApp[app] || /* @__PURE__ */ new Set();
|
|
306
|
+
return {
|
|
307
|
+
app,
|
|
308
|
+
theme: createTheme(appThemeColors[app], app),
|
|
309
|
+
accounts: accounts.map((account) => ({
|
|
310
|
+
...account,
|
|
311
|
+
aliases: normalizeAliases(account.alias),
|
|
312
|
+
subscribed: subscribed.has(account.id)
|
|
313
|
+
})).sort((a, b) => Number(b.subscribed) - Number(a.subscribed))
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
__name(createAppViews, "createAppViews");
|
|
318
|
+
function normalizeAliases(aliases) {
|
|
319
|
+
return aliases.flatMap((alias) => alias.split(",")).map((alias) => alias.trim()).filter(Boolean);
|
|
320
|
+
}
|
|
321
|
+
__name(normalizeAliases, "normalizeAliases");
|
|
322
|
+
function renderApp(section) {
|
|
323
|
+
return `
|
|
324
|
+
<section class="app" style="${renderThemeStyle(section.theme)}">
|
|
325
|
+
<div class="app-head">
|
|
326
|
+
<div class="app-name">${escapeHtml(section.app)}</div>
|
|
327
|
+
</div>
|
|
328
|
+
${section.accounts.length ? `<div class="account-grid">
|
|
329
|
+
${section.accounts.map(renderAccount).join("")}
|
|
330
|
+
</div>` : '<div class="empty-row">暂无可用账号</div>'}
|
|
331
|
+
</section>
|
|
332
|
+
`;
|
|
333
|
+
}
|
|
334
|
+
__name(renderApp, "renderApp");
|
|
335
|
+
function createTheme(input, app) {
|
|
336
|
+
const color = normalizeColor(input, getDefaultThemeColor(app));
|
|
337
|
+
const rgb = hexToRgb(color);
|
|
338
|
+
return {
|
|
339
|
+
color,
|
|
340
|
+
soft: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.22)`,
|
|
341
|
+
faint: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.11)`,
|
|
342
|
+
wash: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.06)`
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
__name(createTheme, "createTheme");
|
|
346
|
+
function getDefaultThemeColor(app) {
|
|
347
|
+
switch (app?.toLowerCase()) {
|
|
348
|
+
case "twitter":
|
|
349
|
+
return "#1d9bf0";
|
|
350
|
+
case "bilibili":
|
|
351
|
+
return "#fb7299";
|
|
352
|
+
case "instagram":
|
|
353
|
+
return "#c13584";
|
|
354
|
+
default:
|
|
355
|
+
return "#315f8a";
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
__name(getDefaultThemeColor, "getDefaultThemeColor");
|
|
359
|
+
function normalizeColor(input, fallback = "#315f8a") {
|
|
360
|
+
const value = input?.trim();
|
|
361
|
+
if (!value) return fallback;
|
|
362
|
+
if (/^#[0-9a-f]{3}$/i.test(value)) {
|
|
363
|
+
const [, r, g, b] = value;
|
|
364
|
+
return `#${r}${r}${g}${g}${b}${b}`.toLowerCase();
|
|
365
|
+
}
|
|
366
|
+
if (/^#[0-9a-f]{6}$/i.test(value)) {
|
|
367
|
+
return value.toLowerCase();
|
|
368
|
+
}
|
|
369
|
+
return fallback;
|
|
370
|
+
}
|
|
371
|
+
__name(normalizeColor, "normalizeColor");
|
|
372
|
+
function hexToRgb(hex) {
|
|
373
|
+
return {
|
|
374
|
+
r: Number.parseInt(hex.slice(1, 3), 16),
|
|
375
|
+
g: Number.parseInt(hex.slice(3, 5), 16),
|
|
376
|
+
b: Number.parseInt(hex.slice(5, 7), 16)
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
__name(hexToRgb, "hexToRgb");
|
|
380
|
+
function renderThemeStyle(theme) {
|
|
381
|
+
return [
|
|
382
|
+
`--app-color: ${theme.color}`,
|
|
383
|
+
`--app-color-soft: ${theme.soft}`,
|
|
384
|
+
`--app-color-faint: ${theme.faint}`,
|
|
385
|
+
`--app-color-wash: ${theme.wash}`
|
|
386
|
+
].join("; ");
|
|
387
|
+
}
|
|
388
|
+
__name(renderThemeStyle, "renderThemeStyle");
|
|
389
|
+
function renderAccount(account) {
|
|
390
|
+
return `
|
|
391
|
+
<article class="account ${account.subscribed ? "subscribed" : "unsubscribed"}">
|
|
392
|
+
<div class="main-line">
|
|
393
|
+
<div class="name">${escapeHtml(account.name)}</div>
|
|
394
|
+
<span class="status"><span class="dot"></span></span>
|
|
395
|
+
</div>
|
|
396
|
+
<div class="sub-line">
|
|
397
|
+
<span class="id">${escapeHtml(account.id)}</span>
|
|
398
|
+
<span class="aliases">${renderAliases(account.aliases)}</span>
|
|
399
|
+
</div>
|
|
400
|
+
</article>
|
|
401
|
+
`;
|
|
402
|
+
}
|
|
403
|
+
__name(renderAccount, "renderAccount");
|
|
404
|
+
function renderAliases(aliases) {
|
|
405
|
+
return aliases.length ? aliases.map((alias) => `<span class="alias">${escapeHtml(alias)}</span>`).join("") : '<span class="alias empty">无别名</span>';
|
|
406
|
+
}
|
|
407
|
+
__name(renderAliases, "renderAliases");
|
|
408
|
+
function escapeHtml(value) {
|
|
409
|
+
return String(value ?? "").replace(/[&<>"']/g, (char) => ({
|
|
410
|
+
"&": "&",
|
|
411
|
+
"<": "<",
|
|
412
|
+
">": ">",
|
|
413
|
+
'"': """,
|
|
414
|
+
"'": "'"
|
|
415
|
+
})[char]);
|
|
416
|
+
}
|
|
417
|
+
__name(escapeHtml, "escapeHtml");
|
|
418
|
+
|
|
419
|
+
// src/retry.ts
|
|
420
|
+
async function retry(retries, fn, delay = 500) {
|
|
421
|
+
try {
|
|
422
|
+
return await fn();
|
|
423
|
+
} catch (err) {
|
|
424
|
+
console.log(`剩余${retries}次尝试`, err);
|
|
425
|
+
if (retries <= 1) throw err;
|
|
426
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
427
|
+
return retry(retries - 1, fn, delay * 2);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
__name(retry, "retry");
|
|
431
|
+
|
|
432
|
+
// src/service.tsx
|
|
433
|
+
var import_jsx_runtime = require("@satorijs/element/jsx-runtime");
|
|
434
|
+
var SubscriptionService = class extends import_koishi2.Service {
|
|
76
435
|
static {
|
|
77
436
|
__name(this, "SubscriptionService");
|
|
78
437
|
}
|
|
79
|
-
|
|
80
|
-
|
|
438
|
+
subTableName = "subscription_service";
|
|
439
|
+
recTableName = "subscription_records";
|
|
81
440
|
subsConfig;
|
|
82
441
|
appMap;
|
|
83
442
|
accountMap;
|
|
@@ -85,7 +444,7 @@ var SubscriptionService = class extends import_koishi.Service {
|
|
|
85
444
|
super(ctx, "subscription");
|
|
86
445
|
this.subsConfig = config;
|
|
87
446
|
this.initMap();
|
|
88
|
-
ctx.model.extend(this.
|
|
447
|
+
ctx.model.extend(this.subTableName, {
|
|
89
448
|
id: "unsigned",
|
|
90
449
|
app: "string",
|
|
91
450
|
account: "string",
|
|
@@ -95,6 +454,14 @@ var SubscriptionService = class extends import_koishi.Service {
|
|
|
95
454
|
primary: "id",
|
|
96
455
|
autoInc: true
|
|
97
456
|
});
|
|
457
|
+
ctx.model.extend(this.recTableName, {
|
|
458
|
+
app: "string",
|
|
459
|
+
id: "string",
|
|
460
|
+
createdAt: "timestamp"
|
|
461
|
+
}, {
|
|
462
|
+
primary: ["app", "id"],
|
|
463
|
+
autoInc: false
|
|
464
|
+
});
|
|
98
465
|
this.registerCommands(ctx, config);
|
|
99
466
|
}
|
|
100
467
|
initMap() {
|
|
@@ -122,25 +489,38 @@ var SubscriptionService = class extends import_koishi.Service {
|
|
|
122
489
|
}
|
|
123
490
|
}
|
|
124
491
|
getApp(app) {
|
|
492
|
+
if (!app) return;
|
|
125
493
|
return this.appMap.get(app.toLowerCase());
|
|
126
494
|
}
|
|
495
|
+
async checkExist(app, id) {
|
|
496
|
+
app = this.getApp(app);
|
|
497
|
+
if ((await this.ctx.database.get(this.recTableName, { app, id })).length > 0) {
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
await this.ctx.database.upsert(this.recTableName, [{
|
|
501
|
+
app,
|
|
502
|
+
id,
|
|
503
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
504
|
+
}]);
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
127
507
|
getAccount(app, account) {
|
|
508
|
+
if (!app || !account) return;
|
|
128
509
|
return this.accountMap.get((app + account).toLowerCase());
|
|
129
510
|
}
|
|
130
511
|
getName(app, account) {
|
|
131
512
|
app = this.getApp(app);
|
|
513
|
+
if (!app) return;
|
|
132
514
|
return this.subsConfig.apps[app].allowedAccounts.find((item) => item.id === this.getAccount(app, account))?.name;
|
|
133
515
|
}
|
|
134
|
-
// 获取对应应用可以订阅的账号
|
|
135
516
|
getAvailableAccounts(app) {
|
|
136
517
|
app = this.getApp(app);
|
|
137
518
|
const appConfig = this.subsConfig.apps[app];
|
|
138
519
|
return appConfig ? appConfig.allowedAccounts.map((item) => item.id) : [];
|
|
139
520
|
}
|
|
140
|
-
// 根据应用和账号获取订阅了这个账号的所有群组
|
|
141
521
|
async getSubscribedGroups(app, account) {
|
|
142
522
|
app = this.getApp(app), account = this.getAccount(app, account);
|
|
143
|
-
const subscriptions = await this.ctx.database.get(this.
|
|
523
|
+
const subscriptions = await this.ctx.database.get(this.subTableName, {
|
|
144
524
|
app,
|
|
145
525
|
account
|
|
146
526
|
});
|
|
@@ -178,6 +558,7 @@ var SubscriptionService = class extends import_koishi.Service {
|
|
|
178
558
|
if (index < 0) continue;
|
|
179
559
|
channels.splice(index, 1);
|
|
180
560
|
}
|
|
561
|
+
;
|
|
181
562
|
((assignMap[platform] ||= {})[assignee] ||= []).push(channel);
|
|
182
563
|
}
|
|
183
564
|
if (channels?.length) {
|
|
@@ -191,24 +572,35 @@ var SubscriptionService = class extends import_koishi.Service {
|
|
|
191
572
|
type: "message",
|
|
192
573
|
channel: {
|
|
193
574
|
id: selfChannelId,
|
|
194
|
-
type:
|
|
575
|
+
type: import_koishi2.Universal.Channel.Type.DIRECT
|
|
195
576
|
},
|
|
196
577
|
user: { id: bot.selfId }
|
|
197
578
|
});
|
|
198
|
-
|
|
199
|
-
|
|
579
|
+
let messageIds = [];
|
|
580
|
+
if (Array.isArray(content)) {
|
|
581
|
+
for (const msg of content) {
|
|
200
582
|
try {
|
|
201
|
-
|
|
583
|
+
const ids = await retry(
|
|
584
|
+
3,
|
|
585
|
+
() => bot.sendMessage(selfChannelId, msg, void 0, { session })
|
|
586
|
+
);
|
|
587
|
+
if (Array.isArray(ids)) messageIds.push(...ids);
|
|
588
|
+
else if (ids != null) messageIds.push(ids);
|
|
202
589
|
} catch (e) {
|
|
203
|
-
|
|
590
|
+
continue;
|
|
204
591
|
}
|
|
205
592
|
}
|
|
206
|
-
|
|
593
|
+
} else {
|
|
594
|
+
messageIds = await retry(
|
|
595
|
+
3,
|
|
596
|
+
() => bot.sendMessage(selfChannelId, content, void 0, { session })
|
|
597
|
+
);
|
|
598
|
+
}
|
|
207
599
|
const forwardContent = /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { forward: true, children: messageIds.map((id) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("message", { id: `${id}` })) });
|
|
208
600
|
const sessions = targets.map(({ id, guildId, locales }) => {
|
|
209
601
|
const session2 = bot.session({
|
|
210
602
|
type: "message",
|
|
211
|
-
channel: { id, type:
|
|
603
|
+
channel: { id, type: import_koishi2.Universal.Channel.Type.TEXT },
|
|
212
604
|
guild: { id: guildId }
|
|
213
605
|
});
|
|
214
606
|
session2.locales = locales;
|
|
@@ -221,17 +613,17 @@ var SubscriptionService = class extends import_koishi.Service {
|
|
|
221
613
|
}
|
|
222
614
|
}))).flat(1);
|
|
223
615
|
}
|
|
224
|
-
// 添加订阅
|
|
225
616
|
async addSubscription(app, account, groupId) {
|
|
226
|
-
app = this.getApp(app)
|
|
227
|
-
|
|
228
|
-
if (!appConfig) {
|
|
617
|
+
app = this.getApp(app);
|
|
618
|
+
if (!app) {
|
|
229
619
|
return { status: false, msg: "应用不存在" };
|
|
230
620
|
}
|
|
621
|
+
account = this.getAccount(app, account);
|
|
622
|
+
const appConfig = this.getAvailableAccounts(app);
|
|
231
623
|
if (!appConfig.includes(account)) {
|
|
232
624
|
return { status: false, msg: "账号不在白名单中" };
|
|
233
625
|
}
|
|
234
|
-
const existing = await this.ctx.database.get(this.
|
|
626
|
+
const existing = await this.ctx.database.get(this.subTableName, {
|
|
235
627
|
app,
|
|
236
628
|
account,
|
|
237
629
|
groupId
|
|
@@ -239,7 +631,7 @@ var SubscriptionService = class extends import_koishi.Service {
|
|
|
239
631
|
if (existing.length > 0) {
|
|
240
632
|
return { status: false, msg: "已订阅该账号" };
|
|
241
633
|
}
|
|
242
|
-
await this.ctx.database.create(this.
|
|
634
|
+
await this.ctx.database.create(this.subTableName, {
|
|
243
635
|
app,
|
|
244
636
|
account,
|
|
245
637
|
groupId,
|
|
@@ -247,25 +639,24 @@ var SubscriptionService = class extends import_koishi.Service {
|
|
|
247
639
|
});
|
|
248
640
|
return { status: true, msg: "订阅成功" };
|
|
249
641
|
}
|
|
250
|
-
// 删除订阅
|
|
251
642
|
async removeSubscription(app, account, groupId) {
|
|
252
|
-
app = this.
|
|
253
|
-
|
|
643
|
+
app = this.getApp(app);
|
|
644
|
+
if (!app) return false;
|
|
645
|
+
account = this.getAccount(app, account);
|
|
646
|
+
if (!account) return false;
|
|
647
|
+
const result = await this.ctx.database.remove(this.subTableName, {
|
|
254
648
|
app,
|
|
255
649
|
account,
|
|
256
650
|
groupId
|
|
257
651
|
});
|
|
258
652
|
return result.removed > 0;
|
|
259
653
|
}
|
|
260
|
-
// 获取群组的所有订阅
|
|
261
654
|
async getGroupSubscriptions(groupId) {
|
|
262
|
-
return await this.ctx.database.get(this.
|
|
655
|
+
return await this.ctx.database.get(this.subTableName, { groupId });
|
|
263
656
|
}
|
|
264
|
-
// 获取所有订阅(用于管理)
|
|
265
657
|
async getAllSubscriptions() {
|
|
266
|
-
return await this.ctx.database.get(this.
|
|
658
|
+
return await this.ctx.database.get(this.subTableName, {});
|
|
267
659
|
}
|
|
268
|
-
// 按应用获取订阅统计
|
|
269
660
|
async getSubscriptionStats() {
|
|
270
661
|
const subscriptions = await this.getAllSubscriptions();
|
|
271
662
|
const stats = {};
|
|
@@ -296,50 +687,112 @@ var SubscriptionService = class extends import_koishi.Service {
|
|
|
296
687
|
if (!channel) return;
|
|
297
688
|
return `${channel.platform}:${channel.id}`;
|
|
298
689
|
}
|
|
690
|
+
async getSubscriptionListView(app, groupId) {
|
|
691
|
+
const subscriptions = await this.getGroupSubscriptions(groupId);
|
|
692
|
+
const subscribedByApp = {};
|
|
693
|
+
subscriptions.forEach((sub) => {
|
|
694
|
+
if (!subscribedByApp[sub.app]) {
|
|
695
|
+
subscribedByApp[sub.app] = /* @__PURE__ */ new Set();
|
|
696
|
+
}
|
|
697
|
+
subscribedByApp[sub.app].add(sub.account);
|
|
698
|
+
});
|
|
699
|
+
const availableAccounts = {};
|
|
700
|
+
const appThemeColors = {};
|
|
701
|
+
const appConfig = this.subsConfig.apps;
|
|
702
|
+
const targetApp = app ? this.getApp(app) : void 0;
|
|
703
|
+
if (app && !targetApp) {
|
|
704
|
+
return `应用不存在:${app}
|
|
705
|
+
请使用配置中的应用名或应用别名,例如:订阅列表 twitter`;
|
|
706
|
+
}
|
|
707
|
+
for (const appName in appConfig) {
|
|
708
|
+
if (targetApp && appName !== targetApp) continue;
|
|
709
|
+
appThemeColors[appName] = appConfig[appName].themeColor;
|
|
710
|
+
const subscribed = subscribedByApp[appName] || /* @__PURE__ */ new Set();
|
|
711
|
+
availableAccounts[appName] = appConfig[appName].allowedAccounts.filter((account) => targetApp || subscribed.has(account.id)).map((account) => ({
|
|
712
|
+
id: account.id,
|
|
713
|
+
name: account.name,
|
|
714
|
+
alias: account.alias ? account.alias.split(",") : []
|
|
715
|
+
}));
|
|
716
|
+
}
|
|
717
|
+
return {
|
|
718
|
+
targetApp,
|
|
719
|
+
subscribedByApp,
|
|
720
|
+
availableAccounts,
|
|
721
|
+
appThemeColors
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
renderSubscriptionListText(view) {
|
|
725
|
+
let result = ``;
|
|
726
|
+
for (const app in view.availableAccounts) {
|
|
727
|
+
result += `【${app}】
|
|
728
|
+
`;
|
|
729
|
+
const subscribed = view.subscribedByApp[app] || /* @__PURE__ */ new Set();
|
|
730
|
+
view.availableAccounts[app].forEach((account) => {
|
|
731
|
+
const isSubscribed = subscribed.has(account.id);
|
|
732
|
+
const status = isSubscribed ? "✅ 已订阅" : "❌ 未订阅";
|
|
733
|
+
const aliases = account.alias.length > 0 ? ` (${account.alias.join(", ")})` : "";
|
|
734
|
+
result += `${status} ${account.name} id:${account.id}
|
|
735
|
+
${aliases}
|
|
736
|
+
`;
|
|
737
|
+
});
|
|
738
|
+
result += "\n";
|
|
739
|
+
}
|
|
740
|
+
return result;
|
|
741
|
+
}
|
|
299
742
|
registerCommands(ctx, config) {
|
|
300
|
-
const
|
|
743
|
+
const screenshotWidth = 1440;
|
|
744
|
+
const screenshotDeviceScaleFactor = 2;
|
|
745
|
+
const screenshotQuality = 82;
|
|
746
|
+
const sub = ctx.command("subscription <app:string> [...accounts]", "为当前群添加账号订阅。账号可使用 ID、名称或别名,可一次添加多个。").alias("订阅").alias("添加订阅").userFields(["authority"]).example("订阅 twitter aimi_sound ayasa_ito").example("推特订阅 爱美 伊藤彩沙").action(async ({ session, options }, app, ...accounts) => {
|
|
301
747
|
if (!await this.authCheck(session, config)) {
|
|
302
748
|
return "权限不足";
|
|
303
749
|
}
|
|
304
750
|
if (!app || !accounts || accounts.length == 0) {
|
|
305
|
-
return "
|
|
751
|
+
return "请指定应用名和账号。\n用法:订阅 <应用名> <账号> [...更多账号]\n示例:订阅 twitter aimi_sound ayasa_ito";
|
|
752
|
+
}
|
|
753
|
+
const appName = this.getApp(app);
|
|
754
|
+
if (!appName) {
|
|
755
|
+
return `应用不存在:${app}`;
|
|
306
756
|
}
|
|
307
|
-
accounts;
|
|
308
757
|
const groupId = this.getChannelId(session.channel);
|
|
309
758
|
if (!groupId) {
|
|
310
759
|
return "请在群聊中使用当前指令";
|
|
311
760
|
}
|
|
312
761
|
let result = "";
|
|
313
762
|
for (let account of accounts) {
|
|
314
|
-
|
|
315
|
-
const { status, msg } = await this.addSubscription(
|
|
763
|
+
const accountName = this.getName(appName, account) || account;
|
|
764
|
+
const { status, msg } = await this.addSubscription(appName, accountName, groupId);
|
|
316
765
|
if (status) {
|
|
317
|
-
result +=
|
|
766
|
+
result += `成功为当前群添加订阅:${appName} - ${accountName}
|
|
318
767
|
`;
|
|
319
768
|
} else {
|
|
320
|
-
result += `添加订阅 ${
|
|
769
|
+
result += `添加订阅 ${appName} - ${accountName} 失败,${msg}
|
|
321
770
|
`;
|
|
322
771
|
}
|
|
323
772
|
}
|
|
324
773
|
return result;
|
|
325
774
|
});
|
|
326
|
-
const unsub = ctx.command("subscription.remove <app:string> <account:string>", "
|
|
775
|
+
const unsub = ctx.command("subscription.remove <app:string> <account:string>", "从当前群删除一个账号订阅。账号可使用 ID、名称或别名。").alias("删除订阅").alias("取消订阅").userFields(["authority"]).example("取消订阅 twitter aimi_sound").example("取消推特订阅 爱美").action(async ({ session, options }, app, account) => {
|
|
327
776
|
if (!await this.authCheck(session, config)) {
|
|
328
777
|
return "权限不足";
|
|
329
778
|
}
|
|
330
779
|
if (!app || !account) {
|
|
331
|
-
return "
|
|
780
|
+
return "请指定应用名和账号。\n用法:取消订阅 <应用名> <账号>\n示例:取消订阅 twitter aimi_sound";
|
|
781
|
+
}
|
|
782
|
+
const appName = this.getApp(app);
|
|
783
|
+
if (!appName) {
|
|
784
|
+
return `应用不存在:${app}`;
|
|
332
785
|
}
|
|
333
|
-
|
|
786
|
+
const accountName = this.getName(appName, account) || account;
|
|
334
787
|
const groupId = this.getChannelId(session.channel);
|
|
335
788
|
if (!groupId) {
|
|
336
789
|
return "请在群聊中使用当前指令";
|
|
337
790
|
}
|
|
338
|
-
const success = await this.removeSubscription(
|
|
791
|
+
const success = await this.removeSubscription(appName, accountName, groupId);
|
|
339
792
|
if (success) {
|
|
340
|
-
return
|
|
793
|
+
return `成功为当前群删除订阅:${appName} - ${accountName}`;
|
|
341
794
|
} else {
|
|
342
|
-
return
|
|
795
|
+
return "删除订阅失败,可能的原因:订阅不存在";
|
|
343
796
|
}
|
|
344
797
|
});
|
|
345
798
|
for (const app in config.apps) {
|
|
@@ -350,330 +803,50 @@ var SubscriptionService = class extends import_koishi.Service {
|
|
|
350
803
|
unsub.alias(`取消${appAlias}订阅`, { args: [app] });
|
|
351
804
|
}
|
|
352
805
|
}
|
|
353
|
-
ctx.command("subscription.list", "
|
|
354
|
-
|
|
806
|
+
ctx.command("subscription.list [app:string]", "查看当前群订阅。默认只显示已订阅账号;指定应用时显示该应用完整状态。").alias("订阅列表").userFields(["authority"]).example("订阅列表").example("订阅列表 twitter").example("订阅列表 推特").action(async ({ session, options }, app) => {
|
|
807
|
+
const passedAuth = await this.authCheck(session, config);
|
|
808
|
+
if (!passedAuth) {
|
|
355
809
|
return "权限不足";
|
|
356
810
|
}
|
|
357
811
|
const groupId = this.getChannelId(session.channel);
|
|
358
812
|
if (!groupId) {
|
|
359
813
|
return "请在群聊中使用当前指令";
|
|
360
814
|
}
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
if (!subscribedByApp[sub2.app]) {
|
|
365
|
-
subscribedByApp[sub2.app] = /* @__PURE__ */ new Set();
|
|
366
|
-
}
|
|
367
|
-
subscribedByApp[sub2.app].add(sub2.account);
|
|
368
|
-
});
|
|
369
|
-
const availableAccounts = {};
|
|
370
|
-
const appConfig = this.subsConfig.apps;
|
|
371
|
-
for (const app in appConfig) {
|
|
372
|
-
availableAccounts[app] = appConfig[app].allowedAccounts.map((account) => ({
|
|
373
|
-
id: account.id,
|
|
374
|
-
name: account.name,
|
|
375
|
-
alias: account.alias ? account.alias.split(",") : []
|
|
376
|
-
}));
|
|
377
|
-
}
|
|
815
|
+
const view = await this.getSubscriptionListView(app, groupId);
|
|
816
|
+
if (typeof view === "string") return view;
|
|
817
|
+
const textFallback = /* @__PURE__ */ __name(() => this.renderSubscriptionListText(view), "textFallback");
|
|
378
818
|
if (!ctx.puppeteer) {
|
|
379
|
-
|
|
380
|
-
for (const app in availableAccounts) {
|
|
381
|
-
result += `【${app}】
|
|
382
|
-
`;
|
|
383
|
-
const subscribed = subscribedByApp[app] || /* @__PURE__ */ new Set();
|
|
384
|
-
availableAccounts[app].forEach((account) => {
|
|
385
|
-
const isSubscribed = subscribed.has(account.id);
|
|
386
|
-
const status = isSubscribed ? "✅ 已订阅" : "❌ 未订阅";
|
|
387
|
-
const aliases = account.alias.length > 0 ? ` (${account.alias.join(", ")})` : "";
|
|
388
|
-
result += `${status} ${account.name} id:${account.id}
|
|
389
|
-
${aliases}
|
|
390
|
-
`;
|
|
391
|
-
});
|
|
392
|
-
result += "\n";
|
|
393
|
-
}
|
|
394
|
-
return result;
|
|
819
|
+
return textFallback();
|
|
395
820
|
}
|
|
396
|
-
const html = generateSubscriptionHTML(groupId, availableAccounts, subscribedByApp
|
|
397
|
-
|
|
821
|
+
const html = generateSubscriptionHTML(groupId, view.availableAccounts, view.subscribedByApp, view.appThemeColors, {
|
|
822
|
+
showTitle: !view.targetApp
|
|
823
|
+
});
|
|
824
|
+
let page;
|
|
398
825
|
try {
|
|
826
|
+
page = await ctx.puppeteer.page();
|
|
827
|
+
page.setDefaultTimeout?.(15e3);
|
|
399
828
|
await page.setViewport({
|
|
400
|
-
width:
|
|
401
|
-
height:
|
|
829
|
+
width: screenshotWidth,
|
|
830
|
+
height: 1,
|
|
831
|
+
deviceScaleFactor: screenshotDeviceScaleFactor
|
|
402
832
|
});
|
|
403
833
|
await page.setContent(html, {
|
|
404
|
-
waitUntil: "
|
|
834
|
+
waitUntil: "load"
|
|
405
835
|
});
|
|
406
836
|
const image = await page.screenshot({
|
|
407
837
|
type: "jpeg",
|
|
838
|
+
quality: screenshotQuality,
|
|
408
839
|
fullPage: true
|
|
409
840
|
});
|
|
410
|
-
|
|
841
|
+
const base64 = image.toString("base64");
|
|
842
|
+
return `<image url="data:image/jpeg;base64,${base64}" />`;
|
|
843
|
+
} catch (error) {
|
|
844
|
+
return textFallback();
|
|
411
845
|
} finally {
|
|
412
|
-
await page
|
|
846
|
+
await page?.close();
|
|
413
847
|
}
|
|
414
848
|
});
|
|
415
|
-
|
|
416
|
-
return `
|
|
417
|
-
<!DOCTYPE html>
|
|
418
|
-
<html>
|
|
419
|
-
<head>
|
|
420
|
-
<meta charset="UTF-8">
|
|
421
|
-
<style>
|
|
422
|
-
* {
|
|
423
|
-
margin: 0;
|
|
424
|
-
padding: 0;
|
|
425
|
-
box-sizing: border-box;
|
|
426
|
-
}
|
|
427
|
-
body {
|
|
428
|
-
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
|
429
|
-
background: #f5f7fa;
|
|
430
|
-
min-height: 100vh;
|
|
431
|
-
padding: 20px;
|
|
432
|
-
}
|
|
433
|
-
.container {
|
|
434
|
-
max-width: 1000px;
|
|
435
|
-
margin: 0 auto;
|
|
436
|
-
}
|
|
437
|
-
.header {
|
|
438
|
-
text-align: center;
|
|
439
|
-
margin-bottom: 20px;
|
|
440
|
-
color: #2c3e50;
|
|
441
|
-
}
|
|
442
|
-
.header h1 {
|
|
443
|
-
font-size: 36px;
|
|
444
|
-
margin-bottom: 12px;
|
|
445
|
-
font-weight: 900;
|
|
446
|
-
}
|
|
447
|
-
.header .subtitle {
|
|
448
|
-
font-size: 21px;
|
|
449
|
-
color: #7f8c8d;
|
|
450
|
-
}
|
|
451
|
-
.apps-container {
|
|
452
|
-
background: white;
|
|
453
|
-
border-radius: 12px;
|
|
454
|
-
box-shadow: 0 3px 18px rgba(0,0,0,0.08);
|
|
455
|
-
overflow: hidden;
|
|
456
|
-
}
|
|
457
|
-
.app-section {
|
|
458
|
-
border-bottom: 1px solid #eaeaea;
|
|
459
|
-
}
|
|
460
|
-
.app-section:last-child {
|
|
461
|
-
border-bottom: none;
|
|
462
|
-
}
|
|
463
|
-
.app-title {
|
|
464
|
-
font-size: 24px;
|
|
465
|
-
font-weight: 600;
|
|
466
|
-
color: #34495e;
|
|
467
|
-
padding: 18px 24px;
|
|
468
|
-
background: #f8f9fa;
|
|
469
|
-
border-bottom: 1px solid #eaeaea;
|
|
470
|
-
display: flex;
|
|
471
|
-
align-items: center;
|
|
472
|
-
gap: 12px;
|
|
473
|
-
}
|
|
474
|
-
.app-title::before {
|
|
475
|
-
content: "▸";
|
|
476
|
-
font-size: 21px;
|
|
477
|
-
color: #3498db;
|
|
478
|
-
}
|
|
479
|
-
.grid {
|
|
480
|
-
display: grid;
|
|
481
|
-
grid-template-columns: repeat(2, 1fr);
|
|
482
|
-
gap: 0;
|
|
483
|
-
}
|
|
484
|
-
.account-card {
|
|
485
|
-
padding: 21px;
|
|
486
|
-
border-right: 1px solid #f0f0f0;
|
|
487
|
-
border-bottom: 1px solid #f0f0f0;
|
|
488
|
-
transition: all 0.2s ease;
|
|
489
|
-
position: relative;
|
|
490
|
-
min-height: 120px;
|
|
491
|
-
display: flex;
|
|
492
|
-
flex-direction: column;
|
|
493
|
-
}
|
|
494
|
-
.account-card:nth-child(4n+1),
|
|
495
|
-
.account-card:nth-child(4n+4) {
|
|
496
|
-
background: #f8f9fa;
|
|
497
|
-
}
|
|
498
|
-
.account-card:nth-child(4n+2),
|
|
499
|
-
.account-card:nth-child(4n+3) {
|
|
500
|
-
background: #f0f2f5;
|
|
501
|
-
}
|
|
502
|
-
.account-card:hover {
|
|
503
|
-
background: #e6f3ff !important;
|
|
504
|
-
}
|
|
505
|
-
.account-card:nth-child(2n) {
|
|
506
|
-
border-right: none;
|
|
507
|
-
}
|
|
508
|
-
.account-card:last-child {
|
|
509
|
-
border-right: none;
|
|
510
|
-
}
|
|
511
|
-
.name-id-container {
|
|
512
|
-
display: flex;
|
|
513
|
-
justify-content: space-between;
|
|
514
|
-
align-items: center;
|
|
515
|
-
margin-bottom: 12px;
|
|
516
|
-
flex-wrap: wrap;
|
|
517
|
-
}
|
|
518
|
-
.name {
|
|
519
|
-
font-size: 21px;
|
|
520
|
-
font-weight: 900;
|
|
521
|
-
color: #2c3e50;
|
|
522
|
-
line-height: 1.3;
|
|
523
|
-
max-width: 60%;
|
|
524
|
-
overflow: hidden;
|
|
525
|
-
text-overflow: ellipsis;
|
|
526
|
-
white-space: nowrap;
|
|
527
|
-
}
|
|
528
|
-
.id {
|
|
529
|
-
font-size: 21px;
|
|
530
|
-
color: #5a6c7d;
|
|
531
|
-
font-family: 'Courier New', monospace;
|
|
532
|
-
background: #f8f9fa;
|
|
533
|
-
padding: 4.5px 9px;
|
|
534
|
-
border-radius: 4.5px;
|
|
535
|
-
line-height: 1.3;
|
|
536
|
-
max-width: 35%;
|
|
537
|
-
overflow: hidden;
|
|
538
|
-
text-overflow: ellipsis;
|
|
539
|
-
white-space: nowrap;
|
|
540
|
-
}
|
|
541
|
-
.name-id-container.vertical {
|
|
542
|
-
flex-direction: column;
|
|
543
|
-
align-items: flex-start;
|
|
544
|
-
}
|
|
545
|
-
.name-id-container.vertical .name {
|
|
546
|
-
max-width: 100%;
|
|
547
|
-
margin-bottom: 6px;
|
|
548
|
-
}
|
|
549
|
-
.name-id-container.vertical .id {
|
|
550
|
-
max-width: 100%;
|
|
551
|
-
align-self: flex-end;
|
|
552
|
-
}
|
|
553
|
-
.alias-status-container {
|
|
554
|
-
display: flex;
|
|
555
|
-
justify-content: space-between;
|
|
556
|
-
margin-top: 12px;
|
|
557
|
-
flex-grow: 1;
|
|
558
|
-
}
|
|
559
|
-
.alias-container {
|
|
560
|
-
flex: 1;
|
|
561
|
-
max-width: calc(100% - 105px);
|
|
562
|
-
align-self: flex-start;
|
|
563
|
-
}
|
|
564
|
-
.alias {
|
|
565
|
-
font-size: 20px;
|
|
566
|
-
color: #6c757d;
|
|
567
|
-
display: flex;
|
|
568
|
-
flex-wrap: wrap;
|
|
569
|
-
gap: 6px;
|
|
570
|
-
align-items: flex-start;
|
|
571
|
-
}
|
|
572
|
-
.alias-tag {
|
|
573
|
-
background: #e8f4fd;
|
|
574
|
-
color: #1971c2;
|
|
575
|
-
padding: 3px 9px;
|
|
576
|
-
border-radius: 15px;
|
|
577
|
-
border: 1px solid #a5d8ff;
|
|
578
|
-
white-space: nowrap;
|
|
579
|
-
margin-bottom: 3px;
|
|
580
|
-
}
|
|
581
|
-
.no-alias {
|
|
582
|
-
font-size: 20px;
|
|
583
|
-
color: #adb5bd;
|
|
584
|
-
font-style: italic;
|
|
585
|
-
}
|
|
586
|
-
.status-container {
|
|
587
|
-
display: flex;
|
|
588
|
-
align-items: flex-end;
|
|
589
|
-
margin-left: 12px;
|
|
590
|
-
}
|
|
591
|
-
.status {
|
|
592
|
-
display: inline-flex;
|
|
593
|
-
align-items: center;
|
|
594
|
-
gap: 9px;
|
|
595
|
-
padding: 4.5px 15px;
|
|
596
|
-
border-radius: 24px;
|
|
597
|
-
font-size: 20px;
|
|
598
|
-
font-weight: 900;
|
|
599
|
-
white-space: nowrap;
|
|
600
|
-
flex-shrink: 0;
|
|
601
|
-
}
|
|
602
|
-
.subscribed {
|
|
603
|
-
background: #d4edda;
|
|
604
|
-
color: #155724;
|
|
605
|
-
border: 1px solid #c3e6cb;
|
|
606
|
-
}
|
|
607
|
-
.unsubscribed {
|
|
608
|
-
background: #f8d7da;
|
|
609
|
-
color: #721c24;
|
|
610
|
-
border: 1px solid #f5c6cb;
|
|
611
|
-
}
|
|
612
|
-
.empty-state {
|
|
613
|
-
text-align: center;
|
|
614
|
-
color: #6c757d;
|
|
615
|
-
font-style: italic;
|
|
616
|
-
padding: 45px;
|
|
617
|
-
background: #f8f9fa;
|
|
618
|
-
grid-column: 1 / -1;
|
|
619
|
-
}
|
|
620
|
-
</style>
|
|
621
|
-
</head>
|
|
622
|
-
<body>
|
|
623
|
-
<div class="container">
|
|
624
|
-
<div class="header">
|
|
625
|
-
<h1>订阅状态总览</h1>
|
|
626
|
-
</div>
|
|
627
|
-
|
|
628
|
-
<div class="apps-container">
|
|
629
|
-
${Object.entries(availableAccounts).map(([app, accounts]) => {
|
|
630
|
-
const subscribed = subscribedByApp[app] || /* @__PURE__ */ new Set();
|
|
631
|
-
return `
|
|
632
|
-
<div class="app-section">
|
|
633
|
-
<div class="app-title">${app}</div>
|
|
634
|
-
<div class="grid">
|
|
635
|
-
${accounts.length > 0 ? accounts.map((account) => {
|
|
636
|
-
const isSubscribed = subscribed.has(account.id);
|
|
637
|
-
const needsVerticalLayout = account.name.length > 15 || account.id.length > 20;
|
|
638
|
-
return `
|
|
639
|
-
<div class="account-card">
|
|
640
|
-
<div class="name-id-container ${needsVerticalLayout ? "vertical" : ""}">
|
|
641
|
-
<div class="name">${account.name}</div>
|
|
642
|
-
<div class="id">${account.id}</div>
|
|
643
|
-
</div>
|
|
644
|
-
<div class="alias-status-container">
|
|
645
|
-
<div class="alias-container">
|
|
646
|
-
${account.alias.length > 0 ? `
|
|
647
|
-
<div class="alias">
|
|
648
|
-
${account.alias.map((alias) => `<span class="alias-tag">${alias}</span>`).join("")}
|
|
649
|
-
</div>
|
|
650
|
-
` : `<div class="no-alias">无别名</div>`}
|
|
651
|
-
</div>
|
|
652
|
-
<div class="status-container">
|
|
653
|
-
<div class="status ${isSubscribed ? "subscribed" : "unsubscribed"}">
|
|
654
|
-
${isSubscribed ? "✅ 已订阅" : "❌ 未订阅"}
|
|
655
|
-
</div>
|
|
656
|
-
</div>
|
|
657
|
-
</div>
|
|
658
|
-
</div>
|
|
659
|
-
`;
|
|
660
|
-
}).join("") : `
|
|
661
|
-
<div class="empty-state">
|
|
662
|
-
暂无可用账号
|
|
663
|
-
</div>
|
|
664
|
-
`}
|
|
665
|
-
</div>
|
|
666
|
-
</div>
|
|
667
|
-
`;
|
|
668
|
-
}).join("")}
|
|
669
|
-
</div>
|
|
670
|
-
</div>
|
|
671
|
-
</body>
|
|
672
|
-
</html>
|
|
673
|
-
`;
|
|
674
|
-
}
|
|
675
|
-
__name(generateSubscriptionHTML, "generateSubscriptionHTML");
|
|
676
|
-
ctx.command("subscription.clear", "删除群组的所有订阅").alias("清空订阅").userFields(["authority"]).action(async ({ session }) => {
|
|
849
|
+
ctx.command("subscription.clear [app:string]", "清空当前群订阅。可指定应用名或别名;不指定时清空全部。执行前会要求二次确认。").alias("清空订阅").userFields(["authority"]).example("清空订阅").example("清空订阅 twitter").example("清空订阅 推特").action(async ({ session }, app) => {
|
|
677
850
|
if (!await this.authCheck(session, config)) {
|
|
678
851
|
return "权限不足";
|
|
679
852
|
}
|
|
@@ -681,12 +854,20 @@ var SubscriptionService = class extends import_koishi.Service {
|
|
|
681
854
|
if (!groupId) {
|
|
682
855
|
return "请在群聊中使用当前指令";
|
|
683
856
|
}
|
|
684
|
-
const
|
|
857
|
+
const targetApp = app ? this.getApp(app) : void 0;
|
|
858
|
+
if (app && !targetApp) {
|
|
859
|
+
return `应用不存在:${app}
|
|
860
|
+
请使用配置中的应用名或应用别名,例如:清空订阅 twitter`;
|
|
861
|
+
}
|
|
862
|
+
const query = targetApp ? { groupId, app: targetApp } : { groupId };
|
|
863
|
+
const subscriptions = await ctx.database.get(this.subTableName, query);
|
|
864
|
+
const scopeText = targetApp ? `应用 ${targetApp}` : "所有应用";
|
|
685
865
|
if (subscriptions.length === 0) {
|
|
686
|
-
return
|
|
866
|
+
return `当前群暂无${scopeText}的订阅`;
|
|
687
867
|
}
|
|
688
|
-
|
|
689
|
-
|
|
868
|
+
await session.send(
|
|
869
|
+
`确认要清空当前群的${scopeText}订阅吗?
|
|
870
|
+
将删除 ${subscriptions.length} 个订阅。
|
|
690
871
|
请输入 "是" 或 "确认" 或 "yes" 来确认删除,输入其他内容取消。`
|
|
691
872
|
);
|
|
692
873
|
const response = await session.prompt(3e4);
|
|
@@ -694,13 +875,13 @@ var SubscriptionService = class extends import_koishi.Service {
|
|
|
694
875
|
return "操作已取消(超时)。";
|
|
695
876
|
}
|
|
696
877
|
if (["是", "确认", "yes", "y"].includes(response.toLowerCase())) {
|
|
697
|
-
await ctx.database.remove(
|
|
698
|
-
return
|
|
878
|
+
await ctx.database.remove(this.subTableName, query);
|
|
879
|
+
return `已删除当前群的${scopeText}订阅(共 ${subscriptions.length} 个)`;
|
|
699
880
|
} else {
|
|
700
881
|
return "操作已取消。";
|
|
701
882
|
}
|
|
702
883
|
});
|
|
703
|
-
ctx.command("subscription.stats", "
|
|
884
|
+
ctx.command("subscription.stats", "查看所有订阅的群组数量统计。", {
|
|
704
885
|
permissions: [`authority:${config.minAuthority}`],
|
|
705
886
|
hidden: true
|
|
706
887
|
}).alias("订阅统计").userFields(["authority"]).action(async ({ session }) => {
|
|
@@ -716,6 +897,13 @@ ${statsList}`;
|
|
|
716
897
|
});
|
|
717
898
|
}
|
|
718
899
|
};
|
|
900
|
+
|
|
901
|
+
// src/index.tsx
|
|
902
|
+
var name = "subscription";
|
|
903
|
+
var inject = {
|
|
904
|
+
required: ["database"],
|
|
905
|
+
optional: ["puppeteer"]
|
|
906
|
+
};
|
|
719
907
|
function apply(ctx, config) {
|
|
720
908
|
ctx.plugin(SubscriptionService, config);
|
|
721
909
|
}
|
|
@@ -723,6 +911,7 @@ __name(apply, "apply");
|
|
|
723
911
|
// Annotate the CommonJS export names for ESM import in node:
|
|
724
912
|
0 && (module.exports = {
|
|
725
913
|
Config,
|
|
914
|
+
SubscriptionService,
|
|
726
915
|
apply,
|
|
727
916
|
inject,
|
|
728
917
|
name
|
package/lib/render.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Dict } from 'koishi';
|
|
2
|
+
interface AccountItem {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
alias: string[];
|
|
6
|
+
}
|
|
7
|
+
interface ThemeInput {
|
|
8
|
+
[app: string]: string | undefined;
|
|
9
|
+
}
|
|
10
|
+
interface RenderOptions {
|
|
11
|
+
showTitle?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function generateSubscriptionHTML(groupId: string, availableAccounts: Dict<AccountItem[]>, subscribedByApp: Dict<Set<string>>, appThemeColors?: ThemeInput, options?: RenderOptions): string;
|
|
14
|
+
export {};
|
package/lib/service.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Context, Service, Fragment } from 'koishi';
|
|
2
|
+
import type { Config } from './config';
|
|
3
|
+
interface RecordsItem {
|
|
4
|
+
app: string;
|
|
5
|
+
id: string;
|
|
6
|
+
createdAt: Date;
|
|
7
|
+
}
|
|
8
|
+
interface SubscriptionItem {
|
|
9
|
+
id: number;
|
|
10
|
+
app: string;
|
|
11
|
+
account: string;
|
|
12
|
+
groupId: string;
|
|
13
|
+
createdAt: Date;
|
|
14
|
+
}
|
|
15
|
+
declare module 'koishi' {
|
|
16
|
+
interface Context {
|
|
17
|
+
subscription: SubscriptionService;
|
|
18
|
+
}
|
|
19
|
+
interface Tables {
|
|
20
|
+
subscription_service: SubscriptionItem;
|
|
21
|
+
subscription_records: RecordsItem;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export declare class SubscriptionService extends Service {
|
|
25
|
+
readonly subTableName = "subscription_service";
|
|
26
|
+
readonly recTableName = "subscription_records";
|
|
27
|
+
subsConfig: Config;
|
|
28
|
+
appMap: Map<string, string>;
|
|
29
|
+
accountMap: Map<string, string>;
|
|
30
|
+
constructor(ctx: Context, config: Config);
|
|
31
|
+
initMap(): void;
|
|
32
|
+
private getApp;
|
|
33
|
+
checkExist(app: string, id: string): Promise<boolean>;
|
|
34
|
+
getAccount(app: string, account: string): string;
|
|
35
|
+
getName(app: string, account: string): string;
|
|
36
|
+
getAvailableAccounts(app: string): string[];
|
|
37
|
+
getSubscribedGroups(app: string, account: string): Promise<string[]>;
|
|
38
|
+
broadcast(app: string, account: string, content: Fragment): Promise<void>;
|
|
39
|
+
private getSelfIds;
|
|
40
|
+
private getAssignedChannels;
|
|
41
|
+
broadcastForward(app: string, account: string, content: Fragment): Promise<any[]>;
|
|
42
|
+
addSubscription(app: string, account: string, groupId: string): Promise<{
|
|
43
|
+
status: boolean;
|
|
44
|
+
msg: string;
|
|
45
|
+
}>;
|
|
46
|
+
removeSubscription(app: string, account: string, groupId: string): Promise<boolean>;
|
|
47
|
+
getGroupSubscriptions(groupId: string): Promise<SubscriptionItem[]>;
|
|
48
|
+
getAllSubscriptions(): Promise<SubscriptionItem[]>;
|
|
49
|
+
getSubscriptionStats(): Promise<{
|
|
50
|
+
app: string;
|
|
51
|
+
account: string;
|
|
52
|
+
count: number;
|
|
53
|
+
}[]>;
|
|
54
|
+
private authCheck;
|
|
55
|
+
private getChannelId;
|
|
56
|
+
private getSubscriptionListView;
|
|
57
|
+
private renderSubscriptionListText;
|
|
58
|
+
private registerCommands;
|
|
59
|
+
}
|
|
60
|
+
export {};
|