merge: latest changes

This commit is contained in:
Marie 2024-02-09 19:22:06 +01:00
commit 85355813ad
No known key found for this signature in database
GPG key ID: 56569BBE47D2C828
91 changed files with 1103 additions and 494 deletions

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class prohibitedWords1707429690000 {
name = 'prohibitedWords1707429690000'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "prohibitedWords" character varying(1024) array NOT NULL DEFAULT '{}'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "prohibitedWords"`);
}
}

View file

@ -84,7 +84,7 @@
"@nestjs/testing": "10.2.10",
"@peertube/http-signature": "1.7.0",
"@transfem-org/sfm-js": "0.24.4",
"@simplewebauthn/server": "9.0.1",
"@simplewebauthn/server": "9.0.2",
"@sinonjs/fake-timers": "11.2.2",
"@smithy/node-http-handler": "2.1.10",
"@swc/cli": "0.1.63",
@ -98,12 +98,12 @@
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.2",
"bullmq": "5.1.5",
"bullmq": "5.1.9",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.1",
"cbor": "9.0.2",
"chalk": "5.3.0",
"chalk-template": "1.1.0",
"chokidar": "3.5.3",
"chokidar": "3.6.0",
"cli-highlight": "2.1.11",
"color-convert": "2.0.1",
"content-disposition": "0.5.4",
@ -205,7 +205,7 @@
"@types/jsrsasign": "10.5.12",
"@types/mime-types": "2.1.4",
"@types/ms": "0.7.34",
"@types/node": "20.11.10",
"@types/node": "20.11.17",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.14",
"@types/oauth": "0.9.4",

View file

@ -408,7 +408,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
*/
@bindThis
public checkDuplicate(name: string): Promise<boolean> {
return this.emojisRepository.exist({ where: { name, host: IsNull() } });
return this.emojisRepository.exists({ where: { name, host: IsNull() } });
}
@bindThis

View file

@ -163,7 +163,7 @@ export class HashtagService {
const instance = await this.metaService.fetch();
const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t));
if (hiddenTags.includes(hashtag)) return;
if (this.utilityService.isSensitiveWordIncluded(hashtag, instance.sensitiveWords)) return;
if (this.utilityService.isKeyWordIncluded(hashtag, instance.sensitiveWords)) return;
// YYYYMMDDHHmm (10分間隔)
const now = new Date();

View file

@ -153,6 +153,8 @@ type Option = {
export class NoteCreateService implements OnApplicationShutdown {
#shutdownController = new AbortController();
public static ContainsProhibitedWordsError = class extends Error {};
constructor(
@Inject(DI.config)
private config: Config,
@ -429,13 +431,19 @@ export class NoteCreateService implements OnApplicationShutdown {
if (data.visibility === 'public' && data.channel == null) {
const sensitiveWords = meta.sensitiveWords;
if (this.utilityService.isSensitiveWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) {
if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) {
data.visibility = 'home';
} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) {
data.visibility = 'home';
}
}
if (!user.host) {
if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', meta.prohibitedWords)) {
throw new NoteCreateService.ContainsProhibitedWordsError();
}
}
const inSilencedInstance = this.utilityService.isSilencedHost(meta.silencedHosts, user.host);
if (data.visibility === 'public' && inSilencedInstance && user.host !== null) {
@ -795,7 +803,7 @@ export class NoteCreateService implements OnApplicationShutdown {
});
// 通知
if (data.reply.userHost === null) {
const isThreadMuted = await this.noteThreadMutingsRepository.exist({
const isThreadMuted = await this.noteThreadMutingsRepository.exists({
where: {
userId: data.reply.userId,
threadId: data.reply.threadId ?? data.reply.id,
@ -830,7 +838,7 @@ export class NoteCreateService implements OnApplicationShutdown {
// Notify
if (data.renote.userHost === null) {
const isThreadMuted = await this.noteThreadMutingsRepository.exist({
const isThreadMuted = await this.noteThreadMutingsRepository.exists({
where: {
userId: data.renote.userId,
threadId: data.renote.threadId ?? data.renote.id,
@ -1057,7 +1065,7 @@ export class NoteCreateService implements OnApplicationShutdown {
@bindThis
private async createMentionedEvents(mentionedUsers: MinimumUser[], note: MiNote, nm: NotificationManager) {
for (const u of mentionedUsers.filter(u => this.userEntityService.isLocalUser(u))) {
const isThreadMuted = await this.noteThreadMutingsRepository.exist({
const isThreadMuted = await this.noteThreadMutingsRepository.exists({
where: {
userId: u.id,
threadId: note.threadId ?? note.id,

View file

@ -612,7 +612,7 @@ export class NoteEditService implements OnApplicationShutdown {
if (data.reply) {
// 通知
if (data.reply.userHost === null) {
const isThreadMuted = await this.noteThreadMutingsRepository.exist({
const isThreadMuted = await this.noteThreadMutingsRepository.exists({
where: {
userId: data.reply.userId,
threadId: data.reply.threadId ?? data.reply.id,
@ -647,7 +647,7 @@ export class NoteEditService implements OnApplicationShutdown {
// Notify
if (data.renote.userHost === null) {
const isThreadMuted = await this.noteThreadMutingsRepository.exist({
const isThreadMuted = await this.noteThreadMutingsRepository.exists({
where: {
userId: data.renote.userId,
threadId: data.renote.threadId ?? data.renote.id,
@ -751,7 +751,7 @@ export class NoteEditService implements OnApplicationShutdown {
@bindThis
private async createMentionedEvents(mentionedUsers: MinimumUser[], note: MiNote, nm: NotificationManager) {
for (const u of mentionedUsers.filter(u => this.userEntityService.isLocalUser(u))) {
const isThreadMuted = await this.noteThreadMutingsRepository.exist({
const isThreadMuted = await this.noteThreadMutingsRepository.exists({
where: {
userId: u.id,
threadId: note.threadId ?? note.id,

View file

@ -49,7 +49,7 @@ export class NoteReadService implements OnApplicationShutdown {
//#endregion
// スレッドミュート
const isThreadMuted = await this.noteThreadMutingsRepository.exist({
const isThreadMuted = await this.noteThreadMutingsRepository.exists({
where: {
userId: userId,
threadId: note.threadId ?? note.id,
@ -70,7 +70,7 @@ export class NoteReadService implements OnApplicationShutdown {
// 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する
setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => {
const exist = await this.noteUnreadsRepository.exist({ where: { id: unread.id } });
const exist = await this.noteUnreadsRepository.exists({ where: { id: unread.id } });
if (!exist) return;

View file

@ -248,7 +248,7 @@ export class ReactionService {
// リアクションされたユーザーがローカルユーザーなら通知を作成
if (note.userHost === null) {
const isThreadMuted = await this.noteThreadMutingsRepository.exist({
const isThreadMuted = await this.noteThreadMutingsRepository.exists({
where: {
userId: note.userId,
threadId: note.threadId ?? note.id,

View file

@ -77,12 +77,12 @@ export class SignupService {
const secret = generateUserToken();
// Check username duplication
if (await this.usersRepository.exist({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
throw new Error('DUPLICATED_USERNAME');
}
// Check deleted username duplication
if (await this.usedUsernamesRepository.exist({ where: { username: username.toLowerCase() } })) {
if (await this.usedUsernamesRepository.exists({ where: { username: username.toLowerCase() } })) {
throw new Error('USED_USERNAME');
}

View file

@ -144,7 +144,7 @@ export class UserFollowingService implements OnModuleInit {
let autoAccept = false;
// 鍵アカウントであっても、既にフォローされていた場合はスルー
const isFollowing = await this.followingsRepository.exist({
const isFollowing = await this.followingsRepository.exists({
where: {
followerId: follower.id,
followeeId: followee.id,
@ -156,7 +156,7 @@ export class UserFollowingService implements OnModuleInit {
// フォローしているユーザーは自動承認オプション
if (!autoAccept && (this.userEntityService.isLocalUser(followee) && followeeProfile.autoAcceptFollowed)) {
const isFollowed = await this.followingsRepository.exist({
const isFollowed = await this.followingsRepository.exists({
where: {
followerId: followee.id,
followeeId: follower.id,
@ -170,7 +170,7 @@ export class UserFollowingService implements OnModuleInit {
if (followee.isLocked && !autoAccept) {
autoAccept = !!(await this.accountMoveService.validateAlsoKnownAs(
follower,
(oldSrc, newSrc) => this.followingsRepository.exist({
(oldSrc, newSrc) => this.followingsRepository.exists({
where: {
followeeId: followee.id,
followerId: newSrc.id,
@ -233,7 +233,7 @@ export class UserFollowingService implements OnModuleInit {
this.cacheService.userFollowingsCache.refresh(follower.id);
const requestExist = await this.followRequestsRepository.exist({
const requestExist = await this.followRequestsRepository.exists({
where: {
followeeId: followee.id,
followerId: follower.id,
@ -531,7 +531,7 @@ export class UserFollowingService implements OnModuleInit {
}
}
const requestExist = await this.followRequestsRepository.exist({
const requestExist = await this.followRequestsRepository.exists({
where: {
followeeId: followee.id,
followerId: follower.id,

View file

@ -43,13 +43,13 @@ export class UtilityService {
}
@bindThis
public isSensitiveWordIncluded(text: string, sensitiveWords: string[]): boolean {
if (sensitiveWords.length === 0) return false;
public isKeyWordIncluded(text: string, keyWords: string[]): boolean {
if (keyWords.length === 0) return false;
if (text === '') return false;
const regexpregexp = /^\/(.+)\/(.*)$/;
const matched = sensitiveWords.some(filter => {
const matched = keyWords.some(filter => {
// represents RegExp
const regexp = filter.match(regexpregexp);
// This should never happen due to input sanitisation.

View file

@ -629,7 +629,7 @@ export class ApInboxService {
return 'skip: follower not found';
}
const isFollowing = await this.followingsRepository.exist({
const isFollowing = await this.followingsRepository.exists({
where: {
followerId: follower.id,
followeeId: actor.id,
@ -686,14 +686,14 @@ export class ApInboxService {
return 'skip: フォロー解除しようとしているユーザーはローカルユーザーではありません';
}
const requestExist = await this.followRequestsRepository.exist({
const requestExist = await this.followRequestsRepository.exists({
where: {
followerId: actor.id,
followeeId: followee.id,
},
});
const isFollowing = await this.followingsRepository.exist({
const isFollowing = await this.followingsRepository.exists({
where: {
followerId: actor.id,
followeeId: followee.id,

View file

@ -345,7 +345,7 @@ export class ApRendererService {
inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId });
if (inReplyToNote != null) {
const inReplyToUserExist = await this.usersRepository.exist({ where: { id: inReplyToNote.userId } });
const inReplyToUserExist = await this.usersRepository.exists({ where: { id: inReplyToNote.userId } });
if (inReplyToUserExist) {
if (inReplyToNote.uri) {
@ -636,7 +636,7 @@ export class ApRendererService {
inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId });
if (inReplyToNote != null) {
const inReplyToUserExist = await this.usersRepository.exist({ where: { id: inReplyToNote.userId } });
const inReplyToUserExist = await this.usersRepository.exists({ where: { id: inReplyToNote.userId } });
if (inReplyToUserExist) {
if (inReplyToNote.uri) {

View file

@ -51,14 +51,14 @@ export class ChannelEntityService {
const banner = channel.bannerId ? await this.driveFilesRepository.findOneBy({ id: channel.bannerId }) : null;
const isFollowing = meId ? await this.channelFollowingsRepository.exist({
const isFollowing = meId ? await this.channelFollowingsRepository.exists({
where: {
followerId: meId,
followeeId: channel.id,
},
}) : false;
const isFavorited = meId ? await this.channelFavoritesRepository.exist({
const isFavorited = meId ? await this.channelFavoritesRepository.exists({
where: {
userId: meId,
channelId: channel.id,

View file

@ -46,7 +46,7 @@ export class ClipEntityService {
description: clip.description,
isPublic: clip.isPublic,
favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }),
isFavorited: meId ? await this.clipFavoritesRepository.exist({ where: { clipId: clip.id, userId: meId } }) : undefined,
isFavorited: meId ? await this.clipFavoritesRepository.exists({ where: { clipId: clip.id, userId: meId } }) : undefined,
});
}

View file

@ -47,7 +47,7 @@ export class FlashEntityService {
summary: flash.summary,
script: flash.script,
likedCount: flash.likedCount,
isLiked: meId ? await this.flashLikesRepository.exist({ where: { flashId: flash.id, userId: meId } }) : undefined,
isLiked: meId ? await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } }) : undefined,
});
}

View file

@ -53,7 +53,7 @@ export class GalleryPostEntityService {
tags: post.tags.length > 0 ? post.tags : undefined,
isSensitive: post.isSensitive,
likedCount: post.likedCount,
isLiked: meId ? await this.galleryLikesRepository.exist({ where: { postId: post.id, userId: meId } }) : undefined,
isLiked: meId ? await this.galleryLikesRepository.exists({ where: { postId: post.id, userId: meId } }) : undefined,
});
}

View file

@ -114,7 +114,7 @@ export class NoteEntityService implements OnModuleInit {
hide = false;
} else {
if (packedNote.renote) {
const isFollowing = await this.followingsRepository.exist({
const isFollowing = await this.followingsRepository.exists({
where: {
followeeId: packedNote.renote.userId,
followerId: meId,
@ -124,7 +124,7 @@ export class NoteEntityService implements OnModuleInit {
hide = !isFollowing;
} else {
// フォロワーかどうか
const isFollowing = await this.followingsRepository.exist({
const isFollowing = await this.followingsRepository.exists({
where: {
followeeId: packedNote.userId,
followerId: meId,

View file

@ -104,7 +104,7 @@ export class PageEntityService {
eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId) : null,
attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter((x): x is MiDriveFile => x != null)),
likedCount: page.likedCount,
isLiked: meId ? await this.pageLikesRepository.exist({ where: { pageId: page.id, userId: meId } }) : undefined,
isLiked: meId ? await this.pageLikesRepository.exists({ where: { pageId: page.id, userId: meId } }) : undefined,
});
}

View file

@ -153,43 +153,43 @@ export class UserEntityService implements OnModuleInit {
followerId: me,
followeeId: target,
}),
this.followingsRepository.exist({
this.followingsRepository.exists({
where: {
followerId: target,
followeeId: me,
},
}),
this.followRequestsRepository.exist({
this.followRequestsRepository.exists({
where: {
followerId: me,
followeeId: target,
},
}),
this.followRequestsRepository.exist({
this.followRequestsRepository.exists({
where: {
followerId: target,
followeeId: me,
},
}),
this.blockingsRepository.exist({
this.blockingsRepository.exists({
where: {
blockerId: me,
blockeeId: target,
},
}),
this.blockingsRepository.exist({
this.blockingsRepository.exists({
where: {
blockerId: target,
blockeeId: me,
},
}),
this.mutingsRepository.exist({
this.mutingsRepository.exists({
where: {
muterId: me,
muteeId: target,
},
}),
this.renoteMutingsRepository.exist({
this.renoteMutingsRepository.exists({
where: {
muterId: me,
muteeId: target,
@ -216,7 +216,7 @@ export class UserEntityService implements OnModuleInit {
/*
const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId);
const isUnread = (myAntennas.length > 0 ? await this.antennaNotesRepository.exist({
const isUnread = (myAntennas.length > 0 ? await this.antennaNotesRepository.exists({
where: {
antennaId: In(myAntennas.map(x => x.id)),
read: false,

View file

@ -76,6 +76,11 @@ export class MiMeta {
})
public sensitiveWords: string[];
@Column('varchar', {
length: 1024, array: true, default: '{}',
})
public prohibitedWords: string[];
@Column('varchar', {
length: 1024, array: true, default: '{}',
})

View file

@ -176,12 +176,12 @@ export class SignupApiService {
}
if (instance.emailRequiredForSignup) {
if (await this.usersRepository.exist({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
throw new FastifyReplyError(400, 'DUPLICATED_USERNAME');
}
// Check deleted username duplication
if (await this.usedUsernamesRepository.exist({ where: { username: username.toLowerCase() } })) {
if (await this.usedUsernamesRepository.exists({ where: { username: username.toLowerCase() } })) {
throw new FastifyReplyError(400, 'USED_USERNAME');
}

View file

@ -160,6 +160,13 @@ export const meta = {
type: 'string',
},
},
prohibitedWords: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
},
},
bannedEmailDomains: {
type: 'array',
optional: true, nullable: false,
@ -549,6 +556,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
blockedHosts: instance.blockedHosts,
silencedHosts: instance.silencedHosts,
sensitiveWords: instance.sensitiveWords,
prohibitedWords: instance.prohibitedWords,
preservedUsernames: instance.preservedUsernames,
bubbleInstances: instance.bubbleInstances,
hcaptchaSecretKey: instance.hcaptchaSecretKey,

View file

@ -55,7 +55,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw e;
});
const exist = await this.promoNotesRepository.exist({ where: { noteId: note.id } });
const exist = await this.promoNotesRepository.exists({ where: { noteId: note.id } });
if (exist) {
throw new ApiError(meta.errors.alreadyPromoted);

View file

@ -41,6 +41,11 @@ export const paramDef = {
type: 'string',
},
},
prohibitedWords: {
type: 'array', nullable: true, items: {
type: 'string',
},
},
themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' },
mascotImageUrl: { type: 'string', nullable: true },
bannerUrl: { type: 'string', nullable: true },
@ -185,6 +190,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (Array.isArray(ps.sensitiveWords)) {
set.sensitiveWords = ps.sensitiveWords.filter(Boolean);
}
if (Array.isArray(ps.prohibitedWords)) {
set.prohibitedWords = ps.prohibitedWords.filter(Boolean);
}
if (Array.isArray(ps.silencedHosts)) {
let lastValue = '';
set.silencedHosts = ps.silencedHosts.sort().filter((h) => {

View file

@ -62,7 +62,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const accessToken = secureRndstr(32);
// Fetch exist access token
const exist = await this.accessTokensRepository.exist({
const exist = await this.accessTokensRepository.exists({
where: {
appId: session.appId,
userId: me.id,

View file

@ -88,7 +88,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
// Check if already blocking
const exist = await this.blockingsRepository.exist({
const exist = await this.blockingsRepository.exists({
where: {
blockerId: blocker.id,
blockeeId: blockee.id,

View file

@ -88,7 +88,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
// Check not blocking
const exist = await this.blockingsRepository.exist({
const exist = await this.blockingsRepository.exists({
where: {
blockerId: blocker.id,
blockeeId: blockee.id,

View file

@ -62,7 +62,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchClip);
}
const exist = await this.clipFavoritesRepository.exist({
const exist = await this.clipFavoritesRepository.exists({
where: {
clipId: clip.id,
userId: me.id,

View file

@ -38,7 +38,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private driveFilesRepository: DriveFilesRepository,
) {
super(meta, paramDef, async (ps, me) => {
const exist = await this.driveFilesRepository.exist({
const exist = await this.driveFilesRepository.exists({
where: {
md5: ps.md5,
userId: me.id,

View file

@ -70,7 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
// if already liked
const exist = await this.flashLikesRepository.exist({
const exist = await this.flashLikesRepository.exists({
where: {
flashId: flash.id,
userId: me.id,

View file

@ -101,7 +101,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
// Check if already following
const exist = await this.followingsRepository.exist({
const exist = await this.followingsRepository.exists({
where: {
followerId: follower.id,
followeeId: followee.id,

View file

@ -85,7 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
// Check not following
const exist = await this.followingsRepository.exist({
const exist = await this.followingsRepository.exists({
where: {
followerId: follower.id,
followeeId: followee.id,

View file

@ -72,7 +72,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
// if already liked
const exist = await this.galleryLikesRepository.exist({
const exist = await this.galleryLikesRepository.exists({
where: {
postId: post.id,
userId: me.id,

View file

@ -71,7 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private downloadService: DownloadService,
) {
super(meta, paramDef, async (ps, me) => {
const userExist = await this.usersRepository.exist({ where: { id: me.id } });
const userExist = await this.usersRepository.exists({ where: { id: me.id } });
if (!userExist) throw new ApiError(meta.errors.noSuchUser);
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
if (file === null) throw new ApiError(meta.errors.noSuchFile);

View file

@ -34,7 +34,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
) {
super(meta, paramDef, async (ps, me) => {
if (ps.tokenId) {
const tokenExist = await this.accessTokensRepository.exist({ where: { id: ps.tokenId } });
const tokenExist = await this.accessTokensRepository.exists({ where: { id: ps.tokenId } });
if (tokenExist) {
await this.accessTokensRepository.delete({
@ -43,7 +43,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
}
} else if (ps.token) {
const tokenExist = await this.accessTokensRepository.exist({ where: { token: ps.token } });
const tokenExist = await this.accessTokensRepository.exists({ where: { token: ps.token } });
if (tokenExist) {
await this.accessTokensRepository.delete({

View file

@ -83,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
// Check if already muting
const exist = await this.mutingsRepository.exist({
const exist = await this.mutingsRepository.exists({
where: {
muterId: muter.id,
muteeId: mutee.id,

View file

@ -17,6 +17,8 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteCreateService } from '@/core/NoteCreateService.js';
import { DI } from '@/di-symbols.js';
import { isPureRenote } from '@/misc/is-pure-renote.js';
import { MetaService } from '@/core/MetaService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { ApiError } from '../../error.js';
export const meta = {
@ -117,6 +119,12 @@ export const meta = {
code: 'CANNOT_RENOTE_OUTSIDE_OF_CHANNEL',
id: '33510210-8452-094c-6227-4a6c05d99f00',
},
containsProhibitedWords: {
message: 'Cannot post because it contains prohibited words.',
code: 'CONTAINS_PROHIBITED_WORDS',
id: 'aa6e01d3-a85c-669d-758a-76aab43af334',
},
},
} as const;
@ -271,7 +279,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Check blocking
if (renote.userId !== me.id) {
const blockExist = await this.blockingsRepository.exist({
const blockExist = await this.blockingsRepository.exists({
where: {
blockerId: renote.userId,
blockeeId: me.id,
@ -319,7 +327,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Check blocking
if (reply.userId !== me.id) {
const blockExist = await this.blockingsRepository.exist({
const blockExist = await this.blockingsRepository.exists({
where: {
blockerId: reply.userId,
blockeeId: me.id,
@ -351,31 +359,40 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
// 投稿を作成
const note = await this.noteCreateService.create(me, {
createdAt: new Date(),
files: files,
poll: ps.poll ? {
choices: ps.poll.choices,
multiple: ps.poll.multiple ?? false,
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
} : undefined,
text: ps.text ?? undefined,
reply,
renote,
cw: ps.cw,
localOnly: ps.localOnly,
reactionAcceptance: ps.reactionAcceptance,
visibility: ps.visibility,
visibleUsers,
channel,
apMentions: ps.noExtractMentions ? [] : undefined,
apHashtags: ps.noExtractHashtags ? [] : undefined,
apEmojis: ps.noExtractEmojis ? [] : undefined,
});
try {
const note = await this.noteCreateService.create(me, {
createdAt: new Date(),
files: files,
poll: ps.poll ? {
choices: ps.poll.choices,
multiple: ps.poll.multiple ?? false,
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
} : undefined,
text: ps.text ?? undefined,
reply,
renote,
cw: ps.cw,
localOnly: ps.localOnly,
reactionAcceptance: ps.reactionAcceptance,
visibility: ps.visibility,
visibleUsers,
channel,
apMentions: ps.noExtractMentions ? [] : undefined,
apHashtags: ps.noExtractHashtags ? [] : undefined,
apEmojis: ps.noExtractEmojis ? [] : undefined,
});
return {
createdNote: await this.noteEntityService.pack(note, me),
};
return {
createdNote: await this.noteEntityService.pack(note, me),
};
} catch (e) {
// TODO: 他のErrorもここでキャッチしてエラーメッセージを当てるようにしたい
if (e instanceof NoteCreateService.ContainsProhibitedWordsError) {
throw new ApiError(meta.errors.containsProhibitedWords);
}
throw e;
}
});
}
}

View file

@ -311,7 +311,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Check blocking
if (renote.userId !== me.id) {
const blockExist = await this.blockingsRepository.exist({
const blockExist = await this.blockingsRepository.exists({
where: {
blockerId: renote.userId,
blockeeId: me.id,
@ -349,7 +349,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Check blocking
if (reply.userId !== me.id) {
const blockExist = await this.blockingsRepository.exist({
const blockExist = await this.blockingsRepository.exists({
where: {
blockerId: reply.userId,
blockeeId: me.id,

View file

@ -67,7 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
// if already favorited
const exist = await this.noteFavoritesRepository.exist({
const exist = await this.noteFavoritesRepository.exists({
where: {
noteId: note.id,
userId: me.id,

View file

@ -70,7 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
// if already liked
const exist = await this.pageLikesRepository.exist({
const exist = await this.pageLikesRepository.exists({
where: {
pageId: page.id,
userId: me.id,

View file

@ -49,7 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw err;
});
const exist = await this.promoReadsRepository.exist({
const exist = await this.promoReadsRepository.exists({
where: {
noteId: note.id,
userId: me.id,

View file

@ -101,7 +101,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (me == null) {
throw new ApiError(meta.errors.forbidden);
} else if (me.id !== user.id) {
const isFollowing = await this.followingsRepository.exist({
const isFollowing = await this.followingsRepository.exists({
where: {
followeeId: user.id,
followerId: me.id,

View file

@ -109,7 +109,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (me == null) {
throw new ApiError(meta.errors.forbidden);
} else if (me.id !== user.id) {
const isFollowing = await this.followingsRepository.exist({
const isFollowing = await this.followingsRepository.exists({
where: {
followeeId: user.id,
followerId: me.id,

View file

@ -90,7 +90,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const listExist = await this.userListsRepository.exist({
const listExist = await this.userListsRepository.exists({
where: {
id: ps.listId,
isPublic: true,
@ -121,7 +121,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
if (currentUser.id !== me.id) {
const blockExist = await this.blockingsRepository.exist({
const blockExist = await this.blockingsRepository.exists({
where: {
blockerId: currentUser.id,
blockeeId: me.id,
@ -132,7 +132,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}
const exist = await this.userListMembershipsRepository.exist({
const exist = await this.userListMembershipsRepository.exists({
where: {
userListId: userList.id,
userId: currentUser.id,

View file

@ -47,7 +47,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const userListExist = await this.userListsRepository.exist({
const userListExist = await this.userListsRepository.exists({
where: {
id: ps.listId,
isPublic: true,
@ -58,7 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchList);
}
const exist = await this.userListFavoritesRepository.exist({
const exist = await this.userListFavoritesRepository.exists({
where: {
userId: me.id,
userListId: ps.listId,

View file

@ -104,7 +104,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Check blocking
if (user.id !== me.id) {
const blockExist = await this.blockingsRepository.exist({
const blockExist = await this.blockingsRepository.exists({
where: {
blockerId: user.id,
blockeeId: me.id,
@ -115,7 +115,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}
const exist = await this.userListMembershipsRepository.exist({
const exist = await this.userListMembershipsRepository.exists({
where: {
userListId: userList.id,
userId: user.id,

View file

@ -74,7 +74,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
userListId: ps.listId,
});
if (me !== null) {
additionalProperties.isLiked = await this.userListFavoritesRepository.exist({
additionalProperties.isLiked = await this.userListFavoritesRepository.exists({
where: {
userId: me.id,
userListId: ps.listId,

View file

@ -45,7 +45,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private userListFavoritesRepository: UserListFavoritesRepository,
) {
super(meta, paramDef, async (ps, me) => {
const userListExist = await this.userListsRepository.exist({
const userListExist = await this.userListsRepository.exists({
where: {
id: ps.listId,
isPublic: true,

View file

@ -43,7 +43,7 @@ class UserListChannel extends Channel {
this.withRenotes = params.withRenotes ?? true;
// Check existence and owner
const listExist = await this.userListsRepository.exist({
const listExist = await this.userListsRepository.exists({
where: {
id: this.listId,
userId: this.user!.id,

View file

@ -16,12 +16,14 @@ describe('Note', () => {
let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse;
let tom: misskey.entities.SignupResponse;
beforeAll(async () => {
const connection = await initTestDb(true);
Notes = connection.getRepository(MiNote);
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
tom = await signup({ username: 'tom', host: 'example.com' });
}, 1000 * 60 * 2);
test('投稿できる', async () => {
@ -607,6 +609,77 @@ describe('Note', () => {
assert.strictEqual(note2.status, 200);
assert.strictEqual(note2.body.createdNote.visibility, 'home');
});
test('禁止ワードを含む投稿はエラーになる (単語指定)', async () => {
const prohibited = await api('admin/update-meta', {
prohibitedWords: [
'test',
],
}, alice);
assert.strictEqual(prohibited.status, 204);
await new Promise(x => setTimeout(x, 2));
const note1 = await api('/notes/create', {
text: 'hogetesthuge',
}, alice);
assert.strictEqual(note1.status, 400);
assert.strictEqual(note1.body.error.code, 'CONTAINS_PROHIBITED_WORDS');
});
test('禁止ワードを含む投稿はエラーになる (正規表現)', async () => {
const prohibited = await api('admin/update-meta', {
prohibitedWords: [
'/Test/i',
],
}, alice);
assert.strictEqual(prohibited.status, 204);
const note2 = await api('/notes/create', {
text: 'hogetesthuge',
}, alice);
assert.strictEqual(note2.status, 400);
assert.strictEqual(note2.body.error.code, 'CONTAINS_PROHIBITED_WORDS');
});
test('禁止ワードを含む投稿はエラーになる (スペースアンド)', async () => {
const prohibited = await api('admin/update-meta', {
prohibitedWords: [
'Test hoge',
],
}, alice);
assert.strictEqual(prohibited.status, 204);
const note2 = await api('/notes/create', {
text: 'hogeTesthuge',
}, alice);
assert.strictEqual(note2.status, 400);
assert.strictEqual(note2.body.error.code, 'CONTAINS_PROHIBITED_WORDS');
});
test('禁止ワードを含んでいてもリモートノートはエラーにならない', async () => {
const prohibited = await api('admin/update-meta', {
prohibitedWords: [
'test',
],
}, alice);
assert.strictEqual(prohibited.status, 204);
await new Promise(x => setTimeout(x, 2));
const note1 = await api('/notes/create', {
text: 'hogetesthuge',
}, tom);
assert.strictEqual(note1.status, 200);
});
});
describe('notes/delete', () => {

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { type SharedOptions, rest } from 'msw';
import { type SharedOptions, http, HttpResponse } from 'msw';
export const onUnhandledRequest = ((req, print) => {
if (req.url.hostname !== 'localhost' || /^\/(?:client-assets\/|fluent-emojis?\/|iframe.html$|node_modules\/|src\/|sb-|static-assets\/|vite\/)/.test(req.url.pathname)) {
@ -13,19 +13,31 @@ export const onUnhandledRequest = ((req, print) => {
}) satisfies SharedOptions['onUnhandledRequest'];
export const commonHandlers = [
rest.get('/fluent-emoji/:codepoints.png', async (req, res, ctx) => {
const { codepoints } = req.params;
http.get('/fluent-emoji/:codepoints.png', async ({ params }) => {
const { codepoints } = params;
const value = await fetch(`https://raw.githubusercontent.com/misskey-dev/emojis/main/dist/${codepoints}.png`).then((response) => response.blob());
return res(ctx.set('Content-Type', 'image/png'), ctx.body(value));
return new HttpResponse(value, {
headers: {
'Content-Type': 'image/png',
},
});
}),
rest.get('/fluent-emojis/:codepoints.png', async (req, res, ctx) => {
const { codepoints } = req.params;
http.get('/fluent-emojis/:codepoints.png', async ({ params }) => {
const { codepoints } = params;
const value = await fetch(`https://raw.githubusercontent.com/misskey-dev/emojis/main/dist/${codepoints}.png`).then((response) => response.blob());
return res(ctx.set('Content-Type', 'image/png'), ctx.body(value));
return new HttpResponse(value, {
headers: {
'Content-Type': 'image/png',
},
});
}),
rest.get('/twemoji/:codepoints.svg', async (req, res, ctx) => {
const { codepoints } = req.params;
http.get('/twemoji/:codepoints.svg', async ({ params }) => {
const { codepoints } = params;
const value = await fetch(`https://unpkg.com/@discordapp/twemoji@15.0.2/dist/svg/${codepoints}.svg`).then((response) => response.blob());
return res(ctx.set('Content-Type', 'image/svg+xml'), ctx.body(value));
return new HttpResponse(value, {
headers: {
'Content-Type': 'image/svg+xml',
},
});
}),
];

View file

@ -29,8 +29,8 @@
"@phosphor-icons/web": "^2.0.3",
"@twemoji/parser": "15.0.0",
"@vitejs/plugin-vue": "5.0.3",
"@vue/compiler-sfc": "3.4.15",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.0.6",
"@vue/compiler-sfc": "3.4.18",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.2",
"astring": "1.8.6",
"broadcast-channel": "7.0.0",
"buraha": "0.0.1",
@ -72,8 +72,8 @@
"typescript": "5.3.3",
"uuid": "9.0.1",
"v-code-diff": "1.7.2",
"vite": "5.0.12",
"vue": "3.4.15",
"vite": "5.1.0",
"vue": "3.4.18",
"vuedraggable": "next"
},
"devDependencies": {
@ -97,12 +97,12 @@
"@storybook/types": "7.6.10",
"@storybook/vue3": "7.6.10",
"@storybook/vue3-vite": "7.6.10",
"@testing-library/vue": "8.0.1",
"@testing-library/vue": "8.0.2",
"@types/escape-regexp": "0.0.3",
"@types/estree": "1.0.5",
"@types/matter-js": "0.19.6",
"@types/micromatch": "4.0.6",
"@types/node": "20.11.10",
"@types/node": "20.11.17",
"@types/punycode": "2.1.3",
"@types/sanitize-html": "2.9.5",
"@types/throttle-debounce": "5.0.2",
@ -112,10 +112,10 @@
"@typescript-eslint/eslint-plugin": "6.18.1",
"@typescript-eslint/parser": "6.18.1",
"@vitest/coverage-v8": "0.34.6",
"@vue/runtime-core": "3.4.15",
"@vue/runtime-core": "3.4.18",
"acorn": "8.11.3",
"cross-env": "7.0.3",
"cypress": "13.6.3",
"cypress": "13.6.4",
"eslint": "8.56.0",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-vue": "9.20.1",
@ -123,10 +123,10 @@
"happy-dom": "10.0.3",
"intersection-observer": "0.12.2",
"micromatch": "4.0.5",
"msw": "2.1.2",
"msw-storybook-addon": "1.10.0",
"msw": "2.1.7",
"msw-storybook-addon": "2.0.0-beta.1",
"nodemon": "3.0.3",
"prettier": "3.2.4",
"prettier": "3.2.5",
"react": "18.2.0",
"react-dom": "18.2.0",
"start-server-and-test": "2.0.3",

View file

@ -60,12 +60,6 @@ export async function common(createVue: () => App<Element>) {
});
}
const splash = document.getElementById('splash');
// 念のためnullチェック(HTMLが古い場合があるため(そのうち消す))
if (splash) splash.addEventListener('transitionend', () => {
splash.remove();
});
let isClientUpdated = false;
//#region クライアントが更新されたかチェック
@ -293,5 +287,10 @@ function removeSplash() {
if (splash) {
splash.style.opacity = '0';
splash.style.pointerEvents = 'none';
// transitionendイベントが発火しない場合があるため
window.setTimeout(() => {
splash.remove();
}, 1000);
}
}

View file

@ -6,7 +6,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { action } from '@storybook/addon-actions';
import { StoryObj } from '@storybook/vue3';
import { rest } from 'msw';
import { HttpResponse, http } from 'msw';
import { abuseUserReport } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkAbuseReport from './MkAbuseReport.vue';
@ -44,9 +44,9 @@ export const Default = {
msw: {
handlers: [
...commonHandlers,
rest.post('/api/admin/resolve-abuse-user-report', async (req, res, ctx) => {
action('POST /api/admin/resolve-abuse-user-report')(await req.json());
return res(ctx.json({}));
http.post('/api/admin/resolve-abuse-user-report', async ({ request }) => {
action('POST /api/admin/resolve-abuse-user-report')(await request.json());
return HttpResponse.json({});
}),
],
},

View file

@ -6,7 +6,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { action } from '@storybook/addon-actions';
import { StoryObj } from '@storybook/vue3';
import { rest } from 'msw';
import { HttpResponse, http } from 'msw';
import { userDetailed } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkAbuseReportWindow from './MkAbuseReportWindow.vue';
@ -44,9 +44,9 @@ export const Default = {
msw: {
handlers: [
...commonHandlers,
rest.post('/api/users/report-abuse', async (req, res, ctx) => {
action('POST /api/users/report-abuse')(await req.json());
return res(ctx.json({}));
http.post('/api/users/report-abuse', async ({ request }) => {
action('POST /api/users/report-abuse')(await request.json());
return HttpResponse.json({});
}),
],
},

View file

@ -5,7 +5,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
import { rest } from 'msw';
import { HttpResponse, http } from 'msw';
import { userDetailed } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkAchievements from './MkAchievements.vue';
@ -39,8 +39,8 @@ export const Empty = {
msw: {
handlers: [
...commonHandlers,
rest.post('/api/users/achievements', (req, res, ctx) => {
return res(ctx.json([]));
http.post('/api/users/achievements', () => {
return HttpResponse.json([]);
}),
],
},
@ -52,8 +52,8 @@ export const All = {
msw: {
handlers: [
...commonHandlers,
rest.post('/api/users/achievements', (req, res, ctx) => {
return res(ctx.json(ACHIEVEMENT_TYPES.map((name) => ({ name, unlockedAt: 0 }))));
http.post('/api/users/achievements', () => {
return HttpResponse.json(ACHIEVEMENT_TYPES.map((name) => ({ name, unlockedAt: 0 })));
}),
],
},

View file

@ -8,7 +8,7 @@ import { action } from '@storybook/addon-actions';
import { expect } from '@storybook/jest';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { StoryObj } from '@storybook/vue3';
import { rest } from 'msw';
import { HttpResponse, http } from 'msw';
import { userDetailed } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkAutocomplete from './MkAutocomplete.vue';
@ -99,11 +99,11 @@ export const User = {
msw: {
handlers: [
...commonHandlers,
rest.post('/api/users/search-by-username-and-host', (req, res, ctx) => {
return res(ctx.json([
http.post('/api/users/search-by-username-and-host', () => {
return HttpResponse.json([
userDetailed('44', 'mizuki', 'misskey-hub.net', 'Mizuki'),
userDetailed('49', 'momoko', 'misskey-hub.net', 'Momoko'),
]));
]);
}),
],
},
@ -132,12 +132,12 @@ export const Hashtag = {
msw: {
handlers: [
...commonHandlers,
rest.post('/api/hashtags/search', (req, res, ctx) => {
return res(ctx.json([
http.post('/api/hashtags/search', () => {
return HttpResponse.json([
'気象警報注意報',
'気象警報',
'気象情報',
]));
]);
}),
],
},

View file

@ -5,7 +5,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
import { rest } from 'msw';
import { HttpResponse, http } from 'msw';
import { userDetailed } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkAvatars from './MkAvatars.vue';
@ -38,12 +38,12 @@ export const Default = {
msw: {
handlers: [
...commonHandlers,
rest.post('/api/users/show', (req, res, ctx) => {
return res(ctx.json([
http.post('/api/users/show', () => {
return HttpResponse.json([
userDetailed('17'),
userDetailed('20'),
userDetailed('18'),
]));
]);
}),
],
},

View file

@ -5,7 +5,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
import { rest } from 'msw';
import { HttpResponse, http } from 'msw';
import { userDetailed, inviteCode } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js';
import MkInviteCode from './MkInviteCode.vue';
@ -39,8 +39,8 @@ export const Default = {
msw: {
handlers: [
...commonHandlers,
rest.post('/api/users/show', (req, res, ctx) => {
return res(ctx.json(userDetailed(req.params.userId as string)));
http.post('/api/users/show', ({ params }) => {
return HttpResponse.json(userDetailed(params.userId as string));
}),
],
},

View file

@ -19,7 +19,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, watch, onUnmounted, provide, ref, shallowRef } from 'vue';
import * as Misskey from 'misskey-js';
import { ChannelConnection as Connection } from 'misskey-js';
import MkNotes from '@/components/MkNotes.vue';
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
import { useStream } from '@/stream.js';
@ -90,8 +89,8 @@ function prepend(note) {
}
}
let connection: Connection;
let connection2: Connection;
let connection: Misskey.ChannelConnection | null = null;
let connection2: Misskey.ChannelConnection | null = null;
let paginationQuery: Paging | null = null;
const stream = useStream();
@ -163,7 +162,7 @@ function connectChannel() {
roleId: props.role,
});
}
if (props.src !== 'directs' && props.src !== 'mentions') connection.on('note', prepend);
if (props.src !== 'directs' && props.src !== 'mentions') connection?.on('note', prepend);
}
function disconnectChannel() {

View file

@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
v-if="player.url.startsWith('http://') || player.url.startsWith('https://')"
sandbox="allow-popups allow-scripts allow-storage-access-by-user-activation allow-same-origin"
scrolling="no"
:allow="player.allow.join(';')"
:allow="player.allow == null ? 'autoplay;encrypted-media;fullscreen' : player.allow.filter(x => ['autoplay', 'clipboard-write', 'fullscreen', 'encrypted-media', 'picture-in-picture', 'web-share'].includes(x)).join(';')"
:class="$style.playerIframe"
:src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')"
:style="{ border: 0 }"

View file

@ -5,7 +5,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
import { rest } from 'msw';
import { HttpResponse, http } from 'msw';
import { commonHandlers } from '../../.storybook/mocks.js';
import { userDetailed } from '../../.storybook/fakes.js';
import MkUserSetupDialog_Follow from './MkUserSetupDialog.Follow.vue';
@ -38,17 +38,17 @@ export const Default = {
msw: {
handlers: [
...commonHandlers,
rest.post('/api/users', (req, res, ctx) => {
return res(ctx.json([
http.post('/api/users', () => {
return HttpResponse.json([
userDetailed('44'),
userDetailed('49'),
]));
]);
}),
rest.post('/api/pinned-users', (req, res, ctx) => {
return res(ctx.json([
http.post('/api/pinned-users', () => {
return HttpResponse.json([
userDetailed('44'),
userDetailed('49'),
]));
]);
}),
],
},

View file

@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import Misskey from 'misskey-js';
import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js';
import MkFolder from '@/components/MkFolder.vue';
import XUser from '@/components/MkUserSetupDialog.User.vue';

View file

@ -5,7 +5,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
import { rest } from 'msw';
import { HttpResponse, http } from 'msw';
import { commonHandlers } from '../../.storybook/mocks.js';
import { userDetailed } from '../../.storybook/fakes.js';
import MkUserSetupDialog from './MkUserSetupDialog.vue';
@ -38,17 +38,17 @@ export const Default = {
msw: {
handlers: [
...commonHandlers,
rest.post('/api/users', (req, res, ctx) => {
return res(ctx.json([
http.post('/api/users', () => {
return HttpResponse.json([
userDetailed('44'),
userDetailed('49'),
]));
]);
}),
rest.post('/api/pinned-users', (req, res, ctx) => {
return res(ctx.json([
http.post('/api/pinned-users', () => {
return HttpResponse.json([
userDetailed('44'),
userDetailed('49'),
]));
]);
}),
],
},

View file

@ -7,7 +7,7 @@
import { expect } from '@storybook/jest';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { StoryObj } from '@storybook/vue3';
import { rest } from 'msw';
import { HttpResponse, http } from 'msw';
import { commonHandlers } from '../../../.storybook/mocks.js';
import MkUrl from './MkUrl.vue';
export const Default = {
@ -59,8 +59,8 @@ export const Default = {
msw: {
handlers: [
...commonHandlers,
rest.get('/url', (req, res, ctx) => {
return res(ctx.json({
http.get('/url', () => {
return HttpResponse.json({
title: 'Misskey Hub',
icon: 'https://misskey-hub.net/favicon.ico',
description: 'Misskeyはオープンソースの分散型ソーシャルネットワーキングプラットフォームです。',
@ -74,7 +74,7 @@ export const Default = {
sitename: 'misskey-hub.net',
sensitive: false,
url: 'https://misskey-hub.net/',
}));
});
}),
],
},

View file

@ -49,6 +49,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
</MkTextarea>
<MkTextarea v-model="prohibitedWords">
<template #label>{{ i18n.ts.prohibitedWords }}</template>
<template #caption>{{ i18n.ts.prohibitedWordsDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template>
</MkTextarea>
<MkTextarea v-model="hiddenTags">
<template #label>{{ i18n.ts.hiddenTags }}</template>
<template #caption>{{ i18n.ts.hiddenTagsDescription }}</template>
@ -87,6 +92,7 @@ const emailRequiredForSignup = ref<boolean>(false);
const approvalRequiredForSignup = ref<boolean>(false);
const bubbleTimelineEnabled = ref<boolean>(false);
const sensitiveWords = ref<string>('');
const prohibitedWords = ref<string>('');
const hiddenTags = ref<string>('');
const preservedUsernames = ref<string>('');
const bubbleTimeline = ref<string>('');
@ -99,6 +105,7 @@ async function init() {
emailRequiredForSignup.value = meta.emailRequiredForSignup;
approvalRequiredForSignup.value = meta.approvalRequiredForSignup;
sensitiveWords.value = meta.sensitiveWords.join('\n');
prohibitedWords.value = meta.prohibitedWords.join('\n');
hiddenTags.value = meta.hiddenTags.join('\n');
preservedUsernames.value = meta.preservedUsernames.join('\n');
tosUrl.value = meta.tosUrl;
@ -115,6 +122,7 @@ function save() {
tosUrl: tosUrl.value,
privacyPolicyUrl: privacyPolicyUrl.value,
sensitiveWords: sensitiveWords.value.split('\n'),
prohibitedWords: prohibitedWords.value.split('\n'),
hiddenTags: hiddenTags.value.split('\n'),
preservedUsernames: preservedUsernames.value.split('\n'),
bubbleInstances: bubbleTimeline.value.split('\n'),

View file

@ -5,7 +5,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
import { rest } from 'msw';
import { HttpResponse, http } from 'msw';
import { userDetailed } from '../../../.storybook/fakes.js';
import { commonHandlers } from '../../../.storybook/mocks.js';
import home_ from './home.vue';
@ -39,12 +39,13 @@ export const Default = {
msw: {
handlers: [
...commonHandlers,
rest.post('/api/users/notes', (req, res, ctx) => {
return res(ctx.json([]));
http.post('/api/users/notes', () => {
return HttpResponse.json([]);
}),
rest.get('/api/charts/user/notes', (req, res, ctx) => {
const length = Math.max(Math.min(parseInt(req.url.searchParams.get('limit') ?? '30', 10), 1), 300);
return res(ctx.json({
http.get('/api/charts/user/notes', ({ request }) => {
const url = new URL(request.url);
const length = Math.max(Math.min(parseInt(url.searchParams.get('limit') ?? '30', 10), 1), 300);
return HttpResponse.json({
total: Array.from({ length }, () => 0),
inc: Array.from({ length }, () => 0),
dec: Array.from({ length }, () => 0),
@ -54,11 +55,12 @@ export const Default = {
renote: Array.from({ length }, () => 0),
withFile: Array.from({ length }, () => 0),
},
}));
});
}),
rest.get('/api/charts/user/pv', (req, res, ctx) => {
const length = Math.max(Math.min(parseInt(req.url.searchParams.get('limit') ?? '30', 10), 1), 300);
return res(ctx.json({
http.get('/api/charts/user/pv', ({ request }) => {
const url = new URL(request.url);
const length = Math.max(Math.min(parseInt(url.searchParams.get('limit') ?? '30', 10), 1), 300);
return HttpResponse.json({
upv: {
user: Array.from({ length }, () => 0),
visitor: Array.from({ length }, () => 0),
@ -67,7 +69,7 @@ export const Default = {
user: Array.from({ length }, () => 0),
visitor: Array.from({ length }, () => 0),
},
}));
});
}),
],
},

View file

@ -20,7 +20,7 @@ export async function getTheme(mode: 'light' | 'dark', getName = false): Promise
const base = [lightTheme, darkTheme].find(x => x.id === theme.base);
if (base && base.codeHighlighter) theme.codeHighlighter = Object.assign({}, base.codeHighlighter, theme.codeHighlighter);
}
if (theme.codeHighlighter) {
let _res: ThemeRegistration = {};
if (theme.codeHighlighter.base === '_none_') {
@ -55,7 +55,7 @@ export async function getHighlighter(): Promise<Highlighter> {
export async function initHighlighter() {
const aiScriptGrammar = await import('aiscript-vscode/aiscript/syntaxes/aiscript.tmLanguage.json');
await loadWasm(import('shiki/onig.wasm?init'));
// テーマの重複を消す
@ -68,10 +68,7 @@ export async function initHighlighter() {
themes,
langs: [
import('shiki/langs/javascript.mjs'),
{
aliases: ['is', 'ais'],
...aiScriptGrammar.default,
} as unknown as LanguageRegistration,
aiScriptGrammar.default as unknown as LanguageRegistration,
],
});

View file

@ -116,6 +116,34 @@ describe('MkUrlPreview', () => {
assert.strictEqual(iframe?.allow, 'fullscreen;web-share');
});
test('A Summaly proxy response without allow falls back to the default', async () => {
const iframe = await renderAndOpenPreview({
url: 'https://example.local',
player: {
url: 'https://example.local/player',
width: null,
height: null,
allow: undefined as any,
},
});
assert.exists(iframe, 'iframe should exist');
assert.strictEqual(iframe?.allow, 'autoplay;encrypted-media;fullscreen');
});
test('Filtering the allow list from the Summaly proxy', async () => {
const iframe = await renderAndOpenPreview({
url: 'https://example.local',
player: {
url: 'https://example.local/player',
width: null,
height: null,
allow: ['autoplay', 'camera', 'fullscreen'],
},
});
assert.exists(iframe, 'iframe should exist');
assert.strictEqual(iframe?.allow, 'autoplay;fullscreen');
});
test('Having a player width should keep the fixed aspect ratio', async () => {
const iframe = await renderAndOpenPreview({
url: 'https://example.local',

View file

@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2024.2.0-beta.10",
"version": "2024.2.0-beta.11",
"description": "Misskey SDK for JavaScript",
"types": "./built/dts/index.d.ts",
"exports": {
@ -39,7 +39,7 @@
"@misskey-dev/eslint-plugin": "1.0.0",
"@swc/jest": "0.2.31",
"@types/jest": "29.5.11",
"@types/node": "20.11.10",
"@types/node": "20.11.17",
"@typescript-eslint/eslint-plugin": "6.18.1",
"@typescript-eslint/parser": "6.18.1",
"eslint": "8.56.0",

View file

@ -4793,6 +4793,7 @@ export type operations = {
hiddenTags: string[];
blockedHosts: string[];
sensitiveWords: string[];
prohibitedWords: string[];
bannedEmailDomains?: string[];
preservedUsernames: string[];
bubbleInstances: string[];
@ -8814,6 +8815,7 @@ export type operations = {
hiddenTags?: string[] | null;
blockedHosts?: string[] | null;
sensitiveWords?: string[] | null;
prohibitedWords?: string[] | null;
themeColor?: string | null;
mascotImageUrl?: string | null;
bannerUrl?: string | null;