koishi-plugin-apple-rank 0.0.1

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/index.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { Context, Schema } from 'koishi';
2
+ export declare const name = "apple-rank";
3
+ export interface Config {
4
+ country: string;
5
+ limit: 10 | 25 | 50 | 100 | 200;
6
+ cacheTtl: number;
7
+ aliases: Record<string, string>;
8
+ }
9
+ export declare const Config: Schema<Config>;
10
+ export declare function apply(ctx: Context, config: Config): void;
package/lib/index.js ADDED
@@ -0,0 +1,249 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
6
+ var __export = (target, all) => {
7
+ for (var name2 in all)
8
+ __defProp(target, name2, { get: all[name2], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ Config: () => Config,
24
+ apply: () => apply,
25
+ name: () => name
26
+ });
27
+ module.exports = __toCommonJS(src_exports);
28
+ var import_koishi = require("koishi");
29
+ var name = "apple-rank";
30
+ var Config = import_koishi.Schema.object({
31
+ country: import_koishi.Schema.string().default("cn").description("App Store 国家/地区代码"),
32
+ limit: import_koishi.Schema.union([10, 25, 50, 100, 200]).default(100).description("榜单拉取数量"),
33
+ cacheTtl: import_koishi.Schema.number().min(60).default(600).description("榜单缓存时间,单位秒"),
34
+ aliases: import_koishi.Schema.dict(String).default({
35
+ 王者荣耀: "989673964",
36
+ 和平精英: "1321803705",
37
+ 星铁: "1523037824",
38
+ 崩铁: "1523037824",
39
+ 崩坏星穹铁道: "1523037824",
40
+ 鸣潮: "6450693428",
41
+ 异环: "6514281568",
42
+ 终末地: "6753859465",
43
+ 明日方舟终末地: "6753859465",
44
+ "明日方舟:终末地": "6753859465"
45
+ }).description("游戏别名到 App Store appId 的映射")
46
+ });
47
+ var feedLabels = {
48
+ topgrossingapplications: "畅销榜",
49
+ topfreeapplications: "免费榜",
50
+ toppaidapplications: "付费榜"
51
+ };
52
+ function toArray(value) {
53
+ if (!value) return [];
54
+ return Array.isArray(value) ? value : [value];
55
+ }
56
+ __name(toArray, "toArray");
57
+ function getEntryUrl(entry) {
58
+ const links = toArray(entry.link);
59
+ return links.find((link) => link.attributes?.href)?.attributes?.href;
60
+ }
61
+ __name(getEntryUrl, "getEntryUrl");
62
+ function normalizeEntry(entry, index) {
63
+ return {
64
+ rank: index + 1,
65
+ appId: entry.id.attributes["im:id"],
66
+ name: entry["im:name"].label,
67
+ artist: entry["im:artist"].label,
68
+ category: entry.category?.attributes?.label,
69
+ bundleId: entry.id.attributes["im:bundleId"],
70
+ price: entry["im:price"]?.label,
71
+ url: getEntryUrl(entry),
72
+ icon: entry["im:image"]?.at(-1)?.label
73
+ };
74
+ }
75
+ __name(normalizeEntry, "normalizeEntry");
76
+ function formatRankItem(item) {
77
+ const category = item.category ? ` / ${item.category}` : "";
78
+ return `${item.rank}. ${item.name}${category}`;
79
+ }
80
+ __name(formatRankItem, "formatRankItem");
81
+ function formatRankItemWithIcon(item) {
82
+ const text = formatRankItem(item);
83
+ return item.icon ? `${import_koishi.h.image(item.icon)}${text}` : text;
84
+ }
85
+ __name(formatRankItemWithIcon, "formatRankItemWithIcon");
86
+ function formatUpdatedAt(updated) {
87
+ if (!updated) return "更新时间:未知";
88
+ const date = new Date(updated);
89
+ if (Number.isNaN(date.valueOf())) return `更新时间:${updated}`;
90
+ return `更新时间:${date.toLocaleString("zh-CN", { timeZone: "Asia/Shanghai", hour12: false })}`;
91
+ }
92
+ __name(formatUpdatedAt, "formatUpdatedAt");
93
+ function parseFeedType(type = "grossing") {
94
+ if (["grossing", "畅销", "畅销榜", "流水"].includes(type)) return "topgrossingapplications";
95
+ if (["free", "免费", "免费榜"].includes(type)) return "topfreeapplications";
96
+ if (["paid", "付费", "付费榜"].includes(type)) return "toppaidapplications";
97
+ }
98
+ __name(parseFeedType, "parseFeedType");
99
+ function parseJson(source) {
100
+ if (typeof source === "string") return JSON.parse(source);
101
+ return source;
102
+ }
103
+ __name(parseJson, "parseJson");
104
+ function apply(ctx, config) {
105
+ let cachedAt = 0;
106
+ let cachedFeed = "";
107
+ let cachedRankFeed = { ranks: [], actualLimit: 0 };
108
+ const recentReplies = /* @__PURE__ */ new Map();
109
+ async function fetchRanks(feedType = "topgrossingapplications") {
110
+ const now = Date.now();
111
+ if (cachedFeed === feedType && cachedRankFeed.ranks.length && now - cachedAt < config.cacheTtl * 1e3) {
112
+ return cachedRankFeed;
113
+ }
114
+ const url = `https://itunes.apple.com/${config.country}/rss/${feedType}/limit=${config.limit}/json`;
115
+ const response = parseJson(await ctx.http.get(url));
116
+ if (!response?.feed) {
117
+ throw new Error("Apple 榜单接口未返回 feed 数据");
118
+ }
119
+ const ranks = toArray(response.feed.entry).map(normalizeEntry);
120
+ cachedAt = now;
121
+ cachedFeed = feedType;
122
+ cachedRankFeed = { ranks, updated: response.feed.updated?.label, actualLimit: ranks.length };
123
+ return cachedRankFeed;
124
+ }
125
+ __name(fetchRanks, "fetchRanks");
126
+ function shouldReplyOnce(key) {
127
+ const now = Date.now();
128
+ const last = recentReplies.get(key);
129
+ if (last && now - last < 1500) return false;
130
+ recentReplies.set(key, now);
131
+ if (recentReplies.size > 100) {
132
+ for (const [storedKey, storedAt] of recentReplies) {
133
+ if (now - storedAt > 10 * 60 * 1e3) recentReplies.delete(storedKey);
134
+ }
135
+ }
136
+ return true;
137
+ }
138
+ __name(shouldReplyOnce, "shouldReplyOnce");
139
+ async function searchApp(keyword) {
140
+ const url = "https://itunes.apple.com/search";
141
+ const response = parseJson(await ctx.http.get(url, {
142
+ params: {
143
+ term: keyword,
144
+ country: config.country,
145
+ entity: "software",
146
+ limit: 5
147
+ }
148
+ }));
149
+ return response.results;
150
+ }
151
+ __name(searchApp, "searchApp");
152
+ async function resolveApp(keyword) {
153
+ const appId = config.aliases[keyword] || (/^\d+$/.test(keyword) ? keyword : void 0);
154
+ if (appId) return appId;
155
+ const results = await searchApp(keyword);
156
+ return results.find((result) => result.primaryGenreName === "Games")?.trackId.toString() || results[0]?.trackId.toString();
157
+ }
158
+ __name(resolveApp, "resolveApp");
159
+ async function findRank(keyword, feedType = "topgrossingapplications") {
160
+ const appId = await resolveApp(keyword);
161
+ if (!appId) return { appId: void 0, item: void 0 };
162
+ const feed = await fetchRanks(feedType);
163
+ return {
164
+ appId,
165
+ item: feed.ranks.find((item) => item.appId === appId),
166
+ updated: feed.updated,
167
+ actualLimit: feed.actualLimit
168
+ };
169
+ }
170
+ __name(findRank, "findRank");
171
+ ctx.command("ios榜 [type:string]", "查看中国区 App Store 榜单").option("limit", "-l <limit:number> 显示数量", { fallback: 10 }).action(async ({ options, session }, type) => {
172
+ if (session && !shouldReplyOnce(`${session.sid}:ios榜:${type || "grossing"}:${options.limit || 10}`)) return;
173
+ const feedType = parseFeedType(type);
174
+ if (!feedType) return "榜单类型可用:畅销 / 免费 / 付费";
175
+ const limit = Math.max(options.limit || 10, 1);
176
+ const feed = await fetchRanks(feedType);
177
+ const actualLimit = feed.actualLimit || feed.ranks.length;
178
+ const lines = feed.ranks.slice(0, Math.min(limit, actualLimit)).map(formatRankItemWithIcon);
179
+ return [`App Store ${config.country.toUpperCase()} ${feedLabels[feedType]} Top ${actualLimit}`, formatUpdatedAt(feed.updated), ...lines].join("\n");
180
+ });
181
+ ctx.command("ios排名 <game:text>", "查询游戏在 iOS 畅销榜的排名").option("type", "-t <type:string> 榜单类型:畅销/免费/付费", { fallback: "畅销" }).action(async ({ options, session }, game) => {
182
+ if (session && !shouldReplyOnce(`${session.sid}:ios排名:${options.type}:${game}`)) return;
183
+ if (!game) return "请输入游戏名,例如:ios排名 王者荣耀";
184
+ const feedType = parseFeedType(options.type);
185
+ if (!feedType) return "榜单类型可用:畅销 / 免费 / 付费";
186
+ const { appId, item, updated, actualLimit } = await findRank(game, feedType);
187
+ if (!appId) return `没找到「${game}」对应的 App Store 应用`;
188
+ if (!item) {
189
+ return `「${game}」未进入 App Store ${config.country.toUpperCase()} ${feedLabels[feedType]} Top ${actualLimit}
190
+ ${formatUpdatedAt(updated)}
191
+ 口径:Apple 公开榜单,只代表 iOS 排名,不代表全平台流水`;
192
+ }
193
+ return [
194
+ item.icon ? import_koishi.h.image(item.icon) : "",
195
+ `${item.name} iOS ${feedLabels[feedType]}排名:第 ${item.rank} 名`,
196
+ formatUpdatedAt(updated),
197
+ `开发者:${item.artist}`,
198
+ `分类:${item.category || "未知"}`,
199
+ "口径:Apple 公开榜单,只代表 iOS 排名,不代表全平台流水"
200
+ ].filter(Boolean).join("\n");
201
+ });
202
+ ctx.command("ios对比 <games:text>", "对比多个游戏的 iOS 畅销榜排名").action(async ({ session }, games) => {
203
+ if (session && !shouldReplyOnce(`${session.sid}:ios对比:${games}`)) return;
204
+ const names = games?.split(/\s+/).filter(Boolean) || [];
205
+ if (names.length < 2) return "请输入至少两个游戏名,例如:ios对比 王者荣耀 原神";
206
+ const feed = await fetchRanks();
207
+ const actualLimit = feed.actualLimit || feed.ranks.length;
208
+ const appIds = await Promise.all(names.map(resolveApp));
209
+ const results = names.map((game, index) => {
210
+ const appId = appIds[index];
211
+ if (!appId) return `${game}:未找到应用`;
212
+ const item = feed.ranks.find((item2) => item2.appId === appId);
213
+ if (!item) return `${game}:未进入畅销榜 Top ${actualLimit}`;
214
+ return [
215
+ item.icon ? import_koishi.h.image(item.icon) : "",
216
+ `${item.name}:第 ${item.rank} 名`
217
+ ].filter(Boolean).join("\n");
218
+ });
219
+ return [
220
+ `App Store ${config.country.toUpperCase()} 畅销榜对比`,
221
+ formatUpdatedAt(feed.updated),
222
+ ...results,
223
+ "口径:Apple 公开榜单,只代表 iOS 排名,不代表全平台流水"
224
+ ].join("\n");
225
+ });
226
+ ctx.command("ios搜索 <keyword:text>", "搜索 App Store 应用").action(async ({ session }, keyword) => {
227
+ if (session && !shouldReplyOnce(`${session.sid}:ios搜索:${keyword}`)) return;
228
+ if (!keyword) return "请输入关键词,例如:ios搜索 王者荣耀";
229
+ const results = await searchApp(keyword);
230
+ if (!results.length) return `没搜到「${keyword}」`;
231
+ return results.map((result, index) => {
232
+ const genres = result.genres?.join(" / ") || result.primaryGenreName;
233
+ const icon = result.artworkUrl100 ? `${import_koishi.h.image(result.artworkUrl100)}` : "";
234
+ return [
235
+ icon,
236
+ `${index + 1}. ${result.trackName} (${result.trackId})`,
237
+ `开发者:${result.artistName}`,
238
+ `分类:${genres}`
239
+ ].filter(Boolean).join("\n");
240
+ }).join("\n\n");
241
+ });
242
+ }
243
+ __name(apply, "apply");
244
+ // Annotate the CommonJS export names for ESM import in node:
245
+ 0 && (module.exports = {
246
+ Config,
247
+ apply,
248
+ name
249
+ });
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "koishi-plugin-apple-rank",
3
+ "description": "给二游痴看流水用的,数据来自苹果的rss",
4
+ "version": "0.0.1",
5
+ "main": "lib/index.js",
6
+ "typings": "lib/index.d.ts",
7
+ "files": [
8
+ "lib",
9
+ "dist"
10
+ ],
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "chatbot",
14
+ "koishi",
15
+ "plugin"
16
+ ],
17
+ "peerDependencies": {
18
+ "koishi": "^4.18.7"
19
+ }
20
+ }
package/readme.md ADDED
@@ -0,0 +1,5 @@
1
+ # koishi-plugin-apple-rank
2
+
3
+ [![npm](https://img.shields.io/npm/v/koishi-plugin-apple-rank?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-apple-rank)
4
+
5
+ 给二游痴看流水用的