process.env.NODE_ENV = "test"; import * as assert from "node:assert"; import type * as childProcess from "node:child_process"; import { Following } from "../src/models/entities/following.js"; import { api, connectStream, initTestDb, post, shutdownServer, signup, startServer, waitFire, } from "./utils.js"; describe("Streaming", () => { let p: childProcess.ChildProcess; let Followings: any; const follow = async (follower: any, followee: any) => { await Followings.save({ id: "a", createdAt: new Date(), followerId: follower.id, followeeId: followee.id, followerHost: follower.host, followerInbox: null, followerSharedInbox: null, followeeHost: followee.host, followeeInbox: null, followeeSharedInbox: null, }); }; describe("Streaming", () => { // Local users let ayano: any; let kyoko: any; let chitose: any; // Remote users let akari: any; let chinatsu: any; let kyokoNote: any; let list: any; before(async () => { p = await startServer(); const connection = await initTestDb(true); Followings = connection.getRepository(Following); ayano = await signup({ username: "ayano" }); kyoko = await signup({ username: "kyoko" }); chitose = await signup({ username: "chitose" }); akari = await signup({ username: "akari", host: "example.com" }); chinatsu = await signup({ username: "chinatsu", host: "example.com" }); kyokoNote = await post(kyoko, { text: "foo" }); // Follow: ayano => kyoko await api("following/create", { userId: kyoko.id }, ayano); // Follow: ayano => akari await follow(ayano, akari); // List: chitose => ayano, kyoko list = await api( "users/lists/create", { name: "my list", }, chitose, ).then((x) => x.body); await api( "users/lists/push", { listId: list.id, userId: ayano.id, }, chitose, ); await api( "users/lists/push", { listId: list.id, userId: kyoko.id, }, chitose, ); }); after(async () => { await shutdownServer(p); }); describe("Events", () => { it("mention event", async () => { const fired = await waitFire( kyoko, "main", // kyoko:main () => post(ayano, { text: "foo @kyoko bar" }), // ayano mention => kyoko (msg) => msg.type === "mention" && msg.body.userId === ayano.id, // wait ayano ); assert.strictEqual(fired, true); }); it("renote event", async () => { const fired = await waitFire( kyoko, "main", // kyoko:main () => post(ayano, { renoteId: kyokoNote.id }), // ayano renote (msg) => msg.type === "renote" && msg.body.renoteId === kyokoNote.id, // wait renote ); assert.strictEqual(fired, true); }); }); describe("Home Timeline", () => { it("自分の投稿が流れる", async () => { const fired = await waitFire( ayano, "homeTimeline", // ayano:Home () => api("notes/create", { text: "foo" }, ayano), // ayano posts (msg) => msg.type === "note" && msg.body.text === "foo", ); assert.strictEqual(fired, true); }); it("フォローしているユーザーの投稿が流れる", async () => { const fired = await waitFire( ayano, "homeTimeline", // ayano:home () => api("notes/create", { text: "foo" }, kyoko), // kyoko posts (msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko ); assert.strictEqual(fired, true); }); it("フォローしていないユーザーの投稿は流れない", async () => { const fired = await waitFire( kyoko, "homeTimeline", // kyoko:home () => api("notes/create", { text: "foo" }, ayano), // ayano posts (msg) => msg.type === "note" && msg.body.userId === ayano.id, // wait ayano ); assert.strictEqual(fired, false); }); it("フォローしているユーザーのダイレクト投稿が流れる", async () => { const fired = await waitFire( ayano, "homeTimeline", // ayano:home () => api( "notes/create", { text: "foo", visibility: "specified", visibleUserIds: [ayano.id], }, kyoko, ), // kyoko dm => ayano (msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko ); assert.strictEqual(fired, true); }); it("フォローしているユーザーでも自分が指定されていないダイレクト投稿は流れない", async () => { const fired = await waitFire( ayano, "homeTimeline", // ayano:home () => api( "notes/create", { text: "foo", visibility: "specified", visibleUserIds: [chitose.id], }, kyoko, ), // kyoko dm => chitose (msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko ); assert.strictEqual(fired, false); }); }); // Home describe("Local Timeline", () => { it("自分の投稿が流れる", async () => { const fired = await waitFire( ayano, "localTimeline", // ayano:Local () => api("notes/create", { text: "foo" }, ayano), // ayano posts (msg) => msg.type === "note" && msg.body.text === "foo", ); assert.strictEqual(fired, true); }); it("フォローしていないローカルユーザーの投稿が流れる", async () => { const fired = await waitFire( ayano, "localTimeline", // ayano:Local () => api("notes/create", { text: "foo" }, chitose), // chitose posts (msg) => msg.type === "note" && msg.body.userId === chitose.id, // wait chitose ); assert.strictEqual(fired, true); }); it("リモートユーザーの投稿は流れない", async () => { const fired = await waitFire( ayano, "localTimeline", // ayano:Local () => api("notes/create", { text: "foo" }, chinatsu), // chinatsu posts (msg) => msg.type === "note" && msg.body.userId === chinatsu.id, // wait chinatsu ); assert.strictEqual(fired, false); }); it("フォローしてたとしてもリモートユーザーの投稿は流れない", async () => { const fired = await waitFire( ayano, "localTimeline", // ayano:Local () => api("notes/create", { text: "foo" }, akari), // akari posts (msg) => msg.type === "note" && msg.body.userId === akari.id, // wait akari ); assert.strictEqual(fired, false); }); it("ホーム指定の投稿は流れない", async () => { const fired = await waitFire( ayano, "localTimeline", // ayano:Local () => api("notes/create", { text: "foo", visibility: "home" }, kyoko), // kyoko home posts (msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko ); assert.strictEqual(fired, false); }); it("フォローしているローカルユーザーのダイレクト投稿は流れない", async () => { const fired = await waitFire( ayano, "localTimeline", // ayano:Local () => api( "notes/create", { text: "foo", visibility: "specified", visibleUserIds: [ayano.id], }, kyoko, ), // kyoko DM => ayano (msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko ); assert.strictEqual(fired, false); }); it("フォローしていないローカルユーザーのフォロワー宛て投稿は流れない", async () => { const fired = await waitFire( ayano, "localTimeline", // ayano:Local () => api( "notes/create", { text: "foo", visibility: "followers" }, chitose, ), (msg) => msg.type === "note" && msg.body.userId === chitose.id, // wait chitose ); assert.strictEqual(fired, false); }); }); describe("Recommended Timeline", () => { it("自分の投稿が流れる", async () => { const fired = await waitFire( ayano, "recommendedTimeline", // ayano:Local () => api("notes/create", { text: "foo" }, ayano), // ayano posts (msg) => msg.type === "note" && msg.body.text === "foo", ); assert.strictEqual(fired, true); }); it("フォローしていないローカルユーザーの投稿が流れる", async () => { const fired = await waitFire( ayano, "recommendedTimeline", // ayano:Local () => api("notes/create", { text: "foo" }, chitose), // chitose posts (msg) => msg.type === "note" && msg.body.userId === chitose.id, // wait chitose ); assert.strictEqual(fired, true); }); it("リモートユーザーの投稿は流れない", async () => { const fired = await waitFire( ayano, "recommendedTimeline", // ayano:Local () => api("notes/create", { text: "foo" }, chinatsu), // chinatsu posts (msg) => msg.type === "note" && msg.body.userId === chinatsu.id, // wait chinatsu ); assert.strictEqual(fired, false); }); it("フォローしてたとしてもリモートユーザーの投稿は流れない", async () => { const fired = await waitFire( ayano, "recommendedTimeline", // ayano:Local () => api("notes/create", { text: "foo" }, akari), // akari posts (msg) => msg.type === "note" && msg.body.userId === akari.id, // wait akari ); assert.strictEqual(fired, false); }); it("ホーム指定の投稿は流れない", async () => { const fired = await waitFire( ayano, "recommendedTimeline", // ayano:Local () => api("notes/create", { text: "foo", visibility: "home" }, kyoko), // kyoko home posts (msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko ); assert.strictEqual(fired, false); }); it("フォローしているローカルユーザーのダイレクト投稿は流れない", async () => { const fired = await waitFire( ayano, "recommendedTimeline", // ayano:Local () => api( "notes/create", { text: "foo", visibility: "specified", visibleUserIds: [ayano.id], }, kyoko, ), // kyoko DM => ayano (msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko ); assert.strictEqual(fired, false); }); it("フォローしていないローカルユーザーのフォロワー宛て投稿は流れない", async () => { const fired = await waitFire( ayano, "recommendedTimeline", // ayano:Local () => api( "notes/create", { text: "foo", visibility: "followers" }, chitose, ), (msg) => msg.type === "note" && msg.body.userId === chitose.id, // wait chitose ); assert.strictEqual(fired, false); }); }); describe("Hybrid Timeline", () => { it("自分の投稿が流れる", async () => { const fired = await waitFire( ayano, "hybridTimeline", // ayano:Hybrid () => api("notes/create", { text: "foo" }, ayano), // ayano posts (msg) => msg.type === "note" && msg.body.text === "foo", ); assert.strictEqual(fired, true); }); it("フォローしていないローカルユーザーの投稿が流れる", async () => { const fired = await waitFire( ayano, "hybridTimeline", // ayano:Hybrid () => api("notes/create", { text: "foo" }, chitose), // chitose posts (msg) => msg.type === "note" && msg.body.userId === chitose.id, // wait chitose ); assert.strictEqual(fired, true); }); it("フォローしているリモートユーザーの投稿が流れる", async () => { const fired = await waitFire( ayano, "hybridTimeline", // ayano:Hybrid () => api("notes/create", { text: "foo" }, akari), // akari posts (msg) => msg.type === "note" && msg.body.userId === akari.id, // wait akari ); assert.strictEqual(fired, true); }); it("フォローしていないリモートユーザーの投稿は流れない", async () => { const fired = await waitFire( ayano, "hybridTimeline", // ayano:Hybrid () => api("notes/create", { text: "foo" }, chinatsu), // chinatsu posts (msg) => msg.type === "note" && msg.body.userId === chinatsu.id, // wait chinatsu ); assert.strictEqual(fired, false); }); it("フォローしているユーザーのダイレクト投稿が流れる", async () => { const fired = await waitFire( ayano, "hybridTimeline", // ayano:Hybrid () => api( "notes/create", { text: "foo", visibility: "specified", visibleUserIds: [ayano.id], }, kyoko, ), (msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko ); assert.strictEqual(fired, true); }); it("フォローしているユーザーのホーム投稿が流れる", async () => { const fired = await waitFire( ayano, "hybridTimeline", // ayano:Hybrid () => api("notes/create", { text: "foo", visibility: "home" }, kyoko), (msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko ); assert.strictEqual(fired, true); }); it("フォローしていないローカルユーザーのホーム投稿は流れない", async () => { const fired = await waitFire( ayano, "hybridTimeline", // ayano:Hybrid () => api("notes/create", { text: "foo", visibility: "home" }, chitose), (msg) => msg.type === "note" && msg.body.userId === chitose.id, ); assert.strictEqual(fired, false); }); it("フォローしていないローカルユーザーのフォロワー宛て投稿は流れない", () => async () => { const fired = await waitFire( ayano, "hybridTimeline", // ayano:Hybrid () => api( "notes/create", { text: "foo", visibility: "followers" }, chitose, ), (msg) => msg.type === "note" && msg.body.userId === chitose.id, ); assert.strictEqual(fired, false); }); }); describe("Global Timeline", () => { it("フォローしていないローカルユーザーの投稿が流れる", () => async () => { const fired = await waitFire( ayano, "globalTimeline", // ayano:Global () => api("notes/create", { text: "foo" }, chitose), // chitose posts (msg) => msg.type === "note" && msg.body.userId === chitose.id, // wait chitose ); assert.strictEqual(fired, true); }); it("フォローしていないリモートユーザーの投稿が流れる", () => async () => { const fired = await waitFire( ayano, "globalTimeline", // ayano:Global () => api("notes/create", { text: "foo" }, chinatsu), // chinatsu posts (msg) => msg.type === "note" && msg.body.userId === chinatsu.id, // wait chinatsu ); assert.strictEqual(fired, true); }); it("ホーム投稿は流れない", () => async () => { const fired = await waitFire( ayano, "globalTimeline", // ayano:Global () => api("notes/create", { text: "foo", visibility: "home" }, kyoko), // kyoko posts (msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko ); assert.strictEqual(fired, false); }); }); describe("UserList Timeline", () => { it("リストに入れているユーザーの投稿が流れる", () => async () => { const fired = await waitFire( chitose, "userList", () => api("notes/create", { text: "foo" }, ayano), (msg) => msg.type === "note" && msg.body.userId === ayano.id, { listId: list.id }, ); assert.strictEqual(fired, true); }); it("リストに入れていないユーザーの投稿は流れない", () => async () => { const fired = await waitFire( chitose, "userList", () => api("notes/create", { text: "foo" }, chinatsu), (msg) => msg.type === "note" && msg.body.userId === chinatsu.id, { listId: list.id }, ); assert.strictEqual(fired, false); }); // #4471 it("リストに入れているユーザーのダイレクト投稿が流れる", () => async () => { const fired = await waitFire( chitose, "userList", () => api( "notes/create", { text: "foo", visibility: "specified", visibleUserIds: [chitose.id], }, ayano, ), (msg) => msg.type === "note" && msg.body.userId === ayano.id, { listId: list.id }, ); assert.strictEqual(fired, true); }); // #4335 it("リストに入れているがフォローはしてないユーザーのフォロワー宛て投稿は流れない", () => async () => { const fired = await waitFire( chitose, "userList", () => api( "notes/create", { text: "foo", visibility: "followers" }, kyoko, ), (msg) => msg.type === "note" && msg.body.userId === kyoko.id, { listId: list.id }, ); assert.strictEqual(fired, false); }); }); describe("Hashtag Timeline", () => { it("指定したハッシュタグの投稿が流れる", () => new Promise(async (done) => { const ws = await connectStream( chitose, "hashtag", ({ type, body }) => { if (type == "note") { assert.deepStrictEqual(body.text, "#foo"); ws.close(); done(); } }, { q: [["foo"]], }, ); post(chitose, { text: "#foo", }); })); it("指定したハッシュタグの投稿が流れる (AND)", () => new Promise(async (done) => { let fooCount = 0; let barCount = 0; let fooBarCount = 0; const ws = await connectStream( chitose, "hashtag", ({ type, body }) => { if (type == "note") { if (body.text === "#foo") fooCount++; if (body.text === "#bar") barCount++; if (body.text === "#foo #bar") fooBarCount++; } }, { q: [["foo", "bar"]], }, ); post(chitose, { text: "#foo", }); post(chitose, { text: "#bar", }); post(chitose, { text: "#foo #bar", }); setTimeout(() => { assert.strictEqual(fooCount, 0); assert.strictEqual(barCount, 0); assert.strictEqual(fooBarCount, 1); ws.close(); done(); }, 3000); })); it("指定したハッシュタグの投稿が流れる (OR)", () => new Promise(async (done) => { let fooCount = 0; let barCount = 0; let fooBarCount = 0; let piyoCount = 0; const ws = await connectStream( chitose, "hashtag", ({ type, body }) => { if (type == "note") { if (body.text === "#foo") fooCount++; if (body.text === "#bar") barCount++; if (body.text === "#foo #bar") fooBarCount++; if (body.text === "#piyo") piyoCount++; } }, { q: [["foo"], ["bar"]], }, ); post(chitose, { text: "#foo", }); post(chitose, { text: "#bar", }); post(chitose, { text: "#foo #bar", }); post(chitose, { text: "#piyo", }); setTimeout(() => { assert.strictEqual(fooCount, 1); assert.strictEqual(barCount, 1); assert.strictEqual(fooBarCount, 1); assert.strictEqual(piyoCount, 0); ws.close(); done(); }, 3000); })); it("指定したハッシュタグの投稿が流れる (AND + OR)", () => new Promise(async (done) => { let fooCount = 0; let barCount = 0; let fooBarCount = 0; let piyoCount = 0; let waaaCount = 0; const ws = await connectStream( chitose, "hashtag", ({ type, body }) => { if (type == "note") { if (body.text === "#foo") fooCount++; if (body.text === "#bar") barCount++; if (body.text === "#foo #bar") fooBarCount++; if (body.text === "#piyo") piyoCount++; if (body.text === "#waaa") waaaCount++; } }, { q: [["foo", "bar"], ["piyo"]], }, ); post(chitose, { text: "#foo", }); post(chitose, { text: "#bar", }); post(chitose, { text: "#foo #bar", }); post(chitose, { text: "#piyo", }); post(chitose, { text: "#waaa", }); setTimeout(() => { assert.strictEqual(fooCount, 0); assert.strictEqual(barCount, 0); assert.strictEqual(fooBarCount, 1); assert.strictEqual(piyoCount, 1); assert.strictEqual(waaaCount, 0); ws.close(); done(); }, 3000); })); }); }); });