Compare commits

..

2 commits

Author SHA1 Message Date
syuilo
f43aac7291 チャット復活 2025-03-26 08:58:05 +09:00
syuilo
6368dde285 Update package.json 2025-03-26 08:57:34 +09:00
9 changed files with 149 additions and 80 deletions

View file

@ -11,7 +11,7 @@
"dependencies": { "dependencies": {
"@types/chalk": "2.2.0", "@types/chalk": "2.2.0",
"@types/lokijs": "1.5.14", "@types/lokijs": "1.5.14",
"@types/node": "20.11.5", "@types/node": "22.13.13",
"@types/promise-retry": "1.1.6", "@types/promise-retry": "1.1.6",
"@types/random-seed": "0.3.5", "@types/random-seed": "0.3.5",
"@types/seedrandom": "3.0.8", "@types/seedrandom": "3.0.8",
@ -30,12 +30,12 @@
"random-seed": "0.3.0", "random-seed": "0.3.0",
"reconnecting-websocket": "4.4.0", "reconnecting-websocket": "4.4.0",
"seedrandom": "3.0.5", "seedrandom": "3.0.5",
"ts-patch": "3.1.2", "ts-patch": "3.3.0",
"twemoji-parser": "14.0.0", "twemoji-parser": "14.0.0",
"typescript": "5.3.3", "typescript": "5.8.2",
"typescript-transform-paths": "3.4.6", "typescript-transform-paths": "3.5.5",
"uuid": "9.0.1", "uuid": "9.0.1",
"ws": "8.16.0" "ws": "8.18.1"
}, },
"devDependencies": { "devDependencies": {
}, },

View file

@ -54,6 +54,7 @@ export default class 藍 {
private meta: loki.Collection<Meta>; private meta: loki.Collection<Meta>;
private contexts: loki.Collection<{ private contexts: loki.Collection<{
isChat: boolean;
noteId?: string; noteId?: string;
userId?: string; userId?: string;
module: string; module: string;
@ -146,7 +147,7 @@ export default class 藍 {
if (data.text && data.text.startsWith('@' + this.account.username)) { if (data.text && data.text.startsWith('@' + this.account.username)) {
// Misskeyのバグで投稿が非公開扱いになる // Misskeyのバグで投稿が非公開扱いになる
if (data.text == null) data = await this.api('notes/show', { noteId: data.id }); if (data.text == null) data = await this.api('notes/show', { noteId: data.id });
this.onReceiveMessage(new Message(this, data)); this.onReceiveMessage(new Message(this, data, false));
} }
}); });
@ -156,7 +157,7 @@ export default class 藍 {
if (data.text && data.text.startsWith('@' + this.account.username)) return; if (data.text && data.text.startsWith('@' + this.account.username)) return;
// Misskeyのバグで投稿が非公開扱いになる // Misskeyのバグで投稿が非公開扱いになる
if (data.text == null) data = await this.api('notes/show', { noteId: data.id }); if (data.text == null) data = await this.api('notes/show', { noteId: data.id });
this.onReceiveMessage(new Message(this, data)); this.onReceiveMessage(new Message(this, data, false));
}); });
// Renoteされたとき // Renoteされたとき
@ -171,16 +172,48 @@ export default class 藍 {
}); });
}); });
// メッセージ
mainStream.on('messagingMessage', data => {
if (data.userId == this.account.id) return; // 自分は弾く
this.onReceiveMessage(new Message(this, data));
});
// 通知 // 通知
mainStream.on('notification', data => { mainStream.on('notification', data => {
this.onNotification(data); this.onNotification(data);
}); });
// チャット
mainStream.on('newChatMessage', data => {
const fromUser = data.fromUser;
if (data.fromUserId == this.account.id) return; // 自分は弾く
this.onReceiveMessage(new Message(this, data, true));
// 一定期間 chatUser / chatRoom のストリームに接続して今後のやり取りに備える
if (data.fromUserId) {
const chatStream = this.connection.connectToChannel('chatUser', {
otherId: data.fromUserId,
});
let timer;
function setTimer() {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
chatStream.dispose();
}, 1000 * 60 * 2);
}
setTimer();
chatStream.on('message', (data) => {
if (data.fromUserId == this.account.id) return; // 自分は弾く
chatStream.send('read', {
id: data.id,
});
this.onReceiveMessage(new Message(this, {
...data,
// fromUserは省略されてくるため
fromUser: fromUser,
}, true));
setTimer();
});
} else {
// TODO: room
}
});
//#endregion //#endregion
// Install modules // Install modules
@ -218,10 +251,14 @@ export default class 藍 {
return; return;
} }
const isNoContext = msg.replyId == null; const isNoContext = !msg.isChat && msg.replyId == null;
// Look up the context // Look up the context
const context = isNoContext ? null : this.contexts.findOne({ const context = isNoContext ? null : this.contexts.findOne(msg.isChat ? {
isChat: true,
userId: msg.userId
} : {
isChat: false,
noteId: msg.replyId noteId: msg.replyId
}); });
@ -266,12 +303,16 @@ export default class 藍 {
await sleep(1000); await sleep(1000);
} }
// リアクションする if (msg.isChat) {
if (reaction) { // TODO: リアクション?
this.api('notes/reactions/create', { } else {
noteId: msg.id, // リアクションする
reaction: reaction if (reaction) {
}); this.api('notes/reactions/create', {
noteId: msg.id,
reaction: reaction
});
}
} }
} }
@ -366,13 +407,12 @@ export default class 藍 {
} }
/** /**
* *
*/ */
@bindThis @bindThis
public sendMessage(userId: any, param: any) { public sendMessage(userId: any, param: any) {
return this.post(Object.assign({ return this.api('chat/messages/create-to-user', Object.assign({
visibility: 'specified', toUserId: userId,
visibleUserIds: [userId],
}, param)); }, param));
} }
@ -393,12 +433,20 @@ export default class 藍 {
* *
* @param module * @param module
* @param key * @param key
* @param id ID稿ID * @param isChat
* @param id ID稿ID
* @param data * @param data
*/ */
@bindThis @bindThis
public subscribeReply(module: Module, key: string | null, id: string, data?: any) { public subscribeReply(module: Module, key: string | null, isChat: boolean, id: string, data?: any) {
this.contexts.insertOne({ this.contexts.insertOne(isChat ? {
isChat: true,
userId: id,
module: module.name,
key: key,
data: data
} : {
isChat: false,
noteId: id, noteId: id,
module: module.name, module: module.name,
key: key, key: key,

View file

@ -11,30 +11,36 @@ import { sleep } from '@/utils/sleep.js';
export default class Message { export default class Message {
private ai: ; private ai: ;
private note: any; private chatMessage: { id: string; fromUser: any; fromUserId: string; text: string; } | null;
private note: { id: string; user: any; userId: string; text: string; renoteId: string; replyId: string; } | null;
public isChat: boolean;
public get id(): string { public get id(): string {
return this.note.id; return this.chatMessage ? this.chatMessage.id : this.note.id;
} }
public get user(): User { public get user(): User {
return this.note.user; return this.chatMessage ? this.chatMessage.fromUser : this.note.user;
} }
public get userId(): string { public get userId(): string {
return this.note.userId; return this.chatMessage ? this.chatMessage.fromUserId : this.note.userId;
} }
public get text(): string { public get text(): string {
return this.note.text; return this.chatMessage ? this.chatMessage.text : this.note.text;
} }
public get quoteId(): string | null { public get quoteId(): string | null {
return this.note.renoteId; return this.chatMessage ? null : this.note.renoteId;
} }
public get visibility(): string { public get replyId(): string | null {
return this.note.visibility; return this.chatMessage ? null : this.note.replyId;
}
public get visibility(): string | null {
return this.chatMessage ? null : this.note.visibility;
} }
/** /**
@ -48,15 +54,13 @@ export default class Message {
.trim(); .trim();
} }
public get replyId(): string {
return this.note.replyId;
}
public friend: Friend; public friend: Friend;
constructor(ai: , note: any) { constructor(ai: , chatMessageOrNote: any, isChat: boolean) {
this.ai = ai; this.ai = ai;
this.note = note; this.chatMessage = isChat ? chatMessageOrNote : null;
this.note = isChat ? null : chatMessageOrNote;
this.isChat = isChat;
this.friend = new Friend(ai, { user: this.user }); this.friend = new Friend(ai, { user: this.user });
@ -83,19 +87,19 @@ export default class Message {
await sleep(2000); await sleep(2000);
} }
const postData = { if (this.chatMessage) {
replyId: this.note.id, return await this.ai.sendMessage(this.chatMessage.fromUserId, {
text: text, text: text,
fileIds: opts?.file ? [opts?.file.id] : undefined, fileId: opts?.file?.id
cw: opts?.cw, });
renoteId: opts?.renote
};
// DM以外は普通に返信し、DMの場合はDMで返信する
if (this.note.visibility != 'specified') {
return await this.ai.post(postData);
} else { } else {
return await this.ai.sendMessage(this.userId, postData); return await this.ai.post({
replyId: this.note.id,
text: text,
fileIds: opts?.file ? [opts?.file.id] : undefined,
cw: opts?.cw,
renoteId: opts?.renote
});
} }
} }

View file

@ -32,12 +32,13 @@ export default abstract class Module {
/** /**
* *
* @param key * @param key
* @param id ID稿ID * @param isChat
* @param id ID稿ID
* @param data * @param data
*/ */
@bindThis @bindThis
protected subscribeReply(key: string | null, id: string, data?: any) { protected subscribeReply(key: string | null, isChat: boolean, id: string, data?: any) {
this.ai.subscribeReply(this, key, id, data); this.ai.subscribeReply(this, key, isChat, id, data);
} }
/** /**

View file

@ -85,7 +85,7 @@ export default class extends Module {
msg.reply(serifs.core.setNameOk(name)); msg.reply(serifs.core.setNameOk(name));
} else { } else {
msg.reply(serifs.core.san).then(reply => { msg.reply(serifs.core.san).then(reply => {
this.subscribeReply(msg.userId, reply.id, { this.subscribeReply(msg.userId, msg.isChat, msg.isChat ? msg.userId : reply.id, {
name: name name: name
}); });
}); });
@ -143,7 +143,7 @@ export default class extends Module {
done(); done();
} else { } else {
msg.reply(serifs.core.yesOrNo).then(reply => { msg.reply(serifs.core.yesOrNo).then(reply => {
this.subscribeReply(msg.userId, reply.id, data); this.subscribeReply(msg.userId, msg.isChat, reply.id, data);
}); });
} }
} }

View file

@ -49,7 +49,7 @@ export default class extends Module {
}); });
msg.reply(serifs.guessingGame.started).then(reply => { msg.reply(serifs.guessingGame.started).then(reply => {
this.subscribeReply(msg.userId, reply.id); this.subscribeReply(msg.userId, msg.isChat, msg.isChat ? msg.userId : reply.id);
}); });
return true; return true;
@ -83,7 +83,7 @@ export default class extends Module {
if (guess == null) { if (guess == null) {
msg.reply(serifs.guessingGame.nan).then(reply => { msg.reply(serifs.guessingGame.nan).then(reply => {
this.subscribeReply(msg.userId, reply.id); this.subscribeReply(msg.userId, msg.isChat, reply.id);
}); });
return; return;
} }
@ -121,7 +121,7 @@ export default class extends Module {
msg.reply(text).then(reply => { msg.reply(text).then(reply => {
if (!end) { if (!end) {
this.subscribeReply(msg.userId, reply.id); this.subscribeReply(msg.userId, msg.isChat, reply.id);
} }
}); });
} }

View file

@ -75,7 +75,7 @@ export default class extends Module {
postId: post.id postId: post.id
}); });
this.subscribeReply(null, post.id); this.subscribeReply(null, false, post.id);
this.log('New kazutori game started'); this.log('New kazutori game started');

View file

@ -14,6 +14,7 @@ export default class extends Module {
private reminds: loki.Collection<{ private reminds: loki.Collection<{
userId: string; userId: string;
id: string; id: string;
isChat: boolean;
thing: string | null; thing: string | null;
quoteId: string | null; quoteId: string | null;
times: number; // 催促した回数(使うのか?) times: number; // 催促した回数(使うのか?)
@ -69,6 +70,7 @@ export default class extends Module {
const remind = this.reminds.insertOne({ const remind = this.reminds.insertOne({
id: msg.id, id: msg.id,
userId: msg.userId, userId: msg.userId,
isChat: msg.isChat,
thing: thing === '' ? null : thing, thing: thing === '' ? null : thing,
quoteId: msg.quoteId, quoteId: msg.quoteId,
times: 0, times: 0,
@ -76,13 +78,13 @@ export default class extends Module {
}); });
// メンションをsubscribe // メンションをsubscribe
this.subscribeReply(remind!.id, msg.id, { this.subscribeReply(remind!.id, msg.isChat, msg.isChat ? msg.userId : msg.id, {
id: remind!.id id: remind!.id
}); });
if (msg.quoteId) { if (msg.quoteId) {
// 引用元をsubscribe // 引用元をsubscribe
this.subscribeReply(remind!.id, msg.quoteId, { this.subscribeReply(remind!.id, false, msg.quoteId, {
id: remind!.id id: remind!.id
}); });
} }
@ -124,6 +126,7 @@ export default class extends Module {
msg.reply(serifs.reminder.doneFromInvalidUser); msg.reply(serifs.reminder.doneFromInvalidUser);
return; return;
} else { } else {
if (msg.isChat) this.unsubscribeReply(key);
return false; return false;
} }
} }
@ -142,22 +145,28 @@ export default class extends Module {
if (friend == null) return; // 処理の流れ上、実際にnullになることは無さそうだけど一応 if (friend == null) return; // 処理の流れ上、実際にnullになることは無さそうだけど一応
let reply; let reply;
try { if (remind.isChat) {
reply = await this.ai.post({ this.ai.sendMessage(friend.userId, {
renoteId: remind.thing == null && remind.quoteId ? remind.quoteId : remind.id, text: serifs.reminder.notifyWithThing(remind.thing, friend.name)
text: acct(friend.doc.user) + ' ' + serifs.reminder.notify(friend.name)
}); });
} catch (err) { } else {
// renote対象が消されていたらリマインダー解除 try {
if (err.statusCode === 400) { reply = await this.ai.post({
this.unsubscribeReply(remind.thing == null && remind.quoteId ? remind.quoteId : remind.id); renoteId: remind.thing == null && remind.quoteId ? remind.quoteId : remind.id,
this.reminds.remove(remind); text: acct(friend.doc.user) + ' ' + serifs.reminder.notify(friend.name)
});
} catch (err) {
// renote対象が消されていたらリマインダー解除
if (err.statusCode === 400) {
this.unsubscribeReply(remind.thing == null && remind.quoteId ? remind.quoteId : remind.id);
this.reminds.remove(remind);
return;
}
return; return;
} }
return;
} }
this.subscribeReply(remind.id, reply.id, { this.subscribeReply(remind.id, remind.isChat, remind.isChat ? remind.userId : reply.id, {
id: remind.id id: remind.id
}); });

View file

@ -47,6 +47,7 @@ export default class extends Module {
// タイマーセット // タイマーセット
this.setTimeoutWithPersistence(time, { this.setTimeoutWithPersistence(time, {
isChat: msg.isChat,
msgId: msg.id, msgId: msg.id,
userId: msg.friend.userId, userId: msg.friend.userId,
time: str time: str
@ -60,9 +61,15 @@ export default class extends Module {
const friend = this.ai.lookupFriend(data.userId); const friend = this.ai.lookupFriend(data.userId);
if (friend == null) return; // 処理の流れ上、実際にnullになることは無さそうだけど一応 if (friend == null) return; // 処理の流れ上、実際にnullになることは無さそうだけど一応
const text = serifs.timer.notify(data.time, friend.name); const text = serifs.timer.notify(data.time, friend.name);
this.ai.post({ if (data.isChat) {
replyId: data.msgId, this.ai.sendMessage(friend.userId, {
text: text text: text
}); });
} else {
this.ai.post({
replyId: data.msgId,
text: text
});
}
} }
} }