Merge remote-tracking branch 'misskey-original/develop' into develop

# Conflicts:
#	packages/backend/src/server/api/stream/channels/home-timeline.ts
#	packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
#	pnpm-lock.yaml
This commit is contained in:
mattyatea 2023-10-20 09:55:24 +09:00
commit d421670b54
43 changed files with 580 additions and 558 deletions

View file

@ -76,7 +76,7 @@
"@nestjs/testing": "10.2.7",
"@peertube/http-signature": "1.7.0",
"@simplewebauthn/server": "8.3.2",
"@sinonjs/fake-timers": "11.1.0",
"@sinonjs/fake-timers": "11.2.1",
"@swc/cli": "0.1.62",
"@swc/core": "1.3.93",
"accepts": "1.3.8",
@ -86,7 +86,7 @@
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.2",
"bullmq": "4.12.4",
"bullmq": "4.12.5",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.1",
"chalk": "5.3.0",
@ -97,7 +97,7 @@
"content-disposition": "0.5.4",
"date-fns": "2.30.0",
"deep-email-validator": "0.1.21",
"fastify": "4.24.2",
"fastify": "4.24.3",
"feed": "4.2.2",
"file-type": "18.5.0",
"fluent-ffmpeg": "2.1.2",
@ -180,38 +180,38 @@
"@types/cbor": "6.0.0",
"@types/color-convert": "2.0.2",
"@types/content-disposition": "0.5.7",
"@types/fluent-ffmpeg": "2.1.22",
"@types/http-link-header": "1.0.3",
"@types/jest": "29.5.5",
"@types/js-yaml": "4.0.7",
"@types/jsdom": "21.1.3",
"@types/jsonld": "1.5.10",
"@types/jsrsasign": "10.5.9",
"@types/mime-types": "2.1.2",
"@types/ms": "0.7.32",
"@types/node": "20.8.6",
"@types/fluent-ffmpeg": "2.1.23",
"@types/http-link-header": "1.0.4",
"@types/jest": "29.5.6",
"@types/js-yaml": "4.0.8",
"@types/jsdom": "21.1.4",
"@types/jsonld": "1.5.11",
"@types/jsrsasign": "10.5.10",
"@types/mime-types": "2.1.3",
"@types/ms": "0.7.33",
"@types/node": "20.8.7",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.11",
"@types/oauth": "0.9.2",
"@types/oauth2orize": "1.11.1",
"@types/oauth2orize-pkce": "0.1.0",
"@types/pg": "8.10.5",
"@types/pug": "2.0.7",
"@types/punycode": "2.1.0",
"@types/qrcode": "1.5.2",
"@types/random-seed": "0.3.3",
"@types/ratelimiter": "3.4.4",
"@types/rename": "1.0.5",
"@types/sanitize-html": "2.9.2",
"@types/semver": "7.5.3",
"@types/nodemailer": "6.4.13",
"@types/oauth": "0.9.3",
"@types/oauth2orize": "1.11.2",
"@types/oauth2orize-pkce": "0.1.1",
"@types/pg": "8.10.7",
"@types/pug": "2.0.8",
"@types/punycode": "2.1.1",
"@types/qrcode": "1.5.4",
"@types/random-seed": "0.3.4",
"@types/ratelimiter": "3.4.5",
"@types/rename": "1.0.6",
"@types/sanitize-html": "2.9.3",
"@types/semver": "7.5.4",
"@types/sharp": "0.32.0",
"@types/simple-oauth2": "5.0.5",
"@types/sinonjs__fake-timers": "8.1.3",
"@types/tinycolor2": "1.4.4",
"@types/tmp": "0.2.4",
"@types/vary": "1.1.1",
"@types/web-push": "3.6.1",
"@types/ws": "8.5.7",
"@types/simple-oauth2": "5.0.6",
"@types/sinonjs__fake-timers": "8.1.4",
"@types/tinycolor2": "1.4.5",
"@types/tmp": "0.2.5",
"@types/vary": "1.1.2",
"@types/web-push": "3.6.2",
"@types/ws": "8.5.8",
"@typescript-eslint/eslint-plugin": "6.8.0",
"@typescript-eslint/parser": "6.8.0",
"aws-sdk-client-mock": "3.0.0",

View file

@ -55,7 +55,6 @@ import { MetaService } from '@/core/MetaService.js';
import { SearchService } from '@/core/SearchService.js';
import { FeaturedService } from '@/core/FeaturedService.js';
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
import { nyaize } from '@/misc/nyaize.js';
import { UtilityService } from '@/core/UtilityService.js';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
import type { MiInstance } from '@/models/Instance.js';
import { MetaService } from '@/core/MetaService.js';

View file

@ -1,20 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export function nyaize(text: string): string {
return text
// ja-JP
.replaceAll('な', 'にゃ').replaceAll('ナ', 'ニャ').replaceAll('ナ', 'ニャ')
// en-US
.replace(/(?<=n)a/gi, x => x === 'A' ? 'YA' : 'ya')
.replace(/(?<=morn)ing/gi, x => x === 'ING' ? 'YAN' : 'yan')
.replace(/(?<=every)one/gi, x => x === 'ONE' ? 'NYAN' : 'nyan')
// ko-KR
.replace(/[나-낳]/g, match => String.fromCharCode(
match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0),
))
.replace(/(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm, '다냥')
.replace(/(야(?=\?))|(야$)|(야(?= ))/gm, '냥');
}

View file

@ -318,8 +318,9 @@ export class ApiCallService implements OnApplicationShutdown {
}
if (ep.meta.requireRolePolicy != null && !user!.isRoot) {
const myRoles = await this.roleService.getUserRoles(user!.id);
const policies = await this.roleService.getUserPolicies(user!.id);
if (!policies[ep.meta.requireRolePolicy]) {
if (!policies[ep.meta.requireRolePolicy] && !myRoles.some(r => r.isAdministrator)) {
throw new ApiError({
message: 'You are not assigned to a required role.',
code: 'ROLE_PERMISSION_DENIED',

View file

@ -182,15 +182,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
query.andWhere(new Brackets(qb => {
qb
.where('note.replyId IS NULL') // 返信ではない
.orWhere(new Brackets(qb => {
qb // 返信だけど投稿者自身への返信
.where('note.replyId IS NOT NULL')
.andWhere('note.replyUserId = note.userId');
}));
}));
if (!ps.withReplies) {
query.andWhere(new Brackets(qb => {
qb
.where('note.replyId IS NULL') // 返信ではない
.orWhere(new Brackets(qb => {
qb // 返信だけど投稿者自身への返信
.where('note.replyId IS NOT NULL')
.andWhere('note.replyUserId = note.userId');
}));
}));
}
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQuery(query, me);

View file

@ -163,6 +163,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
query.andWhere('note.fileIds != \'{}\'');
}
if (!ps.withReplies) {
query.andWhere(new Brackets(qb => {
qb
.where('note.replyId IS NULL') // 返信ではない
.orWhere(new Brackets(qb => {
qb // 返信だけど投稿者自身への返信
.where('note.replyId IS NOT NULL')
.andWhere('note.replyUserId = note.userId');
}));
}));
}
const timeline = await query.limit(ps.limit).getMany();
process.nextTick(() => {

View file

@ -3,12 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import type { NotesRepository, UserListsRepository, UserListMembershipsRepository, MiNote } from '@/models/_.js';
import type { NotesRepository, UserListsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { DI } from '@/di-symbols.js';
@ -67,9 +64,6 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.redisForTimelines)
private redisForTimelines: Redis.Redis,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,

View file

@ -41,11 +41,15 @@ class HomeTimelineChannel extends Channel {
@bindThis
private async onNote(note: Packed<'Note'>) {
const isMe = this.user!.id === note.userId;
if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
if (note.channelId) {
if (!this.followingChannels.has(note.channelId)) return;
} else {
// その投稿のユーザーをフォローしていなかったら弾く
if ((this.user!.id !== note.userId) && !Object.hasOwn(this.following, note.userId)) return;
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
}
// Ignore notes from instances the user has muted
@ -55,7 +59,7 @@ class HomeTimelineChannel extends Channel {
if (this.withFiles && (note.files === undefined || note.files.length === 0)) return;
if (note.visibility === 'followers') {
if (!Object.hasOwn(this.following, note.userId)) return;
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
} else if (note.visibility === 'specified') {
if (!note.visibleUserIds!.includes(this.user!.id)) return;
}
@ -64,7 +68,7 @@ class HomeTimelineChannel extends Channel {
if (note.reply && !this.following[note.userId]?.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
}
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;

View file

@ -49,12 +49,16 @@ class HybridTimelineChannel extends Channel {
@bindThis
private async onNote(note: Packed<'Note'>) {
const isMe = this.user!.id === note.userId;
if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
// チャンネルの投稿ではなく、自分自身の投稿 または
// チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または
// チャンネルの投稿ではなく、全体公開のローカルの投稿 または
// フォローしているチャンネルの投稿 の場合だけ
if (!(
(note.channelId == null && this.user!.id === note.userId) ||
(note.channelId == null && isMe) ||
(note.channelId == null && Object.hasOwn(this.following, note.userId)) ||
(note.channelId == null && (note.user.host == null && note.visibility === 'public')) ||
(note.channelId != null && this.followingChannels.has(note.channelId))
@ -64,7 +68,7 @@ class HybridTimelineChannel extends Channel {
if (this.withFiles && (note.files === undefined || note.files.length === 0)) return;
if (note.visibility === 'followers') {
if (!Object.hasOwn(this.following, note.userId)) return;
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
} else if (note.visibility === 'specified') {
if (!note.visibleUserIds!.includes(this.user!.id)) return;
}
@ -76,7 +80,7 @@ class HybridTimelineChannel extends Channel {
if (note.reply && !this.following[note.userId]?.withReplies && !this.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
}
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;

View file

@ -78,12 +78,14 @@ class UserListChannel extends Channel {
@bindThis
private async onNote(note: Packed<'Note'>) {
const isMe = this.user!.id === note.userId;
if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
if (!Object.hasOwn(this.membershipsMap, note.userId)) return;
if (note.visibility === 'followers') {
if (!Object.hasOwn(this.following, note.userId)) return;
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
} else if (note.visibility === 'specified') {
if (!note.visibleUserIds!.includes(this.user!.id)) return;
}
@ -92,7 +94,7 @@ class UserListChannel extends Channel {
if (note.reply && !this.membershipsMap[note.userId]?.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
if (reply.userId !== this.user!.id && !isMe && reply.userId !== note.userId) return;
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する

View file

@ -115,6 +115,16 @@ describe('Streaming', () => {
assert.strictEqual(fired, true);
});
test('自分の visibility: followers な投稿が流れる', async () => {
const fired = await waitFire(
ayano, 'homeTimeline', // ayano:Home
() => api('notes/create', { text: 'foo', visibility: 'followers' }, ayano), // ayano posts
msg => msg.type === 'note' && msg.body.text === 'foo',
);
assert.strictEqual(fired, true);
});
test('フォローしているユーザーの投稿が流れる', async () => {
const fired = await waitFire(
ayano, 'homeTimeline', // ayano:home
@ -125,6 +135,16 @@ describe('Streaming', () => {
assert.strictEqual(fired, true);
});
test('フォローしているユーザーの visibility: followers な投稿が流れる', async () => {
const fired = await waitFire(
ayano, 'homeTimeline', // ayano:home
() => api('notes/create', { text: 'foo', visibility: 'followers' }, kyoko), // kyoko posts
msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko
);
assert.strictEqual(fired, true);
});
test('フォローしていないユーザーの投稿は流れない', async () => {
const fired = await waitFire(
kyoko, 'homeTimeline', // kyoko:home
@ -241,6 +261,16 @@ describe('Streaming', () => {
assert.strictEqual(fired, true);
});
test('自分の visibility: followers な投稿が流れる', async () => {
const fired = await waitFire(
ayano, 'hybridTimeline',
() => api('notes/create', { text: 'foo', visibility: 'followers' }, ayano), // ayano posts
msg => msg.type === 'note' && msg.body.text === 'foo',
);
assert.strictEqual(fired, true);
});
test('フォローしていないローカルユーザーの投稿が流れる', async () => {
const fired = await waitFire(
ayano, 'hybridTimeline', // ayano:Hybrid
@ -293,6 +323,16 @@ describe('Streaming', () => {
assert.strictEqual(fired, true);
});
test('フォローしているユーザーの visibility: followers な投稿が流れる', async () => {
const fired = await waitFire(
ayano, 'hybridTimeline', // ayano:Hybrid
() => api('notes/create', { text: 'foo', visibility: 'followers' }, kyoko),
msg => msg.type === 'note' && msg.body.userId === kyoko.id, // wait kyoko
);
assert.strictEqual(fired, true);
});
test('フォローしていないローカルユーザーのホーム投稿は流れない', async () => {
const fired = await waitFire(
ayano, 'hybridTimeline', // ayano:Hybrid