Compare commits

...

5 Commits

Author SHA1 Message Date
syuilo 2a7bc847b0
2023.10.0-beta.3 2023-10-05 11:00:46 +09:00
syuilo 2333bdb98a fix(backend): sinceIdやsinceDateを利用してTLを取得することができない
Fix #11961
2023-10-05 10:23:58 +09:00
syuilo 979741ce09 tweak ui 2023-10-05 10:03:13 +09:00
syuilo 5b00fa6f82 enhance(backend): users/notesでチャンネル投稿を含めるオプション
Resolve #11965
2023-10-05 09:48:45 +09:00
syuilo d2bb35bcf3 add more tests 2023-10-05 09:31:03 +09:00
10 changed files with 96 additions and 25 deletions

View File

@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2023.10.0-beta.2",
"version": "2023.10.0-beta.3",
"codename": "nasubi",
"repository": {
"type": "git",

View File

@ -806,6 +806,12 @@ export class NoteCreateService implements OnApplicationShutdown {
const redisPipeline = this.redisForTimelines.pipeline();
if (note.channelId) {
redisPipeline.xadd(
`userTimelineWithChannel:${user.id}`,
'MAXLEN', '~', note.userHost == null ? meta.perLocalUserUserTimelineCacheMax.toString() : meta.perRemoteUserUserTimelineCacheMax.toString(),
'*',
'note', note.id);
const channelFollowings = await this.channelFollowingsRepository.find({
where: {
followeeId: note.channelId,

View File

@ -79,14 +79,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
let timeline: MiNote[] = [];
const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
let noteIdsRes: [string, string[]][] = [];
if (!ps.sinceId && !ps.sinceDate) {
noteIdsRes = await this.redisForTimelines.xrevrange(
`channelTimeline:${channel.id}`,
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
'-',
ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-',
'COUNT', limit);
}
@ -110,7 +110,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
timeline = await query.limit(ps.limit).getMany();
} else {
const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);
const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId);
if (noteIds.length === 0) {
return [];

View File

@ -91,7 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
let timeline: MiNote[] = [];
const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
let htlNoteIdsRes: [string, string[]][] = [];
let ltlNoteIdsRes: [string, string[]][] = [];
@ -100,18 +100,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.redisForTimelines.xrevrange(
ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`,
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
'-',
ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-',
'COUNT', limit),
this.redisForTimelines.xrevrange(
ps.withFiles ? 'localTimelineWithFiles' : 'localTimeline',
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
'-',
ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-',
'COUNT', limit),
]);
}
const htlNoteIds = htlNoteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);
const ltlNoteIds = ltlNoteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);
const htlNoteIds = htlNoteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId);
const ltlNoteIds = ltlNoteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId);
let noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds]));
noteIds.sort((a, b) => a > b ? -1 : 1);
noteIds = noteIds.slice(0, ps.limit);

View File

@ -87,18 +87,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
let timeline: MiNote[] = [];
const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
let noteIdsRes: [string, string[]][] = [];
if (!ps.sinceId && !ps.sinceDate) {
noteIdsRes = await this.redisForTimelines.xrevrange(
ps.withFiles ? 'localTimelineWithFiles' : 'localTimeline',
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
'-',
ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-',
'COUNT', limit);
}
const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);
const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId);
if (noteIds.length === 0) {
return [];

View File

@ -78,18 +78,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
let timeline: MiNote[] = [];
const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
let noteIdsRes: [string, string[]][] = [];
if (!ps.sinceId && !ps.sinceDate) {
noteIdsRes = await this.redisForTimelines.xrevrange(
ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`,
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
'-',
ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-',
'COUNT', limit);
}
const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);
const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId);
if (noteIds.length === 0) {
return [];

View File

@ -102,18 +102,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
let timeline: MiNote[] = [];
const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
let noteIdsRes: [string, string[]][] = [];
if (!ps.sinceId && !ps.sinceDate) {
noteIdsRes = await this.redisForTimelines.xrevrange(
ps.withFiles ? `userListTimelineWithFiles:${list.id}` : `userListTimeline:${list.id}`,
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
'-',
ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-',
'COUNT', limit);
}
const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);
const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId);
if (noteIds.length === 0) {
return [];

View File

@ -44,6 +44,7 @@ export const paramDef = {
userId: { type: 'string', format: 'misskey:id' },
withReplies: { type: 'boolean', default: false },
withRenotes: { type: 'boolean', default: true },
withChannelNotes: { type: 'boolean', default: false },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
@ -79,30 +80,39 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
let timeline: MiNote[] = [];
const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
let noteIdsRes: [string, string[]][] = [];
let repliesNoteIdsRes: [string, string[]][] = [];
let channelNoteIdsRes: [string, string[]][] = [];
if (!ps.sinceId && !ps.sinceDate) {
[noteIdsRes, repliesNoteIdsRes] = await Promise.all([
[noteIdsRes, repliesNoteIdsRes, channelNoteIdsRes] = await Promise.all([
this.redisForTimelines.xrevrange(
ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : `userTimeline:${ps.userId}`,
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
'-',
ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-',
'COUNT', limit),
ps.withReplies
? this.redisForTimelines.xrevrange(
`userTimelineWithReplies:${ps.userId}`,
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
'-',
ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-',
'COUNT', limit)
: Promise.resolve([]),
ps.withChannelNotes
? this.redisForTimelines.xrevrange(
`userTimelineWithChannel:${ps.userId}`,
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : ps.sinceDate ?? '-',
'COUNT', limit)
: Promise.resolve([]),
]);
}
let noteIds = Array.from(new Set([
...noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId),
...repliesNoteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId),
...noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId),
...repliesNoteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId),
...channelNoteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId && x !== ps.sinceId),
]));
noteIds.sort((a, b) => a > b ? -1 : 1);
noteIds = noteIds.slice(0, ps.limit);

View File

@ -363,6 +363,21 @@ describe('Timelines', () => {
assert.strictEqual(res.body.some((note: any) => note.id === carolNote1.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === carolNote2.id), false);
}, 1000 * 10);
test.concurrent('フォローしているユーザーのチャンネル投稿が含まれない', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body);
await api('/following/create', { userId: bob.id }, alice);
await sleep(1000);
const bobNote = await post(bob, { text: 'hi', channelId: channel.id });
await waitForPushToTl();
const res = await api('/notes/timeline', {}, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
});
});
describe('Local TL', () => {
@ -380,6 +395,19 @@ describe('Timelines', () => {
assert.strictEqual(res.body.some((note: any) => note.id === carolNote.id), false);
});
test.concurrent('チャンネル投稿が含まれない', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body);
const bobNote = await post(bob, { text: 'hi', channelId: channel.id });
await waitForPushToTl();
const res = await api('/notes/local-timeline', {}, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
});
test.concurrent('リモートユーザーのノートが含まれない', async () => {
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
@ -776,6 +804,19 @@ describe('Timelines', () => {
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi');
});
test.concurrent('チャンネル投稿が含まれない', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body);
const bobNote = await post(bob, { text: 'hi', channelId: channel.id });
await waitForPushToTl();
const res = await api('/users/notes', { userId: bob.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
});
test.concurrent('[withReplies: false] 他人への返信が含まれない', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
@ -836,6 +877,19 @@ describe('Timelines', () => {
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
}, 1000 * 10);
test.concurrent('[withChannelNotes: true] チャンネル投稿が含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
const channel = await api('/channels/create', { name: 'channel' }, bob).then(x => x.body);
const bobNote = await post(bob, { text: 'hi', channelId: channel.id });
await waitForPushToTl();
const res = await api('/users/notes', { userId: bob.id, withChannelNotes: true }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
});
test.concurrent('ミュートしているユーザーに関連する投稿が含まれない', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);

View File

@ -29,7 +29,7 @@ const props = defineProps<{
user: Misskey.entities.UserDetailed;
}>();
const include = ref<string | null>(null);
const include = ref<string | null>('all');
const pagination = {
endpoint: 'users/notes' as const,
@ -38,6 +38,7 @@ const pagination = {
userId: props.user.id,
withRenotes: include.value === 'all',
withReplies: include.value === 'all' || include.value === 'files',
withChannelNotes: include.value === 'all',
withFiles: include.value === 'files',
})),
};