@fraqjs/plugin-mock 0.1.0
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/dist/index.d.mts +203 -0
- package/dist/index.mjs +1070 -0
- package/package.json +32 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { AnyApiCall, Context, ContextOptions, LogHandler, MilkyEventSource, MilkyEventSubscription, milky } from "@fraqjs/fraq";
|
|
2
|
+
//#region src/entity.d.ts
|
|
3
|
+
interface RandomFriendOverrides extends Partial<Omit<milky.FriendEntity, 'category'>> {
|
|
4
|
+
category?: Partial<milky.FriendCategoryEntity>;
|
|
5
|
+
}
|
|
6
|
+
interface RandomGroupOverrides extends Partial<milky.GroupEntity> {}
|
|
7
|
+
interface RandomGroupMemberOverrides extends Partial<milky.GroupMemberEntity> {}
|
|
8
|
+
declare function createRandomFriend(userId: number, overrides?: RandomFriendOverrides): milky.FriendEntity;
|
|
9
|
+
declare function createRandomGroup(groupId: number, overrides?: RandomGroupOverrides): milky.GroupEntity;
|
|
10
|
+
declare function createRandomGroupMember(groupId: number, userId: number, overrides?: RandomGroupMemberOverrides): milky.GroupMemberEntity;
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/inbox.d.ts
|
|
13
|
+
type MockMessageScene = 'friend' | 'group' | 'temp';
|
|
14
|
+
interface MockMessageContext {
|
|
15
|
+
scene: MockMessageScene;
|
|
16
|
+
peerId: number;
|
|
17
|
+
senderId: number;
|
|
18
|
+
groupId?: number | undefined;
|
|
19
|
+
}
|
|
20
|
+
interface MockInboxOptions {
|
|
21
|
+
selfId?: number;
|
|
22
|
+
baseTime?: number;
|
|
23
|
+
sequenceStart?: number;
|
|
24
|
+
timeStepSeconds?: number;
|
|
25
|
+
conversationKey?(context: MockMessageContext): string;
|
|
26
|
+
}
|
|
27
|
+
interface MockMessageMetaOverrides {
|
|
28
|
+
messageSeq?: number;
|
|
29
|
+
time?: number;
|
|
30
|
+
}
|
|
31
|
+
interface MockFriendMessageOptions extends MockMessageMetaOverrides {
|
|
32
|
+
userId: number;
|
|
33
|
+
peerId?: number;
|
|
34
|
+
senderId?: number;
|
|
35
|
+
friend?: RandomFriendOverrides;
|
|
36
|
+
}
|
|
37
|
+
interface MockGroupMessageOptions extends MockMessageMetaOverrides {
|
|
38
|
+
groupId: number;
|
|
39
|
+
userId: number;
|
|
40
|
+
senderId?: number;
|
|
41
|
+
group?: RandomGroupOverrides;
|
|
42
|
+
groupMember?: RandomGroupMemberOverrides;
|
|
43
|
+
}
|
|
44
|
+
interface MockTempMessageOptions extends MockMessageMetaOverrides {
|
|
45
|
+
userId: number;
|
|
46
|
+
peerId?: number;
|
|
47
|
+
senderId?: number;
|
|
48
|
+
groupId?: number;
|
|
49
|
+
group?: RandomGroupOverrides | null;
|
|
50
|
+
}
|
|
51
|
+
interface MockMessageEventOverrides {
|
|
52
|
+
time?: number;
|
|
53
|
+
selfId?: number;
|
|
54
|
+
}
|
|
55
|
+
declare class MockInbox {
|
|
56
|
+
readonly selfId: number;
|
|
57
|
+
private readonly baseTime;
|
|
58
|
+
private readonly sequenceStart;
|
|
59
|
+
private readonly timeStepSeconds;
|
|
60
|
+
private readonly conversationKeyOf;
|
|
61
|
+
private readonly conversations;
|
|
62
|
+
private readonly allMessages;
|
|
63
|
+
private readonly friends;
|
|
64
|
+
private readonly groups;
|
|
65
|
+
private readonly groupMembers;
|
|
66
|
+
constructor(options?: MockInboxOptions);
|
|
67
|
+
conversationKey(context: MockMessageContext): string;
|
|
68
|
+
friendConversationKey(peerId: number, senderId?: number): string;
|
|
69
|
+
groupConversationKey(groupId: number, senderId?: number): string;
|
|
70
|
+
tempConversationKey(options: {
|
|
71
|
+
peerId?: number;
|
|
72
|
+
senderId: number;
|
|
73
|
+
groupId?: number;
|
|
74
|
+
}): string;
|
|
75
|
+
friend(options: MockFriendMessageOptions, segments: readonly milky.IncomingSegment_ZodInput[] | readonly milky.IncomingSegment[]): milky.IncomingFriendMessage;
|
|
76
|
+
group(options: MockGroupMessageOptions, segments: readonly milky.IncomingSegment_ZodInput[] | readonly milky.IncomingSegment[]): milky.IncomingGroupMessage;
|
|
77
|
+
temp(options: MockTempMessageOptions, segments: readonly milky.IncomingSegment_ZodInput[] | readonly milky.IncomingSegment[]): milky.IncomingTempMessage;
|
|
78
|
+
event(message: milky.IncomingMessage, overrides?: MockMessageEventOverrides): milky.MessageReceiveEvent;
|
|
79
|
+
friendEvent(options: MockFriendMessageOptions, segments: readonly milky.IncomingSegment_ZodInput[] | readonly milky.IncomingSegment[], overrides?: MockMessageEventOverrides): milky.MessageReceiveEvent;
|
|
80
|
+
groupEvent(options: MockGroupMessageOptions, segments: readonly milky.IncomingSegment_ZodInput[] | readonly milky.IncomingSegment[], overrides?: MockMessageEventOverrides): milky.MessageReceiveEvent;
|
|
81
|
+
tempEvent(options: MockTempMessageOptions, segments: readonly milky.IncomingSegment_ZodInput[] | readonly milky.IncomingSegment[], overrides?: MockMessageEventOverrides): milky.MessageReceiveEvent;
|
|
82
|
+
message(conversationKey: string, messageSeq: number): milky.IncomingMessage | undefined;
|
|
83
|
+
getMessage(input: milky.GetMessageInput_ZodInput): milky.GetMessageOutput;
|
|
84
|
+
getHistoryMessages(input: milky.GetHistoryMessagesInput_ZodInput): milky.GetHistoryMessagesOutput;
|
|
85
|
+
markMessageAsRead(input: milky.MarkMessageAsReadInput_ZodInput): milky.MarkMessageAsReadOutput;
|
|
86
|
+
getFriendInfo(input: milky.GetFriendInfoInput_ZodInput): milky.GetFriendInfoOutput;
|
|
87
|
+
getGroupInfo(input: milky.GetGroupInfoInput_ZodInput): milky.GetGroupInfoOutput;
|
|
88
|
+
getGroupMemberInfo(input: milky.GetGroupMemberInfoInput_ZodInput): milky.GetGroupMemberInfoOutput;
|
|
89
|
+
history(conversationKey?: string): milky.IncomingMessage[];
|
|
90
|
+
lastMessage(conversationKey?: string): milky.IncomingMessage | undefined;
|
|
91
|
+
reset(): void;
|
|
92
|
+
private allocateMessageMeta;
|
|
93
|
+
private getOrCreateConversationState;
|
|
94
|
+
private storeMessage;
|
|
95
|
+
private queryConversationKey;
|
|
96
|
+
}
|
|
97
|
+
declare function createMockInbox(options?: MockInboxOptions): MockInbox;
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/service.d.ts
|
|
100
|
+
interface MockApiCall {
|
|
101
|
+
endpoint: string;
|
|
102
|
+
params?: unknown;
|
|
103
|
+
}
|
|
104
|
+
interface MockServiceOptions extends MockInboxOptions {
|
|
105
|
+
inbox?: MockInbox;
|
|
106
|
+
}
|
|
107
|
+
declare class MockService implements MilkyEventSource {
|
|
108
|
+
readonly name = "mock";
|
|
109
|
+
readonly inbox: MockInbox;
|
|
110
|
+
readonly apiCalls: MockApiCall[];
|
|
111
|
+
private onEvent;
|
|
112
|
+
private closeEventsResolver;
|
|
113
|
+
startEventCalls: number;
|
|
114
|
+
private nextStartError;
|
|
115
|
+
constructor(options?: MockServiceOptions);
|
|
116
|
+
start(handler: (event: milky.Event) => void | Promise<void>): Promise<MilkyEventSubscription>;
|
|
117
|
+
closeEvents(): void;
|
|
118
|
+
failNextStart(error: unknown): void;
|
|
119
|
+
emitEvent(event: milky.Event): Promise<void>;
|
|
120
|
+
receiveFriend(options: MockFriendMessageOptions, segments: readonly milky.IncomingSegment_ZodInput[] | readonly milky.IncomingSegment[]): Promise<milky.IncomingFriendMessage>;
|
|
121
|
+
receiveGroup(options: MockGroupMessageOptions, segments: readonly milky.IncomingSegment_ZodInput[] | readonly milky.IncomingSegment[]): Promise<milky.IncomingGroupMessage>;
|
|
122
|
+
receiveTemp(options: MockTempMessageOptions, segments: readonly milky.IncomingSegment_ZodInput[] | readonly milky.IncomingSegment[]): Promise<milky.IncomingTempMessage>;
|
|
123
|
+
handleApiCall(call: AnyApiCall): unknown;
|
|
124
|
+
reset(): void;
|
|
125
|
+
}
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/logging.d.ts
|
|
128
|
+
declare function createSimpleLogHandler(): LogHandler;
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region src/message.d.ts
|
|
131
|
+
type InmsgTemplateValue = string | number | boolean | milky.IncomingSegment_ZodInput;
|
|
132
|
+
interface IncomingReplySource {
|
|
133
|
+
message_seq: number;
|
|
134
|
+
sender_id: number;
|
|
135
|
+
sender_name?: string | null | undefined;
|
|
136
|
+
time: number;
|
|
137
|
+
segments: milky.IncomingSegment[] | milky.IncomingSegment_ZodInput[];
|
|
138
|
+
}
|
|
139
|
+
declare function inmsg(strings: TemplateStringsArray, ...values: InmsgTemplateValue[]): milky.IncomingSegment_ZodInput[];
|
|
140
|
+
declare namespace inseg {
|
|
141
|
+
function text(text: string): milky.IncomingTextSegment_ZodInput;
|
|
142
|
+
function mention(userId: number, name?: string): milky.IncomingMentionSegment_ZodInput;
|
|
143
|
+
function mentionAll(): milky.IncomingMentionAllSegment_ZodInput;
|
|
144
|
+
function face(faceId: number | string, options?: {
|
|
145
|
+
isLarge?: boolean;
|
|
146
|
+
}): milky.IncomingFaceSegment_ZodInput;
|
|
147
|
+
function reply(source: milky.IncomingMessage | IncomingReplySource): milky.IncomingReplySegment_ZodInput;
|
|
148
|
+
function image(options?: {
|
|
149
|
+
resourceId?: string;
|
|
150
|
+
tempUrl?: string;
|
|
151
|
+
width?: number;
|
|
152
|
+
height?: number;
|
|
153
|
+
summary?: string;
|
|
154
|
+
subType?: 'normal' | 'sticker';
|
|
155
|
+
}): milky.IncomingImageSegment_ZodInput;
|
|
156
|
+
function record(options?: {
|
|
157
|
+
resourceId?: string;
|
|
158
|
+
tempUrl?: string;
|
|
159
|
+
duration?: number;
|
|
160
|
+
}): milky.IncomingRecordSegment_ZodInput;
|
|
161
|
+
function video(options?: {
|
|
162
|
+
resourceId?: string;
|
|
163
|
+
tempUrl?: string;
|
|
164
|
+
width?: number;
|
|
165
|
+
height?: number;
|
|
166
|
+
duration?: number;
|
|
167
|
+
}): milky.IncomingVideoSegment_ZodInput;
|
|
168
|
+
function file(options?: {
|
|
169
|
+
fileId?: string;
|
|
170
|
+
fileName?: string;
|
|
171
|
+
fileSize?: number;
|
|
172
|
+
fileHash?: string | null;
|
|
173
|
+
}): milky.IncomingFileSegment_ZodInput;
|
|
174
|
+
function forward(options?: {
|
|
175
|
+
forwardId?: string;
|
|
176
|
+
title?: string;
|
|
177
|
+
preview?: string[];
|
|
178
|
+
summary?: string;
|
|
179
|
+
}): milky.IncomingForwardSegment_ZodInput;
|
|
180
|
+
function marketFace(options?: {
|
|
181
|
+
emojiPackageId?: number;
|
|
182
|
+
emojiId?: string;
|
|
183
|
+
key?: string;
|
|
184
|
+
summary?: string;
|
|
185
|
+
url?: string;
|
|
186
|
+
}): milky.IncomingMarketFaceSegment_ZodInput;
|
|
187
|
+
function lightApp(appName: string, jsonPayload: string): milky.IncomingLightAppSegment_ZodInput;
|
|
188
|
+
function xml(serviceId: number, xmlPayload: string): milky.IncomingXmlSegment_ZodInput;
|
|
189
|
+
function markdown(content: string): milky.IncomingMarkdownSegment_ZodInput;
|
|
190
|
+
}
|
|
191
|
+
//#endregion
|
|
192
|
+
//#region src/index.d.ts
|
|
193
|
+
interface MockPluginOptions extends MockServiceOptions {
|
|
194
|
+
service?: MockService;
|
|
195
|
+
}
|
|
196
|
+
declare const MockPlugin: import("@fraqjs/fraq").Plugin<[options?: MockPluginOptions | undefined], import("@fraqjs/fraq").Injection | undefined, import("@fraqjs/fraq").Injection | undefined>;
|
|
197
|
+
type MockContext = Context & {
|
|
198
|
+
readonly mock: MockService;
|
|
199
|
+
};
|
|
200
|
+
interface MockContextOptions extends MockServiceOptions, ContextOptions {}
|
|
201
|
+
declare function createMockContext(options?: MockContextOptions): MockContext;
|
|
202
|
+
//#endregion
|
|
203
|
+
export { IncomingReplySource, InmsgTemplateValue, MockApiCall, MockContext, MockContextOptions, MockFriendMessageOptions, MockGroupMessageOptions, MockInbox, MockInboxOptions, MockMessageContext, MockMessageEventOverrides, MockMessageMetaOverrides, MockMessageScene, MockPlugin, MockPlugin as default, MockPluginOptions, MockService, MockServiceOptions, MockTempMessageOptions, RandomFriendOverrides, RandomGroupMemberOverrides, RandomGroupOverrides, createMockContext, createMockInbox, createRandomFriend, createRandomGroup, createRandomGroupMember, createSimpleLogHandler, inmsg, inseg };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1070 @@
|
|
|
1
|
+
import { Context, definePlugin } from "@fraqjs/fraq";
|
|
2
|
+
//#region src/logging.ts
|
|
3
|
+
function createSimpleLogHandler() {
|
|
4
|
+
return ({ time, level, module, message, error }) => {
|
|
5
|
+
const timeStr = new Date(time).toISOString();
|
|
6
|
+
const levelStr = level.toUpperCase();
|
|
7
|
+
console.log(`[${timeStr} ${levelStr}] [${module}] ${message}`);
|
|
8
|
+
if (error) console.error(error);
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/random.ts
|
|
13
|
+
const PCG32_MULTIPLIER = 6364136223846793005n;
|
|
14
|
+
const UINT64_MASK = (1n << 64n) - 1n;
|
|
15
|
+
const UINT32_MODULUS = 2 ** 32;
|
|
16
|
+
function rotateRight32(value, rotation) {
|
|
17
|
+
const amount = rotation & 31;
|
|
18
|
+
if (amount === 0) return value >>> 0;
|
|
19
|
+
return (value >>> amount | value << 32 - amount) >>> 0;
|
|
20
|
+
}
|
|
21
|
+
var PCG32 = class {
|
|
22
|
+
state;
|
|
23
|
+
increment;
|
|
24
|
+
constructor(seed, sequence = 1n) {
|
|
25
|
+
this.state = 0n;
|
|
26
|
+
this.increment = (sequence << 1n | 1n) & UINT64_MASK;
|
|
27
|
+
this.nextUint32();
|
|
28
|
+
this.state = this.state + seed & UINT64_MASK;
|
|
29
|
+
this.nextUint32();
|
|
30
|
+
}
|
|
31
|
+
nextUint32() {
|
|
32
|
+
const currentState = this.state;
|
|
33
|
+
this.state = currentState * PCG32_MULTIPLIER + this.increment & UINT64_MASK;
|
|
34
|
+
return rotateRight32(Number((currentState >> 18n ^ currentState) >> 27n & 4294967295n), Number(currentState >> 59n & 31n));
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
function assertSafeInteger$1(value, name) {
|
|
38
|
+
if (!Number.isSafeInteger(value)) throw new RangeError(`${name} must be a safe integer.`);
|
|
39
|
+
}
|
|
40
|
+
var SeededRandom = class {
|
|
41
|
+
generator;
|
|
42
|
+
constructor(seed) {
|
|
43
|
+
assertSafeInteger$1(seed, "seed");
|
|
44
|
+
this.generator = new PCG32(BigInt(seed >>> 0));
|
|
45
|
+
}
|
|
46
|
+
uint32() {
|
|
47
|
+
return this.generator.nextUint32();
|
|
48
|
+
}
|
|
49
|
+
float() {
|
|
50
|
+
return this.uint32() / UINT32_MODULUS;
|
|
51
|
+
}
|
|
52
|
+
bool(probability = .5) {
|
|
53
|
+
return this.float() < probability;
|
|
54
|
+
}
|
|
55
|
+
int(minOrMax, maxExclusive) {
|
|
56
|
+
const minInclusive = maxExclusive === void 0 ? 0 : minOrMax;
|
|
57
|
+
const actualMaxExclusive = maxExclusive ?? minOrMax;
|
|
58
|
+
assertSafeInteger$1(minInclusive, "minInclusive");
|
|
59
|
+
assertSafeInteger$1(actualMaxExclusive, "maxExclusive");
|
|
60
|
+
if (actualMaxExclusive <= minInclusive) throw new RangeError("maxExclusive must be greater than minInclusive.");
|
|
61
|
+
const span = actualMaxExclusive - minInclusive;
|
|
62
|
+
const limit = Math.floor(UINT32_MODULUS / span) * span;
|
|
63
|
+
let value = this.uint32();
|
|
64
|
+
while (value >= limit) value = this.uint32();
|
|
65
|
+
return minInclusive + value % span;
|
|
66
|
+
}
|
|
67
|
+
range(minInclusive, maxInclusive) {
|
|
68
|
+
assertSafeInteger$1(minInclusive, "minInclusive");
|
|
69
|
+
assertSafeInteger$1(maxInclusive, "maxInclusive");
|
|
70
|
+
if (maxInclusive < minInclusive) throw new RangeError("maxInclusive must be greater than or equal to minInclusive.");
|
|
71
|
+
return minInclusive + this.int(maxInclusive - minInclusive + 1);
|
|
72
|
+
}
|
|
73
|
+
pick(items) {
|
|
74
|
+
if (items.length === 0) throw new RangeError("items must not be empty.");
|
|
75
|
+
return items[this.int(items.length)];
|
|
76
|
+
}
|
|
77
|
+
weightedPick(items, weightOf) {
|
|
78
|
+
if (items.length === 0) throw new RangeError("items must not be empty.");
|
|
79
|
+
let totalWeight = 0;
|
|
80
|
+
for (const item of items) totalWeight += weightOf(item);
|
|
81
|
+
const target = this.float() * totalWeight;
|
|
82
|
+
let cumulative = 0;
|
|
83
|
+
for (const item of items) {
|
|
84
|
+
cumulative += weightOf(item);
|
|
85
|
+
if (target < cumulative) return item;
|
|
86
|
+
}
|
|
87
|
+
return items[items.length - 1];
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/vocabulary.json
|
|
92
|
+
var friend = {
|
|
93
|
+
"firstNames": [
|
|
94
|
+
"Alice",
|
|
95
|
+
"Bob",
|
|
96
|
+
"Carol",
|
|
97
|
+
"Dave",
|
|
98
|
+
"Eve",
|
|
99
|
+
"Frank",
|
|
100
|
+
"Grace",
|
|
101
|
+
"Heidi",
|
|
102
|
+
"Ivan",
|
|
103
|
+
"Judy",
|
|
104
|
+
"Mallory",
|
|
105
|
+
"Niaj",
|
|
106
|
+
"Olivia",
|
|
107
|
+
"Peggy",
|
|
108
|
+
"Rupert",
|
|
109
|
+
"Sybil",
|
|
110
|
+
"Trent",
|
|
111
|
+
"Victor",
|
|
112
|
+
"Walter",
|
|
113
|
+
"Zoe"
|
|
114
|
+
],
|
|
115
|
+
"lastNames": [
|
|
116
|
+
"Ash",
|
|
117
|
+
"Brook",
|
|
118
|
+
"Cedar",
|
|
119
|
+
"Dawn",
|
|
120
|
+
"Ember",
|
|
121
|
+
"Field",
|
|
122
|
+
"Grove",
|
|
123
|
+
"Harbor",
|
|
124
|
+
"Ivory",
|
|
125
|
+
"Jasper",
|
|
126
|
+
"Kite",
|
|
127
|
+
"Lake",
|
|
128
|
+
"Meadow",
|
|
129
|
+
"North",
|
|
130
|
+
"Oak",
|
|
131
|
+
"Pine",
|
|
132
|
+
"Quill",
|
|
133
|
+
"River",
|
|
134
|
+
"Stone",
|
|
135
|
+
"Vale"
|
|
136
|
+
],
|
|
137
|
+
"nicknameSuffixes": [
|
|
138
|
+
"",
|
|
139
|
+
"",
|
|
140
|
+
"",
|
|
141
|
+
"Nova",
|
|
142
|
+
"Pixel",
|
|
143
|
+
"Echo",
|
|
144
|
+
"Skylark",
|
|
145
|
+
"Comet",
|
|
146
|
+
"Drift",
|
|
147
|
+
"Aurora"
|
|
148
|
+
],
|
|
149
|
+
"remarks": [
|
|
150
|
+
"",
|
|
151
|
+
"",
|
|
152
|
+
"",
|
|
153
|
+
"Project lead",
|
|
154
|
+
"Design review",
|
|
155
|
+
"Ops contact",
|
|
156
|
+
"Weekend runner",
|
|
157
|
+
"Late-night debugger",
|
|
158
|
+
"Coffee strategist",
|
|
159
|
+
"Always online"
|
|
160
|
+
],
|
|
161
|
+
"categoryNames": [
|
|
162
|
+
"General",
|
|
163
|
+
"Work",
|
|
164
|
+
"Friends",
|
|
165
|
+
"Study",
|
|
166
|
+
"Gaming",
|
|
167
|
+
"Family",
|
|
168
|
+
"Local",
|
|
169
|
+
"Archive"
|
|
170
|
+
]
|
|
171
|
+
};
|
|
172
|
+
var group = {
|
|
173
|
+
"prefixes": [
|
|
174
|
+
"Northwind",
|
|
175
|
+
"Silver",
|
|
176
|
+
"Quiet",
|
|
177
|
+
"Golden",
|
|
178
|
+
"Blue",
|
|
179
|
+
"Rapid",
|
|
180
|
+
"Lucky",
|
|
181
|
+
"Open",
|
|
182
|
+
"Bright",
|
|
183
|
+
"Sunday"
|
|
184
|
+
],
|
|
185
|
+
"topics": [
|
|
186
|
+
"Studio",
|
|
187
|
+
"Workshop",
|
|
188
|
+
"Lab",
|
|
189
|
+
"Guild",
|
|
190
|
+
"Circle",
|
|
191
|
+
"Squad",
|
|
192
|
+
"Forum",
|
|
193
|
+
"Club",
|
|
194
|
+
"Corner",
|
|
195
|
+
"Channel"
|
|
196
|
+
],
|
|
197
|
+
"suffixes": [
|
|
198
|
+
"",
|
|
199
|
+
"",
|
|
200
|
+
"HQ",
|
|
201
|
+
"Room",
|
|
202
|
+
"Collective",
|
|
203
|
+
"Network",
|
|
204
|
+
"Union",
|
|
205
|
+
"Hub"
|
|
206
|
+
],
|
|
207
|
+
"remarks": [
|
|
208
|
+
"",
|
|
209
|
+
"",
|
|
210
|
+
"Pinned for testing",
|
|
211
|
+
"Quiet hours after midnight",
|
|
212
|
+
"Temporary coordination group",
|
|
213
|
+
"Read-only on weekends"
|
|
214
|
+
],
|
|
215
|
+
"descriptions": [
|
|
216
|
+
"A steady group for day-to-day coordination and practical discussion.",
|
|
217
|
+
"A relaxed place for updates, shared notes, and quick questions.",
|
|
218
|
+
"Used for planning, follow-up, and keeping small tasks moving.",
|
|
219
|
+
"A focused channel for experiments, reviews, and status checks.",
|
|
220
|
+
"A casual room where people trade ideas, links, and useful context."
|
|
221
|
+
],
|
|
222
|
+
"questions": [
|
|
223
|
+
"",
|
|
224
|
+
"",
|
|
225
|
+
"What are you hoping to work on here?",
|
|
226
|
+
"Who invited you to join this group?",
|
|
227
|
+
"Share one sentence about your role in this space."
|
|
228
|
+
],
|
|
229
|
+
"announcements": [
|
|
230
|
+
"Welcome aboard. Keep threads tidy and label follow-ups clearly.",
|
|
231
|
+
"Weekly review starts at 20:00. Bring blockers and next steps.",
|
|
232
|
+
"Please avoid duplicate questions; check the pinned notes first.",
|
|
233
|
+
"Files older than 30 days may be archived during cleanup.",
|
|
234
|
+
"Be kind, be direct, and leave enough context for the next person."
|
|
235
|
+
]
|
|
236
|
+
};
|
|
237
|
+
var groupMember = {
|
|
238
|
+
"cardPrefixes": [
|
|
239
|
+
"",
|
|
240
|
+
"",
|
|
241
|
+
"Lead",
|
|
242
|
+
"Deputy",
|
|
243
|
+
"Night",
|
|
244
|
+
"Field",
|
|
245
|
+
"On-call",
|
|
246
|
+
"Guest",
|
|
247
|
+
"Blue",
|
|
248
|
+
"Quiet"
|
|
249
|
+
],
|
|
250
|
+
"cardSuffixes": [
|
|
251
|
+
"",
|
|
252
|
+
"",
|
|
253
|
+
"Ops",
|
|
254
|
+
"Builder",
|
|
255
|
+
"Planner",
|
|
256
|
+
"Scout",
|
|
257
|
+
"Keeper",
|
|
258
|
+
"Pilot",
|
|
259
|
+
"Scribe",
|
|
260
|
+
"Watcher"
|
|
261
|
+
],
|
|
262
|
+
"titles": [
|
|
263
|
+
"",
|
|
264
|
+
"",
|
|
265
|
+
"",
|
|
266
|
+
"Early Bird",
|
|
267
|
+
"Bug Hunter",
|
|
268
|
+
"Note Keeper",
|
|
269
|
+
"Night Shift",
|
|
270
|
+
"Fast Responder",
|
|
271
|
+
"Steady Hand",
|
|
272
|
+
"Build Whisperer"
|
|
273
|
+
],
|
|
274
|
+
"levelLabels": [
|
|
275
|
+
"Sprout",
|
|
276
|
+
"Scout",
|
|
277
|
+
"Maker",
|
|
278
|
+
"Pilot",
|
|
279
|
+
"Anchor",
|
|
280
|
+
"Keeper",
|
|
281
|
+
"Guide",
|
|
282
|
+
"Expert"
|
|
283
|
+
]
|
|
284
|
+
};
|
|
285
|
+
//#endregion
|
|
286
|
+
//#region src/entity.ts
|
|
287
|
+
const ROLE_WEIGHTS = [
|
|
288
|
+
{
|
|
289
|
+
role: "owner",
|
|
290
|
+
weight: 1
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
role: "admin",
|
|
294
|
+
weight: 6
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
role: "member",
|
|
298
|
+
weight: 93
|
|
299
|
+
}
|
|
300
|
+
];
|
|
301
|
+
const SEX_WEIGHTS = [
|
|
302
|
+
{
|
|
303
|
+
sex: "male",
|
|
304
|
+
weight: 45
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
sex: "female",
|
|
308
|
+
weight: 45
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
sex: "unknown",
|
|
312
|
+
weight: 10
|
|
313
|
+
}
|
|
314
|
+
];
|
|
315
|
+
const GROUP_CAPACITY_TIERS = [
|
|
316
|
+
50,
|
|
317
|
+
100,
|
|
318
|
+
200,
|
|
319
|
+
500,
|
|
320
|
+
1e3,
|
|
321
|
+
2e3
|
|
322
|
+
];
|
|
323
|
+
const REFERENCE_TIME = Date.UTC(2025, 0, 1, 0, 0, 0) / 1e3;
|
|
324
|
+
const GROUP_CREATED_TIME_START = Date.UTC(2018, 0, 1, 0, 0, 0) / 1e3;
|
|
325
|
+
const GROUP_CREATED_TIME_END = Date.UTC(2024, 11, 31, 23, 59, 59) / 1e3;
|
|
326
|
+
function assertSeedInput(value, name) {
|
|
327
|
+
if (!Number.isSafeInteger(value)) throw new RangeError(`${name} must be a safe integer.`);
|
|
328
|
+
return value;
|
|
329
|
+
}
|
|
330
|
+
function mixSeed(namespace, ...values) {
|
|
331
|
+
let hash = 2166136261;
|
|
332
|
+
for (const char of namespace) {
|
|
333
|
+
hash ^= char.charCodeAt(0);
|
|
334
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
335
|
+
}
|
|
336
|
+
for (const value of values) {
|
|
337
|
+
let current = assertSeedInput(value, "seed input") >>> 0;
|
|
338
|
+
for (let index = 0; index < 4; index += 1) {
|
|
339
|
+
hash ^= current & 255;
|
|
340
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
341
|
+
current >>>= 8;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return hash;
|
|
345
|
+
}
|
|
346
|
+
function createRandom(namespace, ...values) {
|
|
347
|
+
return new SeededRandom(mixSeed(namespace, ...values));
|
|
348
|
+
}
|
|
349
|
+
function joinParts(...parts) {
|
|
350
|
+
return parts.filter(Boolean).join(" ");
|
|
351
|
+
}
|
|
352
|
+
function createUserProfile(userId) {
|
|
353
|
+
const random = createRandom("friend-profile", userId);
|
|
354
|
+
const firstName = random.pick(friend.firstNames);
|
|
355
|
+
const lastName = random.pick(friend.lastNames);
|
|
356
|
+
const nicknameSuffix = random.pick(friend.nicknameSuffixes);
|
|
357
|
+
const sex = random.weightedPick(SEX_WEIGHTS, (item) => item.weight).sex;
|
|
358
|
+
return {
|
|
359
|
+
user_id: userId,
|
|
360
|
+
nickname: joinParts(firstName, lastName, nicknameSuffix),
|
|
361
|
+
sex,
|
|
362
|
+
qid: `qid_${Math.abs(userId).toString(36).padStart(6, "0")}`
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
function createFriendCategory(userId, overrides) {
|
|
366
|
+
const categoryId = createRandom("friend-category", userId).int(friend.categoryNames.length);
|
|
367
|
+
return {
|
|
368
|
+
category_id: categoryId + 1,
|
|
369
|
+
category_name: friend.categoryNames[categoryId],
|
|
370
|
+
...overrides
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
function createGroupBase(groupId) {
|
|
374
|
+
const random = createRandom("group", groupId);
|
|
375
|
+
const memberCount = random.range(12, 320);
|
|
376
|
+
const minCapacity = memberCount + random.range(0, 120);
|
|
377
|
+
const maxMemberCount = GROUP_CAPACITY_TIERS.find((capacity) => capacity >= minCapacity) ?? GROUP_CAPACITY_TIERS[GROUP_CAPACITY_TIERS.length - 1];
|
|
378
|
+
const createdTime = random.range(GROUP_CREATED_TIME_START, GROUP_CREATED_TIME_END);
|
|
379
|
+
return {
|
|
380
|
+
group_id: groupId,
|
|
381
|
+
group_name: joinParts(random.pick(group.prefixes), random.pick(group.topics), random.pick(group.suffixes)),
|
|
382
|
+
member_count: memberCount,
|
|
383
|
+
max_member_count: maxMemberCount,
|
|
384
|
+
remark: random.pick(group.remarks),
|
|
385
|
+
created_time: createdTime,
|
|
386
|
+
description: random.pick(group.descriptions),
|
|
387
|
+
question: random.pick(group.questions),
|
|
388
|
+
announcement: random.pick(group.announcements)
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
function createRandomFriend(userId, overrides = {}) {
|
|
392
|
+
assertSeedInput(userId, "userId");
|
|
393
|
+
const random = createRandom("friend", userId);
|
|
394
|
+
const friend$1 = {
|
|
395
|
+
...createUserProfile(userId),
|
|
396
|
+
remark: random.pick(friend.remarks),
|
|
397
|
+
category: createFriendCategory(userId, overrides.category)
|
|
398
|
+
};
|
|
399
|
+
return {
|
|
400
|
+
...friend$1,
|
|
401
|
+
...overrides,
|
|
402
|
+
category: {
|
|
403
|
+
...friend$1.category,
|
|
404
|
+
...overrides.category
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function createRandomGroup(groupId, overrides = {}) {
|
|
409
|
+
assertSeedInput(groupId, "groupId");
|
|
410
|
+
return {
|
|
411
|
+
...createGroupBase(groupId),
|
|
412
|
+
...overrides
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
function createRandomGroupMember(groupId, userId, overrides = {}) {
|
|
416
|
+
assertSeedInput(groupId, "groupId");
|
|
417
|
+
assertSeedInput(userId, "userId");
|
|
418
|
+
const random = createRandom("group-member", groupId, userId);
|
|
419
|
+
const profile = createUserProfile(userId);
|
|
420
|
+
const group = createGroupBase(groupId);
|
|
421
|
+
const role = random.weightedPick(ROLE_WEIGHTS, (item) => item.weight).role;
|
|
422
|
+
const joinTimeEnd = Math.max(group.created_time, REFERENCE_TIME - 86400);
|
|
423
|
+
const joinTime = random.range(group.created_time, joinTimeEnd);
|
|
424
|
+
const lastSentTime = random.range(joinTime, REFERENCE_TIME);
|
|
425
|
+
const shutUpEndTime = random.bool(.1) ? lastSentTime + random.range(300, 10080 * 60) : null;
|
|
426
|
+
const cardPrefix = random.pick(groupMember.cardPrefixes);
|
|
427
|
+
const cardSuffix = random.pick(groupMember.cardSuffixes);
|
|
428
|
+
const card = joinParts(cardPrefix, profile.nickname, cardSuffix);
|
|
429
|
+
const title = random.pick(groupMember.titles);
|
|
430
|
+
return {
|
|
431
|
+
user_id: userId,
|
|
432
|
+
nickname: profile.nickname,
|
|
433
|
+
sex: profile.sex,
|
|
434
|
+
group_id: groupId,
|
|
435
|
+
card,
|
|
436
|
+
title,
|
|
437
|
+
level: random.range(1, 100),
|
|
438
|
+
role,
|
|
439
|
+
join_time: joinTime,
|
|
440
|
+
last_sent_time: lastSentTime,
|
|
441
|
+
shut_up_end_time: shutUpEndTime,
|
|
442
|
+
...overrides
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
//#endregion
|
|
446
|
+
//#region src/inbox.ts
|
|
447
|
+
const DEFAULT_BASE_TIME = Date.UTC(2025, 0, 1, 0, 0, 0) / 1e3;
|
|
448
|
+
function assertSafeInteger(value, name) {
|
|
449
|
+
if (!Number.isSafeInteger(value)) throw new RangeError(`${name} must be a safe integer.`);
|
|
450
|
+
}
|
|
451
|
+
function assertPositiveInteger(value, name) {
|
|
452
|
+
assertSafeInteger(value, name);
|
|
453
|
+
if (value <= 0) throw new RangeError(`${name} must be a positive integer.`);
|
|
454
|
+
}
|
|
455
|
+
function cloneSegments(segments) {
|
|
456
|
+
return segments.map((segment) => {
|
|
457
|
+
switch (segment.type) {
|
|
458
|
+
case "reply": return {
|
|
459
|
+
...segment,
|
|
460
|
+
data: {
|
|
461
|
+
...segment.data,
|
|
462
|
+
segments: cloneSegments(segment.data.segments)
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
case "forward":
|
|
466
|
+
case "image":
|
|
467
|
+
case "record":
|
|
468
|
+
case "video":
|
|
469
|
+
case "file":
|
|
470
|
+
case "market_face":
|
|
471
|
+
case "light_app":
|
|
472
|
+
case "xml":
|
|
473
|
+
case "markdown":
|
|
474
|
+
case "mention":
|
|
475
|
+
case "mention_all":
|
|
476
|
+
case "face":
|
|
477
|
+
case "text": return {
|
|
478
|
+
...segment,
|
|
479
|
+
data: { ...segment.data }
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
function groupMemberKey(groupId, userId) {
|
|
485
|
+
return `${groupId}:${userId}`;
|
|
486
|
+
}
|
|
487
|
+
function defaultConversationKey(context) {
|
|
488
|
+
switch (context.scene) {
|
|
489
|
+
case "friend": return `friend:${context.peerId}`;
|
|
490
|
+
case "group": return `group:${context.groupId ?? context.peerId}`;
|
|
491
|
+
case "temp": return `temp:${context.groupId ?? context.peerId}:${context.senderId}`;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
var MockInbox = class {
|
|
495
|
+
selfId;
|
|
496
|
+
baseTime;
|
|
497
|
+
sequenceStart;
|
|
498
|
+
timeStepSeconds;
|
|
499
|
+
conversationKeyOf;
|
|
500
|
+
conversations = /* @__PURE__ */ new Map();
|
|
501
|
+
allMessages = [];
|
|
502
|
+
friends = /* @__PURE__ */ new Map();
|
|
503
|
+
groups = /* @__PURE__ */ new Map();
|
|
504
|
+
groupMembers = /* @__PURE__ */ new Map();
|
|
505
|
+
constructor(options = {}) {
|
|
506
|
+
this.selfId = options.selfId ?? 1e4;
|
|
507
|
+
this.baseTime = options.baseTime ?? DEFAULT_BASE_TIME;
|
|
508
|
+
this.sequenceStart = options.sequenceStart ?? 1;
|
|
509
|
+
this.timeStepSeconds = options.timeStepSeconds ?? 1;
|
|
510
|
+
this.conversationKeyOf = options.conversationKey ?? defaultConversationKey;
|
|
511
|
+
assertPositiveInteger(this.selfId, "selfId");
|
|
512
|
+
assertSafeInteger(this.baseTime, "baseTime");
|
|
513
|
+
assertPositiveInteger(this.sequenceStart, "sequenceStart");
|
|
514
|
+
assertPositiveInteger(this.timeStepSeconds, "timeStepSeconds");
|
|
515
|
+
}
|
|
516
|
+
conversationKey(context) {
|
|
517
|
+
if (context.groupId !== void 0) assertPositiveInteger(context.groupId, "groupId");
|
|
518
|
+
assertPositiveInteger(context.peerId, "peerId");
|
|
519
|
+
assertPositiveInteger(context.senderId, "senderId");
|
|
520
|
+
return this.conversationKeyOf(context);
|
|
521
|
+
}
|
|
522
|
+
friendConversationKey(peerId, senderId = peerId) {
|
|
523
|
+
assertPositiveInteger(peerId, "peerId");
|
|
524
|
+
assertPositiveInteger(senderId, "senderId");
|
|
525
|
+
return this.conversationKey({
|
|
526
|
+
scene: "friend",
|
|
527
|
+
peerId,
|
|
528
|
+
senderId
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
groupConversationKey(groupId, senderId = groupId) {
|
|
532
|
+
assertPositiveInteger(groupId, "groupId");
|
|
533
|
+
assertPositiveInteger(senderId, "senderId");
|
|
534
|
+
return this.conversationKey({
|
|
535
|
+
scene: "group",
|
|
536
|
+
peerId: groupId,
|
|
537
|
+
senderId,
|
|
538
|
+
groupId
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
tempConversationKey(options) {
|
|
542
|
+
const peerId = options.peerId ?? options.senderId;
|
|
543
|
+
assertPositiveInteger(peerId, "peerId");
|
|
544
|
+
assertPositiveInteger(options.senderId, "senderId");
|
|
545
|
+
if (options.groupId !== void 0) assertPositiveInteger(options.groupId, "groupId");
|
|
546
|
+
return this.conversationKey({
|
|
547
|
+
scene: "temp",
|
|
548
|
+
peerId,
|
|
549
|
+
senderId: options.senderId,
|
|
550
|
+
groupId: options.groupId
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
friend(options, segments) {
|
|
554
|
+
assertPositiveInteger(options.userId, "userId");
|
|
555
|
+
const peerId = options.peerId ?? options.userId;
|
|
556
|
+
const senderId = options.senderId ?? options.userId;
|
|
557
|
+
assertPositiveInteger(peerId, "peerId");
|
|
558
|
+
assertPositiveInteger(senderId, "senderId");
|
|
559
|
+
const conversationKey = this.conversationKey({
|
|
560
|
+
scene: "friend",
|
|
561
|
+
peerId,
|
|
562
|
+
senderId
|
|
563
|
+
});
|
|
564
|
+
const { messageSeq, time } = this.allocateMessageMeta(conversationKey, options);
|
|
565
|
+
const message = {
|
|
566
|
+
message_scene: "friend",
|
|
567
|
+
peer_id: peerId,
|
|
568
|
+
message_seq: messageSeq,
|
|
569
|
+
sender_id: senderId,
|
|
570
|
+
time,
|
|
571
|
+
segments: cloneSegments(segments),
|
|
572
|
+
friend: createRandomFriend(options.userId, options.friend)
|
|
573
|
+
};
|
|
574
|
+
this.storeMessage(conversationKey, message);
|
|
575
|
+
return message;
|
|
576
|
+
}
|
|
577
|
+
group(options, segments) {
|
|
578
|
+
assertPositiveInteger(options.groupId, "groupId");
|
|
579
|
+
assertPositiveInteger(options.userId, "userId");
|
|
580
|
+
const senderId = options.senderId ?? options.userId;
|
|
581
|
+
assertPositiveInteger(senderId, "senderId");
|
|
582
|
+
const conversationKey = this.conversationKey({
|
|
583
|
+
scene: "group",
|
|
584
|
+
peerId: options.groupId,
|
|
585
|
+
senderId,
|
|
586
|
+
groupId: options.groupId
|
|
587
|
+
});
|
|
588
|
+
const { messageSeq, time } = this.allocateMessageMeta(conversationKey, options);
|
|
589
|
+
const message = {
|
|
590
|
+
message_scene: "group",
|
|
591
|
+
peer_id: options.groupId,
|
|
592
|
+
message_seq: messageSeq,
|
|
593
|
+
sender_id: senderId,
|
|
594
|
+
time,
|
|
595
|
+
segments: cloneSegments(segments),
|
|
596
|
+
group: createRandomGroup(options.groupId, options.group),
|
|
597
|
+
group_member: createRandomGroupMember(options.groupId, options.userId, options.groupMember)
|
|
598
|
+
};
|
|
599
|
+
this.storeMessage(conversationKey, message);
|
|
600
|
+
return message;
|
|
601
|
+
}
|
|
602
|
+
temp(options, segments) {
|
|
603
|
+
assertPositiveInteger(options.userId, "userId");
|
|
604
|
+
const peerId = options.peerId ?? options.userId;
|
|
605
|
+
const senderId = options.senderId ?? options.userId;
|
|
606
|
+
assertPositiveInteger(peerId, "peerId");
|
|
607
|
+
assertPositiveInteger(senderId, "senderId");
|
|
608
|
+
if (options.groupId !== void 0) assertPositiveInteger(options.groupId, "groupId");
|
|
609
|
+
const conversationKey = this.conversationKey({
|
|
610
|
+
scene: "temp",
|
|
611
|
+
peerId,
|
|
612
|
+
senderId,
|
|
613
|
+
groupId: options.groupId
|
|
614
|
+
});
|
|
615
|
+
const { messageSeq, time } = this.allocateMessageMeta(conversationKey, options);
|
|
616
|
+
const group = options.group === null ? null : options.groupId === void 0 ? void 0 : createRandomGroup(options.groupId, options.group);
|
|
617
|
+
const message = {
|
|
618
|
+
message_scene: "temp",
|
|
619
|
+
peer_id: peerId,
|
|
620
|
+
message_seq: messageSeq,
|
|
621
|
+
sender_id: senderId,
|
|
622
|
+
time,
|
|
623
|
+
segments: cloneSegments(segments),
|
|
624
|
+
group
|
|
625
|
+
};
|
|
626
|
+
this.storeMessage(conversationKey, message);
|
|
627
|
+
return message;
|
|
628
|
+
}
|
|
629
|
+
event(message, overrides) {
|
|
630
|
+
const eventTime = overrides?.time ?? message.time;
|
|
631
|
+
const selfId = overrides?.selfId ?? this.selfId;
|
|
632
|
+
assertSafeInteger(eventTime, "event time");
|
|
633
|
+
assertPositiveInteger(selfId, "event selfId");
|
|
634
|
+
return {
|
|
635
|
+
event_type: "message_receive",
|
|
636
|
+
time: eventTime,
|
|
637
|
+
self_id: selfId,
|
|
638
|
+
data: message
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
friendEvent(options, segments, overrides) {
|
|
642
|
+
return this.event(this.friend(options, segments), overrides);
|
|
643
|
+
}
|
|
644
|
+
groupEvent(options, segments, overrides) {
|
|
645
|
+
return this.event(this.group(options, segments), overrides);
|
|
646
|
+
}
|
|
647
|
+
tempEvent(options, segments, overrides) {
|
|
648
|
+
return this.event(this.temp(options, segments), overrides);
|
|
649
|
+
}
|
|
650
|
+
message(conversationKey, messageSeq) {
|
|
651
|
+
return this.conversations.get(conversationKey)?.bySeq.get(messageSeq);
|
|
652
|
+
}
|
|
653
|
+
getMessage(input) {
|
|
654
|
+
const conversationKey = this.queryConversationKey(input.message_scene, input.peer_id);
|
|
655
|
+
const message = this.message(conversationKey, input.message_seq);
|
|
656
|
+
if (!message) throw new Error(`Message not found (scene=${input.message_scene} peer=${input.peer_id} seq=${input.message_seq})`);
|
|
657
|
+
return { message };
|
|
658
|
+
}
|
|
659
|
+
getHistoryMessages(input) {
|
|
660
|
+
const conversationKey = this.queryConversationKey(input.message_scene, input.peer_id);
|
|
661
|
+
const limit = input.limit ?? 30;
|
|
662
|
+
const startMessageSeq = input.start_message_seq;
|
|
663
|
+
assertPositiveInteger(limit, "limit");
|
|
664
|
+
if (limit > 30) throw new RangeError("limit must be less than or equal to 30.");
|
|
665
|
+
const messages = this.history(conversationKey);
|
|
666
|
+
const endIndex = startMessageSeq == null ? messages.length : messages.findIndex((message) => message.message_seq > startMessageSeq);
|
|
667
|
+
const boundedMessages = endIndex === -1 ? messages : messages.slice(0, endIndex);
|
|
668
|
+
const startIndex = Math.max(0, boundedMessages.length - limit);
|
|
669
|
+
return {
|
|
670
|
+
messages: boundedMessages.slice(startIndex),
|
|
671
|
+
next_message_seq: boundedMessages[startIndex - 1]?.message_seq
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
markMessageAsRead(input) {
|
|
675
|
+
if (input.message_scene === "temp") return {};
|
|
676
|
+
const conversationKey = this.queryConversationKey(input.message_scene, input.peer_id);
|
|
677
|
+
const state = this.getOrCreateConversationState(conversationKey);
|
|
678
|
+
state.lastReadMessageSeq = input.message_seq;
|
|
679
|
+
return {};
|
|
680
|
+
}
|
|
681
|
+
getFriendInfo(input) {
|
|
682
|
+
assertPositiveInteger(input.user_id, "user_id");
|
|
683
|
+
return { friend: this.friends.get(input.user_id) ?? createRandomFriend(input.user_id) };
|
|
684
|
+
}
|
|
685
|
+
getGroupInfo(input) {
|
|
686
|
+
assertPositiveInteger(input.group_id, "group_id");
|
|
687
|
+
return { group: this.groups.get(input.group_id) ?? createRandomGroup(input.group_id) };
|
|
688
|
+
}
|
|
689
|
+
getGroupMemberInfo(input) {
|
|
690
|
+
assertPositiveInteger(input.group_id, "group_id");
|
|
691
|
+
assertPositiveInteger(input.user_id, "user_id");
|
|
692
|
+
const key = groupMemberKey(input.group_id, input.user_id);
|
|
693
|
+
return { member: this.groupMembers.get(key) ?? createRandomGroupMember(input.group_id, input.user_id) };
|
|
694
|
+
}
|
|
695
|
+
history(conversationKey) {
|
|
696
|
+
if (conversationKey === void 0) return [...this.allMessages];
|
|
697
|
+
return [...this.conversations.get(conversationKey)?.messages ?? []];
|
|
698
|
+
}
|
|
699
|
+
lastMessage(conversationKey) {
|
|
700
|
+
return this.history(conversationKey).at(-1);
|
|
701
|
+
}
|
|
702
|
+
reset() {
|
|
703
|
+
this.conversations.clear();
|
|
704
|
+
this.allMessages.length = 0;
|
|
705
|
+
this.friends.clear();
|
|
706
|
+
this.groups.clear();
|
|
707
|
+
this.groupMembers.clear();
|
|
708
|
+
}
|
|
709
|
+
allocateMessageMeta(conversationKey, overrides) {
|
|
710
|
+
const state = this.getOrCreateConversationState(conversationKey);
|
|
711
|
+
const messageSeq = overrides.messageSeq ?? state.nextSeq;
|
|
712
|
+
const time = overrides.time ?? state.nextTime;
|
|
713
|
+
assertPositiveInteger(messageSeq, "messageSeq");
|
|
714
|
+
assertSafeInteger(time, "time");
|
|
715
|
+
state.nextSeq = Math.max(state.nextSeq, messageSeq + 1);
|
|
716
|
+
state.nextTime = Math.max(state.nextTime, time + this.timeStepSeconds);
|
|
717
|
+
return {
|
|
718
|
+
messageSeq,
|
|
719
|
+
time
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
getOrCreateConversationState(conversationKey) {
|
|
723
|
+
let state = this.conversations.get(conversationKey);
|
|
724
|
+
if (!state) {
|
|
725
|
+
state = {
|
|
726
|
+
nextSeq: this.sequenceStart,
|
|
727
|
+
nextTime: this.baseTime,
|
|
728
|
+
messages: [],
|
|
729
|
+
bySeq: /* @__PURE__ */ new Map(),
|
|
730
|
+
lastReadMessageSeq: void 0
|
|
731
|
+
};
|
|
732
|
+
this.conversations.set(conversationKey, state);
|
|
733
|
+
}
|
|
734
|
+
return state;
|
|
735
|
+
}
|
|
736
|
+
storeMessage(conversationKey, message) {
|
|
737
|
+
const state = this.getOrCreateConversationState(conversationKey);
|
|
738
|
+
state.messages.push(message);
|
|
739
|
+
state.bySeq.set(message.message_seq, message);
|
|
740
|
+
this.allMessages.push(message);
|
|
741
|
+
switch (message.message_scene) {
|
|
742
|
+
case "friend":
|
|
743
|
+
this.friends.set(message.friend.user_id, message.friend);
|
|
744
|
+
break;
|
|
745
|
+
case "group":
|
|
746
|
+
this.groups.set(message.group.group_id, message.group);
|
|
747
|
+
this.groupMembers.set(groupMemberKey(message.group_member.group_id, message.group_member.user_id), message.group_member);
|
|
748
|
+
break;
|
|
749
|
+
case "temp":
|
|
750
|
+
if (message.group) this.groups.set(message.group.group_id, message.group);
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
queryConversationKey(scene, peerId) {
|
|
755
|
+
assertPositiveInteger(peerId, "peer_id");
|
|
756
|
+
switch (scene) {
|
|
757
|
+
case "friend": return this.friendConversationKey(peerId);
|
|
758
|
+
case "group": return this.groupConversationKey(peerId);
|
|
759
|
+
case "temp": throw new Error("Temporary message queries are not supported.");
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
function createMockInbox(options) {
|
|
764
|
+
return new MockInbox(options);
|
|
765
|
+
}
|
|
766
|
+
//#endregion
|
|
767
|
+
//#region src/service.ts
|
|
768
|
+
var MockService = class {
|
|
769
|
+
name = "mock";
|
|
770
|
+
inbox;
|
|
771
|
+
apiCalls = [];
|
|
772
|
+
onEvent;
|
|
773
|
+
closeEventsResolver;
|
|
774
|
+
startEventCalls = 0;
|
|
775
|
+
nextStartError;
|
|
776
|
+
constructor(options = {}) {
|
|
777
|
+
this.inbox = options.inbox ?? new MockInbox(options);
|
|
778
|
+
}
|
|
779
|
+
async start(handler) {
|
|
780
|
+
this.startEventCalls += 1;
|
|
781
|
+
if (this.nextStartError) {
|
|
782
|
+
const error = this.nextStartError;
|
|
783
|
+
this.nextStartError = void 0;
|
|
784
|
+
throw error;
|
|
785
|
+
}
|
|
786
|
+
this.onEvent = handler;
|
|
787
|
+
return {
|
|
788
|
+
closed: new Promise((resolve) => {
|
|
789
|
+
this.closeEventsResolver = resolve;
|
|
790
|
+
}),
|
|
791
|
+
stop: () => {
|
|
792
|
+
this.closeEvents();
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
closeEvents() {
|
|
797
|
+
this.closeEventsResolver?.();
|
|
798
|
+
this.closeEventsResolver = void 0;
|
|
799
|
+
this.onEvent = void 0;
|
|
800
|
+
}
|
|
801
|
+
failNextStart(error) {
|
|
802
|
+
this.nextStartError = error;
|
|
803
|
+
}
|
|
804
|
+
async emitEvent(event) {
|
|
805
|
+
await this.onEvent?.(event);
|
|
806
|
+
}
|
|
807
|
+
async receiveFriend(options, segments) {
|
|
808
|
+
const event = this.inbox.friendEvent(options, segments);
|
|
809
|
+
await this.emitEvent(event);
|
|
810
|
+
return event.data;
|
|
811
|
+
}
|
|
812
|
+
async receiveGroup(options, segments) {
|
|
813
|
+
const event = this.inbox.groupEvent(options, segments);
|
|
814
|
+
await this.emitEvent(event);
|
|
815
|
+
return event.data;
|
|
816
|
+
}
|
|
817
|
+
async receiveTemp(options, segments) {
|
|
818
|
+
const event = this.inbox.tempEvent(options, segments);
|
|
819
|
+
await this.emitEvent(event);
|
|
820
|
+
return event.data;
|
|
821
|
+
}
|
|
822
|
+
handleApiCall(call) {
|
|
823
|
+
this.apiCalls.push({
|
|
824
|
+
endpoint: call.endpoint,
|
|
825
|
+
params: call.params
|
|
826
|
+
});
|
|
827
|
+
const params = call.params;
|
|
828
|
+
switch (call.endpoint) {
|
|
829
|
+
case "get_message": return this.inbox.getMessage(params);
|
|
830
|
+
case "get_history_messages": return this.inbox.getHistoryMessages(params);
|
|
831
|
+
case "mark_message_as_read": return this.inbox.markMessageAsRead(params);
|
|
832
|
+
case "get_friend_info": return this.inbox.getFriendInfo(params);
|
|
833
|
+
case "get_group_info": return this.inbox.getGroupInfo(params);
|
|
834
|
+
case "get_group_member_info": return this.inbox.getGroupMemberInfo(params);
|
|
835
|
+
default: return {};
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
reset() {
|
|
839
|
+
this.apiCalls.length = 0;
|
|
840
|
+
this.inbox.reset();
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
//#endregion
|
|
844
|
+
//#region src/message.ts
|
|
845
|
+
function inmsg(strings, ...values) {
|
|
846
|
+
let buffer = "";
|
|
847
|
+
const segments = [];
|
|
848
|
+
for (let index = 0; index < strings.length; index += 1) {
|
|
849
|
+
buffer += strings[index];
|
|
850
|
+
if (index >= values.length) continue;
|
|
851
|
+
const value = values[index];
|
|
852
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
853
|
+
buffer += value.toString();
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
if (buffer) {
|
|
857
|
+
segments.push(inseg.text(buffer));
|
|
858
|
+
buffer = "";
|
|
859
|
+
}
|
|
860
|
+
segments.push(value);
|
|
861
|
+
}
|
|
862
|
+
if (buffer) segments.push(inseg.text(buffer));
|
|
863
|
+
return trimBoundaryTextSegments(segments);
|
|
864
|
+
}
|
|
865
|
+
function trimBoundaryTextSegments(segments) {
|
|
866
|
+
const result = [...segments];
|
|
867
|
+
const first = result[0];
|
|
868
|
+
if (first?.type === "text") {
|
|
869
|
+
const text = first.data.text.trimStart();
|
|
870
|
+
if (text) result[0] = inseg.text(text);
|
|
871
|
+
else result.shift();
|
|
872
|
+
}
|
|
873
|
+
const lastIndex = result.length - 1;
|
|
874
|
+
const last = result[lastIndex];
|
|
875
|
+
if (last?.type === "text") {
|
|
876
|
+
const text = last.data.text.trimEnd();
|
|
877
|
+
if (text) result[lastIndex] = inseg.text(text);
|
|
878
|
+
else result.pop();
|
|
879
|
+
}
|
|
880
|
+
return result;
|
|
881
|
+
}
|
|
882
|
+
let inseg;
|
|
883
|
+
(function(_inseg) {
|
|
884
|
+
function text(text) {
|
|
885
|
+
return {
|
|
886
|
+
type: "text",
|
|
887
|
+
data: { text }
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
_inseg.text = text;
|
|
891
|
+
function mention(userId, name = `user_${userId}`) {
|
|
892
|
+
return {
|
|
893
|
+
type: "mention",
|
|
894
|
+
data: {
|
|
895
|
+
user_id: userId,
|
|
896
|
+
name
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
_inseg.mention = mention;
|
|
901
|
+
function mentionAll() {
|
|
902
|
+
return {
|
|
903
|
+
type: "mention_all",
|
|
904
|
+
data: {}
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
_inseg.mentionAll = mentionAll;
|
|
908
|
+
function face(faceId, options) {
|
|
909
|
+
return {
|
|
910
|
+
type: "face",
|
|
911
|
+
data: {
|
|
912
|
+
face_id: faceId.toString(),
|
|
913
|
+
is_large: options?.isLarge ?? false
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
_inseg.face = face;
|
|
918
|
+
function reply(source) {
|
|
919
|
+
const data = "message_scene" in source ? {
|
|
920
|
+
message_seq: source.message_seq,
|
|
921
|
+
sender_id: source.sender_id,
|
|
922
|
+
sender_name: void 0,
|
|
923
|
+
time: source.time,
|
|
924
|
+
segments: source.segments
|
|
925
|
+
} : source;
|
|
926
|
+
return {
|
|
927
|
+
type: "reply",
|
|
928
|
+
data: {
|
|
929
|
+
message_seq: data.message_seq,
|
|
930
|
+
sender_id: data.sender_id,
|
|
931
|
+
sender_name: data.sender_name,
|
|
932
|
+
time: data.time,
|
|
933
|
+
segments: [...data.segments]
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
_inseg.reply = reply;
|
|
938
|
+
function image(options) {
|
|
939
|
+
return {
|
|
940
|
+
type: "image",
|
|
941
|
+
data: {
|
|
942
|
+
resource_id: options?.resourceId ?? "mock-image",
|
|
943
|
+
temp_url: options?.tempUrl ?? "https://example.invalid/mock-image",
|
|
944
|
+
width: options?.width ?? 640,
|
|
945
|
+
height: options?.height ?? 480,
|
|
946
|
+
summary: options?.summary ?? "[image]",
|
|
947
|
+
sub_type: options?.subType ?? "normal"
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
_inseg.image = image;
|
|
952
|
+
function record(options) {
|
|
953
|
+
return {
|
|
954
|
+
type: "record",
|
|
955
|
+
data: {
|
|
956
|
+
resource_id: options?.resourceId ?? "mock-record",
|
|
957
|
+
temp_url: options?.tempUrl ?? "https://example.invalid/mock-record",
|
|
958
|
+
duration: options?.duration ?? 3
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
_inseg.record = record;
|
|
963
|
+
function video(options) {
|
|
964
|
+
return {
|
|
965
|
+
type: "video",
|
|
966
|
+
data: {
|
|
967
|
+
resource_id: options?.resourceId ?? "mock-video",
|
|
968
|
+
temp_url: options?.tempUrl ?? "https://example.invalid/mock-video",
|
|
969
|
+
width: options?.width ?? 1280,
|
|
970
|
+
height: options?.height ?? 720,
|
|
971
|
+
duration: options?.duration ?? 12
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
_inseg.video = video;
|
|
976
|
+
function file(options) {
|
|
977
|
+
return {
|
|
978
|
+
type: "file",
|
|
979
|
+
data: {
|
|
980
|
+
file_id: options?.fileId ?? "mock-file",
|
|
981
|
+
file_name: options?.fileName ?? "mock.txt",
|
|
982
|
+
file_size: options?.fileSize ?? 1024,
|
|
983
|
+
file_hash: options?.fileHash
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
_inseg.file = file;
|
|
988
|
+
function forward(options) {
|
|
989
|
+
return {
|
|
990
|
+
type: "forward",
|
|
991
|
+
data: {
|
|
992
|
+
forward_id: options?.forwardId ?? "mock-forward",
|
|
993
|
+
title: options?.title ?? "Chat History",
|
|
994
|
+
preview: options?.preview ?? ["Alice: ping", "Bob: pong"],
|
|
995
|
+
summary: options?.summary ?? "View forwarded messages"
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
_inseg.forward = forward;
|
|
1000
|
+
function marketFace(options) {
|
|
1001
|
+
return {
|
|
1002
|
+
type: "market_face",
|
|
1003
|
+
data: {
|
|
1004
|
+
emoji_package_id: options?.emojiPackageId ?? 1,
|
|
1005
|
+
emoji_id: options?.emojiId ?? "mock-market-face",
|
|
1006
|
+
key: options?.key ?? "mock-key",
|
|
1007
|
+
summary: options?.summary ?? "[market face]",
|
|
1008
|
+
url: options?.url ?? "https://example.invalid/mock-market-face"
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
_inseg.marketFace = marketFace;
|
|
1013
|
+
function lightApp(appName, jsonPayload) {
|
|
1014
|
+
return {
|
|
1015
|
+
type: "light_app",
|
|
1016
|
+
data: {
|
|
1017
|
+
app_name: appName,
|
|
1018
|
+
json_payload: jsonPayload
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
_inseg.lightApp = lightApp;
|
|
1023
|
+
function xml(serviceId, xmlPayload) {
|
|
1024
|
+
return {
|
|
1025
|
+
type: "xml",
|
|
1026
|
+
data: {
|
|
1027
|
+
service_id: serviceId,
|
|
1028
|
+
xml_payload: xmlPayload
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
_inseg.xml = xml;
|
|
1033
|
+
function markdown(content) {
|
|
1034
|
+
return {
|
|
1035
|
+
type: "markdown",
|
|
1036
|
+
data: { content }
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
_inseg.markdown = markdown;
|
|
1040
|
+
})(inseg || (inseg = {}));
|
|
1041
|
+
//#endregion
|
|
1042
|
+
//#region src/index.ts
|
|
1043
|
+
function installMock(ctx, service) {
|
|
1044
|
+
ctx.hookApi((call) => service.handleApiCall(call));
|
|
1045
|
+
ctx.installEventSource(service);
|
|
1046
|
+
ctx.provide(MockService, service);
|
|
1047
|
+
return service;
|
|
1048
|
+
}
|
|
1049
|
+
const MockPlugin = definePlugin({
|
|
1050
|
+
name: "mock",
|
|
1051
|
+
provides: [MockService],
|
|
1052
|
+
apply(ctx, options) {
|
|
1053
|
+
installMock(ctx, options?.service ?? new MockService(options));
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
const unreachableClient = new Proxy({}, { get(_target, prop) {
|
|
1057
|
+
if (typeof prop === "string" && prop.includes("_")) return () => {
|
|
1058
|
+
throw new Error(`[plugin-mock] API "${prop}" fell through to the stub client.`);
|
|
1059
|
+
};
|
|
1060
|
+
} });
|
|
1061
|
+
function createMockContext(options) {
|
|
1062
|
+
const ctx = Context.fromClient(unreachableClient, {
|
|
1063
|
+
...options,
|
|
1064
|
+
logHandler: options?.logHandler ?? createSimpleLogHandler()
|
|
1065
|
+
});
|
|
1066
|
+
const mock = installMock(ctx, new MockService(options));
|
|
1067
|
+
return Object.assign(ctx, { mock });
|
|
1068
|
+
}
|
|
1069
|
+
//#endregion
|
|
1070
|
+
export { MockInbox, MockPlugin, MockPlugin as default, MockService, createMockContext, createMockInbox, createRandomFriend, createRandomGroup, createRandomGroupMember, createSimpleLogHandler, inmsg, inseg };
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fraqjs/plugin-mock",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "提供模拟 Milky 协议端的能力,帮助开发者编写插件测试",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"main": "dist/index.mjs",
|
|
10
|
+
"typings": "dist/index.d.mts",
|
|
11
|
+
"keywords": [],
|
|
12
|
+
"author": "fraqjs",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/fraqjs/fraq.git"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://fraq.dev/docs/plugins/mock",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"@fraqjs/fraq": "^0.14.0"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=22"
|
|
24
|
+
},
|
|
25
|
+
"fraq": {
|
|
26
|
+
"category": "infrastructure"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsdown",
|
|
30
|
+
"test": "tsx --test test/*.test.ts"
|
|
31
|
+
}
|
|
32
|
+
}
|