-
Notifications
You must be signed in to change notification settings - Fork 74
/
Copy pathmessage.ts
320 lines (262 loc) · 12 KB
/
message.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
import { getterProperty, hiddenProperty, Specable } from "../utils/specable";
import { CharacterAI, CheckAndThrow } from "../client";
import { CAIImage } from "../utils/image";
import ObjectPatcher from "../utils/patcher";
import { Candidate, EditedCandidate } from "./candidate";
import { Conversation } from "./conversation";
import Parser from "../parser";
import DMConversation from "./dmConversation";
import { GroupChatConversation } from "../groupchat/groupChatConversation";
import Warnings from "../warnings";
export class CAIMessage extends Specable {
@hiddenProperty
private client: CharacterAI;
@hiddenProperty
private conversation: Conversation;
@hiddenProperty
private image?: CAIImage;
// turn_key
@hiddenProperty
private turn_key: any = {};
@getterProperty
public get turnKey() { return {
chatId: this.turn_key.chat_id,
turnId: this.turn_key.turn_id,
}; }
@getterProperty
public get turnId(): string { return this.turnKey.turnId; }
@getterProperty
public get chatId() { return this.turnKey.chatId; }
// create_time
@hiddenProperty
private create_time = "";
@getterProperty
public get creationDate() { return new Date(this.create_time); }
// last_update_time
@hiddenProperty
private last_update_time = "";
@getterProperty
public get lastUpdateDate() { return new Date(this.last_update_time); }
// state
public state = "STATE_OK";
// author
@hiddenProperty
private author: any = {};
@getterProperty
public get authorId() { return this.author.author_id; }
@getterProperty
public get isHuman() { return this.author.is_human ?? false; }
@getterProperty
public get authorUsername() { return this.author.name; }
// is_pinned
@hiddenProperty
private is_pinned: boolean = false;
@getterProperty
public get isPinned() { return this.is_pinned; }
public set isPinned(value) { this.is_pinned = value; }
async getAuthorProfile() {
this.client.checkAndThrow(CheckAndThrow.RequiresAuthentication);
const { authorUsername, isHuman } = this;
if (!isHuman) throw Error("Failed to fetch author because this is message was not made by a human.");
return await this.client.fetchProfileByUsername(authorUsername);
}
async getCharacter() {
this.client.checkAndThrow(CheckAndThrow.RequiresAuthentication);
const { authorId, isHuman } = this;
if (isHuman) throw Error("Failed to fetch character because this is message was not made by a character.");
return await this.client.fetchCharacter(authorId);
}
// candidates
// |_ candidate_id
// |_ create_time
// |_ raw_content?
// |_ is_final?
@hiddenProperty
private candidates: any[] = [];
// the main request forces you to use candidates
@hiddenProperty
private candidateIdToCandidate: Record<string, Candidate> = {};
// the way candidates work is basically the messages that get edited you have
// a way to select between 1/30 candidates and [0] will always be the latest candidate
// turn key is the identifier that holds them (aka a "message.id")
// combo is chat id + turn key but we already have the chat id
// turn keys are indexed by the conversation
// this function will index candidates and give them proper instances
private addCandidate(candidateObject: any, addAfterToActualRawCandidates: boolean) {
const isEditedCandidate = this.candidates.length > 1;
const candidate = isEditedCandidate
? new EditedCandidate(this.client, this, candidateObject)
: new Candidate(this.client, this, candidateObject);
if (candidate.wasFlagged)
Warnings.show('contentFiltered');
this.candidateIdToCandidate[candidate.candidateId] = candidate;
if (addAfterToActualRawCandidates) this.candidates.unshift(candidateObject);
}
private indexCandidates() {
this.candidateIdToCandidate = {};
for (let i = 0; i < this.candidates.length; i++) {
const candidateObject = this.candidates[i];
this.addCandidate(candidateObject, false); // we use previously added, no need to readd
}
}
private getCandidateByTurnId(turnId: string) {
const candidate = this.candidateIdToCandidate[turnId];
if (!candidate) throw new Error("Candidate not found");
return candidate;
}
private getCandidateByIndex(index: number) {
const candidate = Object.values(this.candidateIdToCandidate)[index];
if (!candidate) throw new Error("Candidate not found");
return candidate;
}
public getCandidates(): Record<string, Candidate> {
// create copy to avoid modification
return {...this.candidateIdToCandidate};
}
// get primaryCandidate
// content is influenced by the primary candidate to save time/braincells
public get content() { return this.primaryCandidate?.content ?? ""; }
public get wasFlagged() { return this.primaryCandidate?.wasFlagged ?? false; }
// primary_candidate_id
@hiddenProperty
private primary_candidate_id = "";
public get primaryCandidate() { return this.getCandidateByTurnId(this.primary_candidate_id); }
// use specific candidate id to change specific id or it will change the latest
async edit(newContent: string, specificCandidateId?: string) {
this.client.checkAndThrow(CheckAndThrow.RequiresAuthentication);
const candidateId = specificCandidateId ?? this.primaryCandidate.candidateId;
let request;
if (this.conversation instanceof DMConversation) {
request = await this.client.sendDMWebsocketCommandAsync({
command: "edit_turn_candidate",
originId: "Android",
streaming: true,
waitForAIResponse: false,
payload: {
turn_key: this.turn_key,
current_candidate_id: candidateId,
new_candidate_raw_content: newContent
}
});
} else {
}
this.candidates = request.pop().turn.candidates;
this.indexCandidates();
}
// next/previous/candidate_id
private async internalSwitchPrimaryCandidate(candidate: 'next' | 'previous' | string) {
this.client.checkAndThrow(CheckAndThrow.RequiresAuthentication);
let candidateId = candidate;
let candidates = this.getCandidates();
const candidateValues = Object.values(candidates);
const primaryCandidateIndex = Object.keys(candidates).indexOf(this.primary_candidate_id);
if (primaryCandidateIndex != -1) {
if (candidate == 'next') candidateId = candidateValues[primaryCandidateIndex + 1]?.candidateId ?? "";
if (candidate == 'previous') candidateId = candidateValues[primaryCandidateIndex - 1]?.candidateId ?? "";
}
if (candidateId.trim() == "") throw new Error("Cannot find the message, it is invalid or it is out of range.")
const firstRequest = await this.client.requester.request(`https://neo.character.ai/annotations/${this.conversation.chatId}/${this.turnId}/${candidateId}`, {
method: 'POST',
contentType: 'application/json',
body: '{}',
includeAuthorization: true
});
const firstResponse = await Parser.parseJSON(firstRequest);
if (!firstRequest.ok) throw new Error(firstResponse);
await this.client.sendDMWebsocketCommandAsync({
command: "update_primary_candidate",
originId: "Android",
streaming: false,
waitForAIResponse: true,
expectedReturnCommand: "ok",
payload: {
candidate_id: candidateId,
turn_key: this.turn_key,
}
});
this.primary_candidate_id = candidate;
this.indexCandidates();
return this.primaryCandidate;
}
async switchToPreviousCandidate() { return this.internalSwitchPrimaryCandidate("previous"); }
async switchToNextCandidate() { return this.internalSwitchPrimaryCandidate("next"); }
async switchPrimaryCandidateTo(candidateId: string) { return await this.internalSwitchPrimaryCandidate(candidateId); }
async regenerate() { return this.conversation.regenerateMessage(this); }
private getConversationMessageAfterIndex(offset: number): CAIMessage | null {
const conversationMessageIds = this.conversation.messageIds;
let index = conversationMessageIds.indexOf(this.turnId);
if (index == -1) return null;
index += offset;
if (index < 0 || index >= conversationMessageIds.length) return null;
return this.conversation.messages[index];
}
public getMessageBefore() { return this.getConversationMessageAfterIndex(-1); }
public getMessageAfter() { return this.getConversationMessageAfterIndex(1); }
private async getAllMessagesAfter() {
const conversationMessageIds = this.conversation.messageIds;
const index = conversationMessageIds.indexOf(this.turnId);
if (index == -1) return [];
let messagesAfter: CAIMessage[] = [];
// FIXME: might wanna not use the cache for that one
for (let i = index; i < conversationMessageIds.length; i++)
messagesAfter.push(this.conversation.messages[i]);
return messagesAfter;
}
private async handlePin(isPinned: boolean) {
this.client.checkAndThrow(CheckAndThrow.RequiresAuthentication);
if (this.isPinned == isPinned) return;
await this.client.sendDMWebsocketCommandAsync({
command: "set_turn_pin",
originId: "Android",
streaming: false,
waitForAIResponse: false,
payload: {
turn_key: this.turn_key,
is_pinned: isPinned
}
});
this.isPinned = isPinned;
}
async pin() { await this.handlePin(true); }
async unpin() { await this.handlePin(false); }
// https://neo.character.ai/chat/id/copy
async copyFromHere(isDM: boolean = true): Promise<DMConversation | GroupChatConversation> {
this.client.checkAndThrow(CheckAndThrow.RequiresAuthentication);
const request = await this.client.requester.request(`https://neo.character.ai/chat/${this.conversation.chatId}/copy`, {
method: 'POST',
contentType: 'application/json',
body: Parser.stringify({ end_turn_id: this.turnId }),
includeAuthorization: true
});
const response = await Parser.parseJSON(request);
if (!request.ok) throw new Error(String(response));
const { new_chat_id: newChatId } = response;
return isDM
? await this.client.fetchDMConversation(newChatId)
: await this.client.fetchGroupChatConversation();
}
async rewindFromHere() {
this.client.checkAndThrow(CheckAndThrow.RequiresAuthentication);
if (this.conversation.getLastMessage()?.turnId == this.turnId)
throw new Error("You cannot rewind from the last message in the conversation.");
return await this.conversation.deleteMessagesInBulk(await this.getAllMessagesAfter());
}
async delete() { return await this.conversation.deleteMessage(this); }
// TTS
async getTTSUrlWithQuery(voiceQuery?: string) { return await this.primaryCandidate.getTTSUrlWithQuery(voiceQuery); }
async getTTSUrl(voiceId: string) { return await this.primaryCandidate.getTTSUrl(voiceId); }
/**
* Patches and indexes an unsanitized turn.
* @remarks **Do not use this method.** It is meant for internal use only.
*/
public indexTurn(turn: any) {
ObjectPatcher.patch(this.client, this, turn);
this.indexCandidates();
}
constructor(client: CharacterAI, conversation: Conversation, turn: any) {
super();
this.client = client;
this.conversation = conversation;
this.indexTurn(turn);
}
}