koishi-plugin-subscription 0.0.7 → 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 -72
- package/lib/index.js +517 -360
- 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,77 +1,11 @@
|
|
|
1
|
-
import { Context
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
interface SubscriptionItem {
|
|
8
|
-
id: number;
|
|
9
|
-
app: string;
|
|
10
|
-
account: string;
|
|
11
|
-
groupId: string;
|
|
12
|
-
createdAt: Date;
|
|
13
|
-
}
|
|
14
|
-
interface AppConfig {
|
|
15
|
-
alias: string[];
|
|
16
|
-
allowedAccounts: {
|
|
17
|
-
id: string;
|
|
18
|
-
name: string;
|
|
19
|
-
alias: string;
|
|
20
|
-
}[];
|
|
21
|
-
}
|
|
22
|
-
export interface Config {
|
|
23
|
-
apps: Dict<AppConfig>;
|
|
24
|
-
minAuthority: number;
|
|
25
|
-
mode: "noAdmin" | "and" | "or";
|
|
26
|
-
}
|
|
27
|
-
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';
|
|
28
6
|
export declare const name = "subscription";
|
|
29
7
|
export declare const inject: {
|
|
30
8
|
required: string[];
|
|
31
9
|
optional: string[];
|
|
32
10
|
};
|
|
33
|
-
declare
|
|
34
|
-
interface Context {
|
|
35
|
-
subscription: SubscriptionService;
|
|
36
|
-
}
|
|
37
|
-
interface Tables {
|
|
38
|
-
subscription_service: SubscriptionItem;
|
|
39
|
-
subscription_records: RecordsItem;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
declare class SubscriptionService extends Service {
|
|
43
|
-
readonly subTableName = "subscription_service";
|
|
44
|
-
readonly recTableName = "subscription_records";
|
|
45
|
-
subsConfig: Config;
|
|
46
|
-
appMap: Map<string, string>;
|
|
47
|
-
accountMap: Map<string, string>;
|
|
48
|
-
constructor(ctx: Context, config: Config);
|
|
49
|
-
initMap(): void;
|
|
50
|
-
private getApp;
|
|
51
|
-
checkExist(app: string, id: string): Promise<boolean>;
|
|
52
|
-
getAccount(app: string, account: string): string;
|
|
53
|
-
getName(app: string, account: string): string;
|
|
54
|
-
getAvailableAccounts(app: string): string[];
|
|
55
|
-
getSubscribedGroups(app: string, account: string): Promise<string[]>;
|
|
56
|
-
broadcast(app: string, account: string, content: Fragment): Promise<void>;
|
|
57
|
-
private getSelfIds;
|
|
58
|
-
private getAssignedChannels;
|
|
59
|
-
broadcastForward(app: string, account: string, content: Fragment): Promise<any[]>;
|
|
60
|
-
addSubscription(app: string, account: string, groupId: string): Promise<{
|
|
61
|
-
status: boolean;
|
|
62
|
-
msg: string;
|
|
63
|
-
}>;
|
|
64
|
-
removeSubscription(app: string, account: string, groupId: string): Promise<boolean>;
|
|
65
|
-
getGroupSubscriptions(groupId: string): Promise<SubscriptionItem[]>;
|
|
66
|
-
getAllSubscriptions(): Promise<SubscriptionItem[]>;
|
|
67
|
-
getSubscriptionStats(): Promise<{
|
|
68
|
-
app: string;
|
|
69
|
-
account: string;
|
|
70
|
-
count: number;
|
|
71
|
-
}[]>;
|
|
72
|
-
private authCheck;
|
|
73
|
-
private getChannelId;
|
|
74
|
-
private registerCommands;
|
|
75
|
-
}
|
|
76
|
-
export declare function apply(ctx: Context, config: Config): void;
|
|
77
|
-
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
30
|
|
|
31
|
-
// src/
|
|
32
|
-
|
|
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
|
-
|
|
44
|
-
// src/index.tsx
|
|
45
|
-
var import_jsx_runtime = require("@satorijs/element/jsx-runtime");
|
|
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,16 +55,386 @@ 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";
|
|
81
439
|
recTableName = "subscription_records";
|
|
82
440
|
subsConfig;
|
|
@@ -131,6 +489,7 @@ var SubscriptionService = class extends import_koishi.Service {
|
|
|
131
489
|
}
|
|
132
490
|
}
|
|
133
491
|
getApp(app) {
|
|
492
|
+
if (!app) return;
|
|
134
493
|
return this.appMap.get(app.toLowerCase());
|
|
135
494
|
}
|
|
136
495
|
async checkExist(app, id) {
|
|
@@ -146,19 +505,19 @@ var SubscriptionService = class extends import_koishi.Service {
|
|
|
146
505
|
return false;
|
|
147
506
|
}
|
|
148
507
|
getAccount(app, account) {
|
|
508
|
+
if (!app || !account) return;
|
|
149
509
|
return this.accountMap.get((app + account).toLowerCase());
|
|
150
510
|
}
|
|
151
511
|
getName(app, account) {
|
|
152
512
|
app = this.getApp(app);
|
|
513
|
+
if (!app) return;
|
|
153
514
|
return this.subsConfig.apps[app].allowedAccounts.find((item) => item.id === this.getAccount(app, account))?.name;
|
|
154
515
|
}
|
|
155
|
-
// 获取对应应用可以订阅的账号
|
|
156
516
|
getAvailableAccounts(app) {
|
|
157
517
|
app = this.getApp(app);
|
|
158
518
|
const appConfig = this.subsConfig.apps[app];
|
|
159
519
|
return appConfig ? appConfig.allowedAccounts.map((item) => item.id) : [];
|
|
160
520
|
}
|
|
161
|
-
// 根据应用和账号获取订阅了这个账号的所有群组
|
|
162
521
|
async getSubscribedGroups(app, account) {
|
|
163
522
|
app = this.getApp(app), account = this.getAccount(app, account);
|
|
164
523
|
const subscriptions = await this.ctx.database.get(this.subTableName, {
|
|
@@ -199,6 +558,7 @@ var SubscriptionService = class extends import_koishi.Service {
|
|
|
199
558
|
if (index < 0) continue;
|
|
200
559
|
channels.splice(index, 1);
|
|
201
560
|
}
|
|
561
|
+
;
|
|
202
562
|
((assignMap[platform] ||= {})[assignee] ||= []).push(channel);
|
|
203
563
|
}
|
|
204
564
|
if (channels?.length) {
|
|
@@ -212,7 +572,7 @@ var SubscriptionService = class extends import_koishi.Service {
|
|
|
212
572
|
type: "message",
|
|
213
573
|
channel: {
|
|
214
574
|
id: selfChannelId,
|
|
215
|
-
type:
|
|
575
|
+
type: import_koishi2.Universal.Channel.Type.DIRECT
|
|
216
576
|
},
|
|
217
577
|
user: { id: bot.selfId }
|
|
218
578
|
});
|
|
@@ -240,7 +600,7 @@ var SubscriptionService = class extends import_koishi.Service {
|
|
|
240
600
|
const sessions = targets.map(({ id, guildId, locales }) => {
|
|
241
601
|
const session2 = bot.session({
|
|
242
602
|
type: "message",
|
|
243
|
-
channel: { id, type:
|
|
603
|
+
channel: { id, type: import_koishi2.Universal.Channel.Type.TEXT },
|
|
244
604
|
guild: { id: guildId }
|
|
245
605
|
});
|
|
246
606
|
session2.locales = locales;
|
|
@@ -253,13 +613,13 @@ var SubscriptionService = class extends import_koishi.Service {
|
|
|
253
613
|
}
|
|
254
614
|
}))).flat(1);
|
|
255
615
|
}
|
|
256
|
-
// 添加订阅
|
|
257
616
|
async addSubscription(app, account, groupId) {
|
|
258
|
-
app = this.getApp(app)
|
|
259
|
-
|
|
260
|
-
if (!appConfig) {
|
|
617
|
+
app = this.getApp(app);
|
|
618
|
+
if (!app) {
|
|
261
619
|
return { status: false, msg: "应用不存在" };
|
|
262
620
|
}
|
|
621
|
+
account = this.getAccount(app, account);
|
|
622
|
+
const appConfig = this.getAvailableAccounts(app);
|
|
263
623
|
if (!appConfig.includes(account)) {
|
|
264
624
|
return { status: false, msg: "账号不在白名单中" };
|
|
265
625
|
}
|
|
@@ -279,9 +639,11 @@ var SubscriptionService = class extends import_koishi.Service {
|
|
|
279
639
|
});
|
|
280
640
|
return { status: true, msg: "订阅成功" };
|
|
281
641
|
}
|
|
282
|
-
// 删除订阅
|
|
283
642
|
async removeSubscription(app, account, groupId) {
|
|
284
|
-
app = this.
|
|
643
|
+
app = this.getApp(app);
|
|
644
|
+
if (!app) return false;
|
|
645
|
+
account = this.getAccount(app, account);
|
|
646
|
+
if (!account) return false;
|
|
285
647
|
const result = await this.ctx.database.remove(this.subTableName, {
|
|
286
648
|
app,
|
|
287
649
|
account,
|
|
@@ -289,15 +651,12 @@ var SubscriptionService = class extends import_koishi.Service {
|
|
|
289
651
|
});
|
|
290
652
|
return result.removed > 0;
|
|
291
653
|
}
|
|
292
|
-
// 获取群组的所有订阅
|
|
293
654
|
async getGroupSubscriptions(groupId) {
|
|
294
655
|
return await this.ctx.database.get(this.subTableName, { groupId });
|
|
295
656
|
}
|
|
296
|
-
// 获取所有订阅(用于管理)
|
|
297
657
|
async getAllSubscriptions() {
|
|
298
658
|
return await this.ctx.database.get(this.subTableName, {});
|
|
299
659
|
}
|
|
300
|
-
// 按应用获取订阅统计
|
|
301
660
|
async getSubscriptionStats() {
|
|
302
661
|
const subscriptions = await this.getAllSubscriptions();
|
|
303
662
|
const stats = {};
|
|
@@ -328,50 +687,112 @@ var SubscriptionService = class extends import_koishi.Service {
|
|
|
328
687
|
if (!channel) return;
|
|
329
688
|
return `${channel.platform}:${channel.id}`;
|
|
330
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
|
+
}
|
|
331
742
|
registerCommands(ctx, config) {
|
|
332
|
-
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) => {
|
|
333
747
|
if (!await this.authCheck(session, config)) {
|
|
334
748
|
return "权限不足";
|
|
335
749
|
}
|
|
336
750
|
if (!app || !accounts || accounts.length == 0) {
|
|
337
|
-
return "
|
|
751
|
+
return "请指定应用名和账号。\n用法:订阅 <应用名> <账号> [...更多账号]\n示例:订阅 twitter aimi_sound ayasa_ito";
|
|
752
|
+
}
|
|
753
|
+
const appName = this.getApp(app);
|
|
754
|
+
if (!appName) {
|
|
755
|
+
return `应用不存在:${app}`;
|
|
338
756
|
}
|
|
339
|
-
accounts;
|
|
340
757
|
const groupId = this.getChannelId(session.channel);
|
|
341
758
|
if (!groupId) {
|
|
342
759
|
return "请在群聊中使用当前指令";
|
|
343
760
|
}
|
|
344
761
|
let result = "";
|
|
345
762
|
for (let account of accounts) {
|
|
346
|
-
|
|
347
|
-
const { status, msg } = await this.addSubscription(
|
|
763
|
+
const accountName = this.getName(appName, account) || account;
|
|
764
|
+
const { status, msg } = await this.addSubscription(appName, accountName, groupId);
|
|
348
765
|
if (status) {
|
|
349
|
-
result +=
|
|
766
|
+
result += `成功为当前群添加订阅:${appName} - ${accountName}
|
|
350
767
|
`;
|
|
351
768
|
} else {
|
|
352
|
-
result += `添加订阅 ${
|
|
769
|
+
result += `添加订阅 ${appName} - ${accountName} 失败,${msg}
|
|
353
770
|
`;
|
|
354
771
|
}
|
|
355
772
|
}
|
|
356
773
|
return result;
|
|
357
774
|
});
|
|
358
|
-
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) => {
|
|
359
776
|
if (!await this.authCheck(session, config)) {
|
|
360
777
|
return "权限不足";
|
|
361
778
|
}
|
|
362
779
|
if (!app || !account) {
|
|
363
|
-
return "
|
|
780
|
+
return "请指定应用名和账号。\n用法:取消订阅 <应用名> <账号>\n示例:取消订阅 twitter aimi_sound";
|
|
781
|
+
}
|
|
782
|
+
const appName = this.getApp(app);
|
|
783
|
+
if (!appName) {
|
|
784
|
+
return `应用不存在:${app}`;
|
|
364
785
|
}
|
|
365
|
-
|
|
786
|
+
const accountName = this.getName(appName, account) || account;
|
|
366
787
|
const groupId = this.getChannelId(session.channel);
|
|
367
788
|
if (!groupId) {
|
|
368
789
|
return "请在群聊中使用当前指令";
|
|
369
790
|
}
|
|
370
|
-
const success = await this.removeSubscription(
|
|
791
|
+
const success = await this.removeSubscription(appName, accountName, groupId);
|
|
371
792
|
if (success) {
|
|
372
|
-
return
|
|
793
|
+
return `成功为当前群删除订阅:${appName} - ${accountName}`;
|
|
373
794
|
} else {
|
|
374
|
-
return
|
|
795
|
+
return "删除订阅失败,可能的原因:订阅不存在";
|
|
375
796
|
}
|
|
376
797
|
});
|
|
377
798
|
for (const app in config.apps) {
|
|
@@ -382,330 +803,50 @@ var SubscriptionService = class extends import_koishi.Service {
|
|
|
382
803
|
unsub.alias(`取消${appAlias}订阅`, { args: [app] });
|
|
383
804
|
}
|
|
384
805
|
}
|
|
385
|
-
ctx.command("subscription.list", "
|
|
386
|
-
|
|
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) {
|
|
387
809
|
return "权限不足";
|
|
388
810
|
}
|
|
389
811
|
const groupId = this.getChannelId(session.channel);
|
|
390
812
|
if (!groupId) {
|
|
391
813
|
return "请在群聊中使用当前指令";
|
|
392
814
|
}
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
if (!subscribedByApp[sub2.app]) {
|
|
397
|
-
subscribedByApp[sub2.app] = /* @__PURE__ */ new Set();
|
|
398
|
-
}
|
|
399
|
-
subscribedByApp[sub2.app].add(sub2.account);
|
|
400
|
-
});
|
|
401
|
-
const availableAccounts = {};
|
|
402
|
-
const appConfig = this.subsConfig.apps;
|
|
403
|
-
for (const app in appConfig) {
|
|
404
|
-
availableAccounts[app] = appConfig[app].allowedAccounts.map((account) => ({
|
|
405
|
-
id: account.id,
|
|
406
|
-
name: account.name,
|
|
407
|
-
alias: account.alias ? account.alias.split(",") : []
|
|
408
|
-
}));
|
|
409
|
-
}
|
|
815
|
+
const view = await this.getSubscriptionListView(app, groupId);
|
|
816
|
+
if (typeof view === "string") return view;
|
|
817
|
+
const textFallback = /* @__PURE__ */ __name(() => this.renderSubscriptionListText(view), "textFallback");
|
|
410
818
|
if (!ctx.puppeteer) {
|
|
411
|
-
|
|
412
|
-
for (const app in availableAccounts) {
|
|
413
|
-
result += `【${app}】
|
|
414
|
-
`;
|
|
415
|
-
const subscribed = subscribedByApp[app] || /* @__PURE__ */ new Set();
|
|
416
|
-
availableAccounts[app].forEach((account) => {
|
|
417
|
-
const isSubscribed = subscribed.has(account.id);
|
|
418
|
-
const status = isSubscribed ? "✅ 已订阅" : "❌ 未订阅";
|
|
419
|
-
const aliases = account.alias.length > 0 ? ` (${account.alias.join(", ")})` : "";
|
|
420
|
-
result += `${status} ${account.name} id:${account.id}
|
|
421
|
-
${aliases}
|
|
422
|
-
`;
|
|
423
|
-
});
|
|
424
|
-
result += "\n";
|
|
425
|
-
}
|
|
426
|
-
return result;
|
|
819
|
+
return textFallback();
|
|
427
820
|
}
|
|
428
|
-
const html = generateSubscriptionHTML(groupId, availableAccounts, subscribedByApp
|
|
429
|
-
|
|
821
|
+
const html = generateSubscriptionHTML(groupId, view.availableAccounts, view.subscribedByApp, view.appThemeColors, {
|
|
822
|
+
showTitle: !view.targetApp
|
|
823
|
+
});
|
|
824
|
+
let page;
|
|
430
825
|
try {
|
|
826
|
+
page = await ctx.puppeteer.page();
|
|
827
|
+
page.setDefaultTimeout?.(15e3);
|
|
431
828
|
await page.setViewport({
|
|
432
|
-
width:
|
|
433
|
-
height:
|
|
829
|
+
width: screenshotWidth,
|
|
830
|
+
height: 1,
|
|
831
|
+
deviceScaleFactor: screenshotDeviceScaleFactor
|
|
434
832
|
});
|
|
435
833
|
await page.setContent(html, {
|
|
436
|
-
waitUntil: "
|
|
834
|
+
waitUntil: "load"
|
|
437
835
|
});
|
|
438
836
|
const image = await page.screenshot({
|
|
439
837
|
type: "jpeg",
|
|
838
|
+
quality: screenshotQuality,
|
|
440
839
|
fullPage: true
|
|
441
840
|
});
|
|
442
|
-
|
|
841
|
+
const base64 = image.toString("base64");
|
|
842
|
+
return `<image url="data:image/jpeg;base64,${base64}" />`;
|
|
843
|
+
} catch (error) {
|
|
844
|
+
return textFallback();
|
|
443
845
|
} finally {
|
|
444
|
-
await page
|
|
846
|
+
await page?.close();
|
|
445
847
|
}
|
|
446
848
|
});
|
|
447
|
-
|
|
448
|
-
return `
|
|
449
|
-
<!DOCTYPE html>
|
|
450
|
-
<html>
|
|
451
|
-
<head>
|
|
452
|
-
<meta charset="UTF-8">
|
|
453
|
-
<style>
|
|
454
|
-
* {
|
|
455
|
-
margin: 0;
|
|
456
|
-
padding: 0;
|
|
457
|
-
box-sizing: border-box;
|
|
458
|
-
}
|
|
459
|
-
body {
|
|
460
|
-
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
|
461
|
-
background: #f5f7fa;
|
|
462
|
-
min-height: 100vh;
|
|
463
|
-
padding: 20px;
|
|
464
|
-
}
|
|
465
|
-
.container {
|
|
466
|
-
max-width: 1000px;
|
|
467
|
-
margin: 0 auto;
|
|
468
|
-
}
|
|
469
|
-
.header {
|
|
470
|
-
text-align: center;
|
|
471
|
-
margin-bottom: 20px;
|
|
472
|
-
color: #2c3e50;
|
|
473
|
-
}
|
|
474
|
-
.header h1 {
|
|
475
|
-
font-size: 36px;
|
|
476
|
-
margin-bottom: 12px;
|
|
477
|
-
font-weight: 900;
|
|
478
|
-
}
|
|
479
|
-
.header .subtitle {
|
|
480
|
-
font-size: 21px;
|
|
481
|
-
color: #7f8c8d;
|
|
482
|
-
}
|
|
483
|
-
.apps-container {
|
|
484
|
-
background: white;
|
|
485
|
-
border-radius: 12px;
|
|
486
|
-
box-shadow: 0 3px 18px rgba(0,0,0,0.08);
|
|
487
|
-
overflow: hidden;
|
|
488
|
-
}
|
|
489
|
-
.app-section {
|
|
490
|
-
border-bottom: 1px solid #eaeaea;
|
|
491
|
-
}
|
|
492
|
-
.app-section:last-child {
|
|
493
|
-
border-bottom: none;
|
|
494
|
-
}
|
|
495
|
-
.app-title {
|
|
496
|
-
font-size: 24px;
|
|
497
|
-
font-weight: 600;
|
|
498
|
-
color: #34495e;
|
|
499
|
-
padding: 18px 24px;
|
|
500
|
-
background: #f8f9fa;
|
|
501
|
-
border-bottom: 1px solid #eaeaea;
|
|
502
|
-
display: flex;
|
|
503
|
-
align-items: center;
|
|
504
|
-
gap: 12px;
|
|
505
|
-
}
|
|
506
|
-
.app-title::before {
|
|
507
|
-
content: "▸";
|
|
508
|
-
font-size: 21px;
|
|
509
|
-
color: #3498db;
|
|
510
|
-
}
|
|
511
|
-
.grid {
|
|
512
|
-
display: grid;
|
|
513
|
-
grid-template-columns: repeat(2, 1fr);
|
|
514
|
-
gap: 0;
|
|
515
|
-
}
|
|
516
|
-
.account-card {
|
|
517
|
-
padding: 21px;
|
|
518
|
-
border-right: 1px solid #f0f0f0;
|
|
519
|
-
border-bottom: 1px solid #f0f0f0;
|
|
520
|
-
transition: all 0.2s ease;
|
|
521
|
-
position: relative;
|
|
522
|
-
min-height: 120px;
|
|
523
|
-
display: flex;
|
|
524
|
-
flex-direction: column;
|
|
525
|
-
}
|
|
526
|
-
.account-card:nth-child(4n+1),
|
|
527
|
-
.account-card:nth-child(4n+4) {
|
|
528
|
-
background: #f8f9fa;
|
|
529
|
-
}
|
|
530
|
-
.account-card:nth-child(4n+2),
|
|
531
|
-
.account-card:nth-child(4n+3) {
|
|
532
|
-
background: #f0f2f5;
|
|
533
|
-
}
|
|
534
|
-
.account-card:hover {
|
|
535
|
-
background: #e6f3ff !important;
|
|
536
|
-
}
|
|
537
|
-
.account-card:nth-child(2n) {
|
|
538
|
-
border-right: none;
|
|
539
|
-
}
|
|
540
|
-
.account-card:last-child {
|
|
541
|
-
border-right: none;
|
|
542
|
-
}
|
|
543
|
-
.name-id-container {
|
|
544
|
-
display: flex;
|
|
545
|
-
justify-content: space-between;
|
|
546
|
-
align-items: center;
|
|
547
|
-
margin-bottom: 12px;
|
|
548
|
-
flex-wrap: wrap;
|
|
549
|
-
}
|
|
550
|
-
.name {
|
|
551
|
-
font-size: 21px;
|
|
552
|
-
font-weight: 900;
|
|
553
|
-
color: #2c3e50;
|
|
554
|
-
line-height: 1.3;
|
|
555
|
-
max-width: 60%;
|
|
556
|
-
overflow: hidden;
|
|
557
|
-
text-overflow: ellipsis;
|
|
558
|
-
white-space: nowrap;
|
|
559
|
-
}
|
|
560
|
-
.id {
|
|
561
|
-
font-size: 21px;
|
|
562
|
-
color: #5a6c7d;
|
|
563
|
-
font-family: 'Courier New', monospace;
|
|
564
|
-
background: #f8f9fa;
|
|
565
|
-
padding: 4.5px 9px;
|
|
566
|
-
border-radius: 4.5px;
|
|
567
|
-
line-height: 1.3;
|
|
568
|
-
max-width: 35%;
|
|
569
|
-
overflow: hidden;
|
|
570
|
-
text-overflow: ellipsis;
|
|
571
|
-
white-space: nowrap;
|
|
572
|
-
}
|
|
573
|
-
.name-id-container.vertical {
|
|
574
|
-
flex-direction: column;
|
|
575
|
-
align-items: flex-start;
|
|
576
|
-
}
|
|
577
|
-
.name-id-container.vertical .name {
|
|
578
|
-
max-width: 100%;
|
|
579
|
-
margin-bottom: 6px;
|
|
580
|
-
}
|
|
581
|
-
.name-id-container.vertical .id {
|
|
582
|
-
max-width: 100%;
|
|
583
|
-
align-self: flex-end;
|
|
584
|
-
}
|
|
585
|
-
.alias-status-container {
|
|
586
|
-
display: flex;
|
|
587
|
-
justify-content: space-between;
|
|
588
|
-
margin-top: 12px;
|
|
589
|
-
flex-grow: 1;
|
|
590
|
-
}
|
|
591
|
-
.alias-container {
|
|
592
|
-
flex: 1;
|
|
593
|
-
max-width: calc(100% - 105px);
|
|
594
|
-
align-self: flex-start;
|
|
595
|
-
}
|
|
596
|
-
.alias {
|
|
597
|
-
font-size: 20px;
|
|
598
|
-
color: #6c757d;
|
|
599
|
-
display: flex;
|
|
600
|
-
flex-wrap: wrap;
|
|
601
|
-
gap: 6px;
|
|
602
|
-
align-items: flex-start;
|
|
603
|
-
}
|
|
604
|
-
.alias-tag {
|
|
605
|
-
background: #e8f4fd;
|
|
606
|
-
color: #1971c2;
|
|
607
|
-
padding: 3px 9px;
|
|
608
|
-
border-radius: 15px;
|
|
609
|
-
border: 1px solid #a5d8ff;
|
|
610
|
-
white-space: nowrap;
|
|
611
|
-
margin-bottom: 3px;
|
|
612
|
-
}
|
|
613
|
-
.no-alias {
|
|
614
|
-
font-size: 20px;
|
|
615
|
-
color: #adb5bd;
|
|
616
|
-
font-style: italic;
|
|
617
|
-
}
|
|
618
|
-
.status-container {
|
|
619
|
-
display: flex;
|
|
620
|
-
align-items: flex-end;
|
|
621
|
-
margin-left: 12px;
|
|
622
|
-
}
|
|
623
|
-
.status {
|
|
624
|
-
display: inline-flex;
|
|
625
|
-
align-items: center;
|
|
626
|
-
gap: 9px;
|
|
627
|
-
padding: 4.5px 15px;
|
|
628
|
-
border-radius: 24px;
|
|
629
|
-
font-size: 20px;
|
|
630
|
-
font-weight: 900;
|
|
631
|
-
white-space: nowrap;
|
|
632
|
-
flex-shrink: 0;
|
|
633
|
-
}
|
|
634
|
-
.subscribed {
|
|
635
|
-
background: #d4edda;
|
|
636
|
-
color: #155724;
|
|
637
|
-
border: 1px solid #c3e6cb;
|
|
638
|
-
}
|
|
639
|
-
.unsubscribed {
|
|
640
|
-
background: #f8d7da;
|
|
641
|
-
color: #721c24;
|
|
642
|
-
border: 1px solid #f5c6cb;
|
|
643
|
-
}
|
|
644
|
-
.empty-state {
|
|
645
|
-
text-align: center;
|
|
646
|
-
color: #6c757d;
|
|
647
|
-
font-style: italic;
|
|
648
|
-
padding: 45px;
|
|
649
|
-
background: #f8f9fa;
|
|
650
|
-
grid-column: 1 / -1;
|
|
651
|
-
}
|
|
652
|
-
</style>
|
|
653
|
-
</head>
|
|
654
|
-
<body>
|
|
655
|
-
<div class="container">
|
|
656
|
-
<div class="header">
|
|
657
|
-
<h1>订阅状态总览</h1>
|
|
658
|
-
</div>
|
|
659
|
-
|
|
660
|
-
<div class="apps-container">
|
|
661
|
-
${Object.entries(availableAccounts).map(([app, accounts]) => {
|
|
662
|
-
const subscribed = subscribedByApp[app] || /* @__PURE__ */ new Set();
|
|
663
|
-
return `
|
|
664
|
-
<div class="app-section">
|
|
665
|
-
<div class="app-title">${app}</div>
|
|
666
|
-
<div class="grid">
|
|
667
|
-
${accounts.length > 0 ? accounts.map((account) => {
|
|
668
|
-
const isSubscribed = subscribed.has(account.id);
|
|
669
|
-
const needsVerticalLayout = account.name.length > 15 || account.id.length > 20;
|
|
670
|
-
return `
|
|
671
|
-
<div class="account-card">
|
|
672
|
-
<div class="name-id-container ${needsVerticalLayout ? "vertical" : ""}">
|
|
673
|
-
<div class="name">${account.name}</div>
|
|
674
|
-
<div class="id">${account.id}</div>
|
|
675
|
-
</div>
|
|
676
|
-
<div class="alias-status-container">
|
|
677
|
-
<div class="alias-container">
|
|
678
|
-
${account.alias.length > 0 ? `
|
|
679
|
-
<div class="alias">
|
|
680
|
-
${account.alias.map((alias) => `<span class="alias-tag">${alias}</span>`).join("")}
|
|
681
|
-
</div>
|
|
682
|
-
` : `<div class="no-alias">无别名</div>`}
|
|
683
|
-
</div>
|
|
684
|
-
<div class="status-container">
|
|
685
|
-
<div class="status ${isSubscribed ? "subscribed" : "unsubscribed"}">
|
|
686
|
-
${isSubscribed ? "✅ 已订阅" : "❌ 未订阅"}
|
|
687
|
-
</div>
|
|
688
|
-
</div>
|
|
689
|
-
</div>
|
|
690
|
-
</div>
|
|
691
|
-
`;
|
|
692
|
-
}).join("") : `
|
|
693
|
-
<div class="empty-state">
|
|
694
|
-
暂无可用账号
|
|
695
|
-
</div>
|
|
696
|
-
`}
|
|
697
|
-
</div>
|
|
698
|
-
</div>
|
|
699
|
-
`;
|
|
700
|
-
}).join("")}
|
|
701
|
-
</div>
|
|
702
|
-
</div>
|
|
703
|
-
</body>
|
|
704
|
-
</html>
|
|
705
|
-
`;
|
|
706
|
-
}
|
|
707
|
-
__name(generateSubscriptionHTML, "generateSubscriptionHTML");
|
|
708
|
-
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) => {
|
|
709
850
|
if (!await this.authCheck(session, config)) {
|
|
710
851
|
return "权限不足";
|
|
711
852
|
}
|
|
@@ -713,12 +854,20 @@ var SubscriptionService = class extends import_koishi.Service {
|
|
|
713
854
|
if (!groupId) {
|
|
714
855
|
return "请在群聊中使用当前指令";
|
|
715
856
|
}
|
|
716
|
-
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}` : "所有应用";
|
|
717
865
|
if (subscriptions.length === 0) {
|
|
718
|
-
return
|
|
866
|
+
return `当前群暂无${scopeText}的订阅`;
|
|
719
867
|
}
|
|
720
|
-
|
|
721
|
-
|
|
868
|
+
await session.send(
|
|
869
|
+
`确认要清空当前群的${scopeText}订阅吗?
|
|
870
|
+
将删除 ${subscriptions.length} 个订阅。
|
|
722
871
|
请输入 "是" 或 "确认" 或 "yes" 来确认删除,输入其他内容取消。`
|
|
723
872
|
);
|
|
724
873
|
const response = await session.prompt(3e4);
|
|
@@ -726,13 +875,13 @@ var SubscriptionService = class extends import_koishi.Service {
|
|
|
726
875
|
return "操作已取消(超时)。";
|
|
727
876
|
}
|
|
728
877
|
if (["是", "确认", "yes", "y"].includes(response.toLowerCase())) {
|
|
729
|
-
await ctx.database.remove(
|
|
730
|
-
return
|
|
878
|
+
await ctx.database.remove(this.subTableName, query);
|
|
879
|
+
return `已删除当前群的${scopeText}订阅(共 ${subscriptions.length} 个)`;
|
|
731
880
|
} else {
|
|
732
881
|
return "操作已取消。";
|
|
733
882
|
}
|
|
734
883
|
});
|
|
735
|
-
ctx.command("subscription.stats", "
|
|
884
|
+
ctx.command("subscription.stats", "查看所有订阅的群组数量统计。", {
|
|
736
885
|
permissions: [`authority:${config.minAuthority}`],
|
|
737
886
|
hidden: true
|
|
738
887
|
}).alias("订阅统计").userFields(["authority"]).action(async ({ session }) => {
|
|
@@ -748,6 +897,13 @@ ${statsList}`;
|
|
|
748
897
|
});
|
|
749
898
|
}
|
|
750
899
|
};
|
|
900
|
+
|
|
901
|
+
// src/index.tsx
|
|
902
|
+
var name = "subscription";
|
|
903
|
+
var inject = {
|
|
904
|
+
required: ["database"],
|
|
905
|
+
optional: ["puppeteer"]
|
|
906
|
+
};
|
|
751
907
|
function apply(ctx, config) {
|
|
752
908
|
ctx.plugin(SubscriptionService, config);
|
|
753
909
|
}
|
|
@@ -755,6 +911,7 @@ __name(apply, "apply");
|
|
|
755
911
|
// Annotate the CommonJS export names for ESM import in node:
|
|
756
912
|
0 && (module.exports = {
|
|
757
913
|
Config,
|
|
914
|
+
SubscriptionService,
|
|
758
915
|
apply,
|
|
759
916
|
inject,
|
|
760
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 {};
|