Compare commits

..

No commits in common. "4c77f3e597a05bdb9bfaca45b98f21e771e1a6e3" and "e786ff4bf1b8095a0a3c6f9b0307a95d890e81da" have entirely different histories.

40 changed files with 199 additions and 450 deletions

View File

@ -52,22 +52,18 @@
- Enhance: シンタックスハイライトのエンジンをJavaScriptベースのものに変更
- フロントエンドの読み込みサイズを軽量化しました
- ほとんどの言語のハイライトは問題なく行えますが、互換性の問題により一部の言語が正常にハイライトできなくなる可能性があります。詳しくは https://shiki.style/references/engine-js-compat をご覧ください。
- Fix: チャットに動画ファイルを送付すると、動画の表示が崩れてしまい視聴出来ない問題を修正
- Fix: "時計"ウィジェット(Clock)において、Transparent設定が有効でも、その背景が透過されない問題を修正
- Fix: 一定時間操作がなかったら動画プレイヤーのコントロールを隠すように
- Fix: Twitchのクリップがプレイヤーで再生できない問題を修正
### Server
- Enhance: リストやフォローをエクスポートする際にリプライを含むかどうかの情報を含むように
- Enhance: チャットルームの最大メンバー数を30人から50人に調整
- Enhance: ノートのレスポンスにアンケートが添付されているかどうかを示すフラグ`hasPoll`を追加
- Enhance: チャットルームのレスポンスに招待されているかどうかを示すフラグ`invitationExists`を追加
- Enhance: レートリミットの計算方法を調整 (#13997)
- Enhance: 外部サイトのOGPのキャッシュ期間を調整
- Fix: チャットルームが削除された場合・チャットルームから抜けた場合に、未読状態が残り続けることがあるのを修正
- Fix: ユーザ除外アンテナをインポートできない問題を修正
- Fix: アンテナのセンシティブなチャンネルのノートを含むかどうかの情報がエクスポートされない問題を修正
- Fix: ミュート対象ユーザーが引用されているートがRNされたときにミュートを貫通してしまう問題を修正 #16009
- Fix: 連合モードが「なし」の場合に、生成されるHTML内のactivity jsonへのリンクタグを省略するように
- Fix: コントロールパネルから招待コードを作成すると作成者の情報が記録されない問題を修正

6
locales/index.d.ts vendored
View File

@ -1326,10 +1326,6 @@ export interface Locale extends ILocale {
*
*/
"syncDeviceDarkMode": string;
/**
* {x}
*/
"switchDarkModeManuallyWhenSyncEnabledConfirm": ParameterizedString<"x">;
/**
*
*/
@ -9711,7 +9707,7 @@ export interface Locale extends ILocale {
*/
"excludeInactiveUsers": string;
/**
* TLに含むかの情報がファイルにない場合にTLに含むようにする
* TLに含むようにする
*/
"withReplies": string;
};

View File

@ -327,7 +327,6 @@ dark: "ダーク"
lightThemes: "明るいテーマ"
darkThemes: "暗いテーマ"
syncDeviceDarkMode: "デバイスのダークモードと同期する"
switchDarkModeManuallyWhenSyncEnabledConfirm: "「{x}」がオンになっています。同期をオフにして手動でモードを切り替えますか?"
drive: "ドライブ"
fileName: "ファイル名"
selectFile: "ファイルを選択"
@ -2557,7 +2556,7 @@ _exportOrImport:
userLists: "リスト"
excludeMutingUsers: "ミュートしているユーザーを除外"
excludeInactiveUsers: "使われていないアカウントを除外"
withReplies: "返信をTLに含むかの情報がファイルにない場合に、インポートした人による返信をTLに含むようにする"
withReplies: "インポートした人による返信をTLに含むようにする"
_charts:
federation: "連合"

View File

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

View File

@ -120,8 +120,6 @@ export class FanoutTimelineEndpointService {
filter = (note) => {
if (isUserRelated(note, userIdsWhoBlockingMe, ps.ignoreAuthorFromBlock)) return false;
if (isUserRelated(note, userIdsWhoMeMuting, ps.ignoreAuthorFromMute)) return false;
if (isUserRelated(note.renote, userIdsWhoBlockingMe, ps.ignoreAuthorFromBlock)) return false;
if (isUserRelated(note.renote, userIdsWhoMeMuting, ps.ignoreAuthorFromMute)) return false;
if (!ps.ignoreAuthorFromMute && isRenote(note) && !isQuote(note) && userIdsWhoMeMutingRenotes.has(note.userId)) return false;
if (isInstanceMuted(note, userMutedInstances)) return false;

View File

@ -77,51 +77,9 @@ export class QueryService {
return q;
}
/**
* 使
*
*
*
* Notes for future maintainers:
* 1) FanoutTimelineEndpointService
* FanoutTimelineEndpointService
* 2) queryService
*
* - packages/backend/src/server/api/endpoints/clips/notes.ts
*/
@bindThis
public generateBaseNoteFilteringQuery(
query: SelectQueryBuilder<any>,
me: { id: MiUser['id'] } | null,
{
excludeUserFromMute,
excludeAuthor,
}: {
excludeUserFromMute?: MiUser['id'],
excludeAuthor?: boolean,
} = {},
): void {
this.generateBlockedHostQueryForNote(query, excludeAuthor);
this.generateSuspendedUserQueryForNote(query, excludeAuthor);
if (me) {
this.generateMutedUserQueryForNotes(query, me, { excludeUserFromMute });
this.generateBlockedUserQueryForNotes(query, me);
this.generateMutedUserQueryForNotes(query, me, { noteColumn: 'renote', excludeUserFromMute });
this.generateBlockedUserQueryForNotes(query, me, { noteColumn: 'renote' });
}
}
// ここでいうBlockedは被Blockedの意
@bindThis
public generateBlockedUserQueryForNotes(
q: SelectQueryBuilder<any>,
me: { id: MiUser['id'] },
{
noteColumn = 'note',
}: {
noteColumn?: string,
} = {},
): void {
public generateBlockedUserQueryForNotes(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }): void {
const blockingQuery = this.blockingsRepository.createQueryBuilder('blocking')
.select('blocking.blockerId')
.where('blocking.blockeeId = :blockeeId', { blockeeId: me.id });
@ -130,20 +88,16 @@ export class QueryService {
// 投稿の返信先の作者にブロックされていない かつ
// 投稿の引用元の作者にブロックされていない
q
.andWhere(`note.userId NOT IN (${ blockingQuery.getQuery() })`)
.andWhere(new Brackets(qb => {
qb
.where(`${noteColumn}.userId IS NULL`)
.orWhere(`${noteColumn}.userId NOT IN (${ blockingQuery.getQuery() })`);
.where('note.replyUserId IS NULL')
.orWhere(`note.replyUserId NOT IN (${ blockingQuery.getQuery() })`);
}))
.andWhere(new Brackets(qb => {
qb
.where(`${noteColumn}.replyUserId IS NULL`)
.orWhere(`${noteColumn}.replyUserId NOT IN (${ blockingQuery.getQuery() })`);
}))
.andWhere(new Brackets(qb => {
qb
.where(`${noteColumn}.renoteUserId IS NULL`)
.orWhere(`${noteColumn}.renoteUserId NOT IN (${ blockingQuery.getQuery() })`);
.where('note.renoteUserId IS NULL')
.orWhere(`note.renoteUserId NOT IN (${ blockingQuery.getQuery() })`);
}));
q.setParameters(blockingQuery.getParameters());
@ -183,23 +137,13 @@ export class QueryService {
}
@bindThis
public generateMutedUserQueryForNotes(
q: SelectQueryBuilder<any>,
me: { id: MiUser['id'] },
{
excludeUserFromMute,
noteColumn = 'note',
}: {
excludeUserFromMute?: MiUser['id'],
noteColumn?: string,
} = {},
): void {
public generateMutedUserQueryForNotes(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }, exclude?: { id: MiUser['id'] }): void {
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
.select('muting.muteeId')
.where('muting.muterId = :muterId', { muterId: me.id });
if (excludeUserFromMute) {
mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: excludeUserFromMute });
if (exclude) {
mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: exclude.id });
}
const mutingInstanceQuery = this.userProfilesRepository.createQueryBuilder('user_profile')
@ -210,36 +154,32 @@ export class QueryService {
// 投稿の返信先の作者をミュートしていない かつ
// 投稿の引用元の作者をミュートしていない
q
.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`)
.andWhere(new Brackets(qb => {
qb
.where(`${noteColumn}.userId IS NULL`)
.orWhere(`${noteColumn}.userId NOT IN (${ mutingQuery.getQuery() })`);
.where('note.replyUserId IS NULL')
.orWhere(`note.replyUserId NOT IN (${ mutingQuery.getQuery() })`);
}))
.andWhere(new Brackets(qb => {
qb
.where(`${noteColumn}.replyUserId IS NULL`)
.orWhere(`${noteColumn}.replyUserId NOT IN (${ mutingQuery.getQuery() })`);
}))
.andWhere(new Brackets(qb => {
qb
.where(`${noteColumn}.renoteUserId IS NULL`)
.orWhere(`${noteColumn}.renoteUserId NOT IN (${ mutingQuery.getQuery() })`);
.where('note.renoteUserId IS NULL')
.orWhere(`note.renoteUserId NOT IN (${ mutingQuery.getQuery() })`);
}))
// mute instances
.andWhere(new Brackets(qb => {
qb
.andWhere(`${noteColumn}.userHost IS NULL`)
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? ${noteColumn}.userHost)`);
.andWhere('note.userHost IS NULL')
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.userHost)`);
}))
.andWhere(new Brackets(qb => {
qb
.where(`${noteColumn}.replyUserHost IS NULL`)
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? ${noteColumn}.replyUserHost)`);
.where('note.replyUserHost IS NULL')
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.replyUserHost)`);
}))
.andWhere(new Brackets(qb => {
qb
.where(`${noteColumn}.renoteUserHost IS NULL`)
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? ${noteColumn}.renoteUserHost)`);
.where('note.renoteUserHost IS NULL')
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`);
}));
q.setParameters(mutingQuery.getParameters());

View File

@ -83,7 +83,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
canUseTranslator: true,
canHideAds: false,
driveCapacityMb: 100,
maxFileSizeMb: 30,
maxFileSizeMb: 10,
alwaysMarkNsfw: false,
canUpdateBioMedia: true,
pinLimit: 5,

View File

@ -234,7 +234,10 @@ export class SearchService {
}
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBaseNoteFilteringQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSuspendedUserQueryForNote(query);
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
return query.limit(pagination.limit).getMany();
}

View File

@ -91,7 +91,7 @@ export class UserListService implements OnApplicationShutdown, OnModuleInit {
}
@bindThis
public async addMember(target: MiUser, list: MiUserList, me: MiUser, options: { withReplies?: boolean } = {}) {
public async addMember(target: MiUser, list: MiUserList, me: MiUser) {
const currentCount = await this.userListMembershipsRepository.countBy({
userListId: list.id,
});
@ -104,7 +104,6 @@ export class UserListService implements OnApplicationShutdown, OnModuleInit {
userId: target.id,
userListId: list.id,
userListUserId: list.userId,
withReplies: options.withReplies ?? false,
} as MiUserListMembership);
this.globalEventService.publishInternalEvent('userListMemberAdded', { userListId: list.id, memberId: target.id });

View File

@ -3,17 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { MiUser } from '@/models/_.js';
interface NoteLike {
userId: MiUser['id'];
reply?: NoteLike | null;
renote?: NoteLike | null;
replyUserId?: MiUser['id'] | null;
renoteUserId?: MiUser['id'] | null;
}
export function isUserRelated(note: NoteLike | null | undefined, userIds: Set<string>, ignoreAuthor = false): boolean {
export function isUserRelated(note: any, userIds: Set<string>, ignoreAuthor = false): boolean {
if (!note) {
return false;
}
@ -22,16 +12,13 @@ export function isUserRelated(note: NoteLike | null | undefined, userIds: Set<st
return true;
}
const replyUserId = note.replyUserId ?? note.reply?.userId;
if (replyUserId != null && replyUserId !== note.userId && userIds.has(replyUserId)) {
if (note.reply != null && note.reply.userId !== note.userId && userIds.has(note.reply.userId)) {
return true;
}
const renoteUserId = note.renoteUserId ?? note.renote?.userId;
if (renoteUserId != null && renoteUserId !== note.userId && userIds.has(renoteUserId)) {
if (note.renote != null && note.renote.userId !== note.userId && userIds.has(note.renote.userId)) {
return true;
}
return false;
}

View File

@ -94,8 +94,7 @@ export class ExportFollowingProcessorService {
continue;
}
const userAcct = this.utilityService.getFullApAccount(u.username, u.host);
const content = `${userAcct},withReplies=${following.withReplies}`;
const content = this.utilityService.getFullApAccount(u.username, u.host);
await new Promise<void>((res, rej) => {
stream.write(content + '\n', err => {
if (err) {

View File

@ -67,12 +67,10 @@ export class ExportUserListsProcessorService {
const users = await this.usersRepository.findBy({
id: In(memberships.map(j => j.userId)),
});
const usersWithReplies = new Set(memberships.filter(m => m.withReplies).map(m => m.userId));
for (const u of users) {
const acct = this.utilityService.getFullApAccount(u.username, u.host);
// 3rd column and later will be key=value pairs
const content = `${list.name},${acct},withReplies=${usersWithReplies.has(u.id)}`;
const content = `${list.name},${acct}`;
await new Promise<void>((res, rej) => {
stream.write(content + '\n', err => {
if (err) {

View File

@ -67,19 +67,8 @@ export class ImportFollowingProcessorService {
const user = job.data.user;
try {
const parts = line.split(',');
const acct = parts[0].trim();
const acct = line.split(',')[0].trim();
const { username, host } = Acct.parse(acct);
let withReplies: boolean | null = null;
for (const keyValue of parts.slice(2)) {
const [key, value] = keyValue.split('=');
switch (key) {
case 'withReplies':
withReplies = value === 'true';
break;
}
}
if (!host) return;
@ -106,7 +95,7 @@ export class ImportFollowingProcessorService {
this.logger.info(`Follow ${target.id} ${job.data.withReplies ? 'with replies' : 'without replies'} ...`);
await this.queueService.createFollowJob([{ from: user, to: { id: target.id }, silent: true, withReplies: withReplies ?? job.data.withReplies }]);
this.queueService.createFollowJob([{ from: user, to: { id: target.id }, silent: true, withReplies: job.data.withReplies }]);
} catch (e) {
this.logger.warn(`Error: ${e}`);
}

View File

@ -70,19 +70,8 @@ export class ImportUserListsProcessorService {
linenum++;
try {
const parts = line.split(',');
const listName = parts[0].trim();
const { username, host } = Acct.parse(parts[1].trim());
let withReplies = false;
for (const keyValue of parts.slice(2)) {
const [key, value] = keyValue.split('=');
switch (key) {
case 'withReplies':
withReplies = value === 'true';
break;
}
}
const listName = line.split(',')[0].trim();
const { username, host } = Acct.parse(line.split(',')[1].trim());
let list = await this.userListsRepository.findOneBy({
userId: user.id,
@ -111,9 +100,7 @@ export class ImportUserListsProcessorService {
if (await this.userListMembershipsRepository.findOneBy({ userListId: list!.id, userId: target.id }) != null) continue;
await this.userListService.addMember(target, list, user, {
withReplies: withReplies,
});
this.userListService.addMember(target, list!, user);
} catch (e) {
this.logger.warn(`Error in line:${linenum} ${e}`);
}

View File

@ -111,8 +111,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// NOTE: センシティブ除外の設定はこのエンドポイントでは無視する。
// https://github.com/misskey-dev/misskey/pull/15346#discussion_r1929950255
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSuspendedUserQueryForNote(query);
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBaseNoteFilteringQuery(query, me);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
const notes = await query.getMany();
if (sinceId != null && untilId == null) {

View File

@ -121,7 +121,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('note.channel', 'channel');
this.queryService.generateBaseNoteFilteringQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSuspendedUserQueryForNote(query);
if (me) {
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
}
//#endregion
return await query.limit(ps.limit).getMany();

View File

@ -91,8 +91,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (me) {
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateMutedUserQueryForNotes(query, me, { noteColumn: 'renote' });
this.queryService.generateBlockedUserQueryForNotes(query, me, { noteColumn: 'renote' });
}
const notes = await query

View File

@ -70,7 +70,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBaseNoteFilteringQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSuspendedUserQueryForNote(query);
if (me) {
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
}
const notes = await query.limit(ps.limit).getMany();

View File

@ -78,8 +78,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateBaseNoteFilteringQuery(query, me);
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (me) {
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
}
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');

View File

@ -243,7 +243,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBaseNoteFilteringQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSuspendedUserQueryForNote(query);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.includeMyRenotes === false) {

View File

@ -156,7 +156,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBaseNoteFilteringQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSuspendedUserQueryForNote(query);
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.withFiles) {

View File

@ -72,8 +72,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBaseNoteFilteringQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSuspendedUserQueryForNote(query);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateMutedNoteThreadQuery(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
if (ps.visibility) {
query.andWhere('note.visibility = :visibility', { visibility: ps.visibility });

View File

@ -72,7 +72,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBaseNoteFilteringQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSuspendedUserQueryForNote(query);
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
const renotes = await query.limit(ps.limit).getMany();

View File

@ -56,7 +56,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBaseNoteFilteringQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSuspendedUserQueryForNote(query);
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
const timeline = await query.limit(ps.limit).getMany();

View File

@ -96,7 +96,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBaseNoteFilteringQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSuspendedUserQueryForNote(query);
if (me) this.queryService.generateMutedUserQueryForNotes(query, me);
if (me) this.queryService.generateBlockedUserQueryForNotes(query, me);
try {
if ('tag' in ps) {

View File

@ -199,7 +199,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}));
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBaseNoteFilteringQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSuspendedUserQueryForNote(query);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.includeMyRenotes === false) {

View File

@ -184,7 +184,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}));
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBaseNoteFilteringQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSuspendedUserQueryForNote(query);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.includeMyRenotes === false) {

View File

@ -102,7 +102,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBaseNoteFilteringQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
this.queryService.generateSuspendedUserQueryForNote(query);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
const notes = await query.getMany();
notes.sort((a, b) => a.id > b.id ? -1 : 1);

View File

@ -186,10 +186,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBaseNoteFilteringQuery(query, me, {
excludeAuthor: true,
excludeUserFromMute: ps.userId,
});
this.queryService.generateBlockedHostQueryForNote(query, true);
this.queryService.generateSuspendedUserQueryForNote(query, true);
if (me) {
this.queryService.generateMutedUserQueryForNotes(query, me, { id: ps.userId });
this.queryService.generateBlockedUserQueryForNotes(query, me);
}
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');

View File

@ -64,7 +64,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.queryService.generateMutedUserQueryForUsers(query, me);
this.queryService.generateBlockQueryForUsers(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me, { noteColumn: 'renote' });
const followingQuery = this.followingsRepository.createQueryBuilder('following')
.select('following.followeeId')

View File

@ -94,8 +94,8 @@ export class UrlPreviewService {
summary.icon = this.wrap(summary.icon);
summary.thumbnail = this.wrap(summary.thumbnail);
// Cache 1day
reply.header('Cache-Control', 'max-age=86400, immutable');
// Cache 7days
reply.header('Cache-Control', 'max-age=604800, immutable');
return summary;
} catch (err) {

View File

@ -9,9 +9,9 @@ import * as assert from 'assert';
// node-fetch only supports it's own Blob yet
// https://github.com/node-fetch/node-fetch/pull/1664
import { Blob } from 'node-fetch';
import { MiUser } from '@/models/_.js';
import { api, castAsError, initTestDb, post, signup, simpleGet, uploadFile } from '../utils.js';
import type * as misskey from 'misskey-js';
import { MiUser } from '@/models/_.js';
describe('Endpoints', () => {
let alice: misskey.entities.SignupResponse;
@ -572,10 +572,19 @@ describe('Endpoints', () => {
describe('drive', () => {
test('ドライブ情報を取得できる', async () => {
await uploadFile(alice, {
blob: new Blob([new Uint8Array(256)]),
});
await uploadFile(alice, {
blob: new Blob([new Uint8Array(512)]),
});
await uploadFile(alice, {
blob: new Blob([new Uint8Array(1024)]),
});
const res = await api('drive', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
expect(res.body).toHaveProperty('usage', 0);
expect(res.body).toHaveProperty('usage', 1792);
});
});

View File

@ -345,44 +345,6 @@ describe('Timelines', () => {
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
});
test.concurrent('ミュートしているユーザーのノートの、関係のないユーザによる引用ノートの、フォローしているユーザーによるリノートが含まれない', async () => {
const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]);
await api('following/create', { userId: bob.id }, alice);
await api('mute/create', { userId: carol.id }, alice);
await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' });
const daveNote = await post(dave, { text: 'quote hi', renoteId: carolNote.id });
const bobNote = await post(bob, { renoteId: daveNote.id });
await waitForPushToTl();
const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
assert.strictEqual(res.body.some(note => note.id === daveNote.id), false);
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
});
test.concurrent('ミュートしているユーザーのノートの、関係のないユーザによるリプライの、フォローしているユーザーによるリノートが含まれない', async () => {
const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]);
await api('following/create', { userId: bob.id }, alice);
await api('mute/create', { userId: carol.id }, alice);
await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' });
const daveNote = await post(dave, { text: 'quote hi', replyId: carolNote.id });
const bobNote = await post(bob, { renoteId: daveNote.id });
await waitForPushToTl();
const res = await api('notes/timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
assert.strictEqual(res.body.some(note => note.id === daveNote.id), false);
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
});
test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
@ -725,42 +687,6 @@ describe('Timelines', () => {
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
});
test.concurrent('ミュートしているユーザーのノートの、関係のないユーザによる引用ノートの、リノートが含まれない', async () => {
const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]);
await api('mute/create', { userId: carol.id }, alice);
await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' });
const daveNote = await post(dave, { text: 'quote hi', renoteId: carolNote.id });
const bobNote = await post(bob, { renoteId: daveNote.id });
await waitForPushToTl();
const res = await api('notes/local-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
assert.strictEqual(res.body.some(note => note.id === daveNote.id), false);
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
});
test.concurrent('ミュートしているユーザーのノートの、関係のないユーザによるリプライの、リノートが含まれない', async () => {
const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]);
await api('mute/create', { userId: carol.id }, alice);
await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' });
const daveNote = await post(dave, { text: 'quote hi', replyId: carolNote.id });
const bobNote = await post(bob, { renoteId: daveNote.id });
await waitForPushToTl();
const res = await api('notes/local-timeline', { limit: 100 }, alice);
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
assert.strictEqual(res.body.some(note => note.id === daveNote.id), false);
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
});
test.concurrent('withReplies: false でフォローしているユーザーからの自分への返信が含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
@ -1457,39 +1383,6 @@ describe('Timelines', () => {
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
});
test.concurrent('ミュートしているユーザーのノートの、関係のないユーザによる引用ノートの、リノートが含まれない', async () => {
const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]);
await api('mute/create', { userId: carol.id }, alice);
await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' });
const daveNote = await post(dave, { text: 'quote hi', renoteId: carolNote.id });
const bobNote = await post(bob, { renoteId: daveNote.id });
await waitForPushToTl();
const res = await api('users/notes', { userId: bob.id, limit: 100 }, alice);
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
});
test.concurrent('ミュートしているユーザーのノートの、関係のないユーザによるリプライの、リノートが含まれない', async () => {
const [alice, bob, carol, dave] = await Promise.all([signup(), signup(), signup(), signup()]);
await api('following/create', { userId: bob.id }, alice);
await api('mute/create', { userId: carol.id }, alice);
await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' });
const daveNote = await post(dave, { text: 'quote hi', replyId: carolNote.id });
const bobNote = await post(bob, { renoteId: daveNote.id });
await waitForPushToTl();
const res = await api('users/notes', { userId: bob.id, limit: 100 }, alice);
assert.strictEqual(res.body.some(note => note.id === bobNote.id), false);
});
test.concurrent('ミュートしていても userId に指定したユーザーの投稿が含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
@ -1498,8 +1391,6 @@ describe('Timelines', () => {
const bobNote1 = await post(bob, { text: 'hi' });
const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id });
const bobNote3 = await post(bob, { text: 'hi', renoteId: bobNote1.id });
const bobNote4 = await post(bob, { renoteId: bobNote2.id });
const bobNote5 = await post(bob, { renoteId: bobNote3.id });
await waitForPushToTl();
@ -1508,8 +1399,6 @@ describe('Timelines', () => {
assert.strictEqual(res.body.some(note => note.id === bobNote1.id), true);
assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true);
assert.strictEqual(res.body.some(note => note.id === bobNote3.id), true);
assert.strictEqual(res.body.some(note => note.id === bobNote4.id), true);
assert.strictEqual(res.body.some(note => note.id === bobNote5.id), true);
});
test.concurrent('自身の visibility: specified なノートが含まれる', async () => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

View File

@ -6,15 +6,9 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import { Test } from '@nestjs/testing';
import { jest } from '@jest/globals';
import { MockResolver } from '../misc/mock-resolver.js';
import type { IActor, IApDocument, ICollection, IObject, IPost } from '@/core/activitypub/type.js';
import type { MiRemoteUser } from '@/models/User.js';
import { ApImageService } from '@/core/activitypub/models/ApImageService.js';
import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
@ -25,14 +19,14 @@ import { GlobalModule } from '@/GlobalModule.js';
import { CoreModule } from '@/core/CoreModule.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { LoggerService } from '@/core/LoggerService.js';
import type { IActor, IApDocument, ICollection, IObject, IPost } from '@/core/activitypub/type.js';
import { MiMeta, MiNote, UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import { DownloadService } from '@/core/DownloadService.js';
import type { MiRemoteUser } from '@/models/User.js';
import { genAidx } from '@/misc/id/aidx.js';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
import { MockResolver } from '../misc/mock-resolver.js';
const host = 'https://host1.test';
@ -126,13 +120,7 @@ describe('ActivityPub', () => {
imports: [GlobalModule, CoreModule],
})
.overrideProvider(DownloadService).useValue({
async downloadUrl(url: string, path: string): Promise<{ filename: string }> {
if (url.endsWith('.png')) {
fs.copyFileSync(
_dirname + '/../resources/hw.png',
path,
);
}
async downloadUrl(): Promise<{ filename: string }> {
return {
filename: 'dummy.tmp',
};
@ -452,7 +440,7 @@ describe('ActivityPub', () => {
});
});
describe('JSON-LD', () => {
describe('JSON-LD', () =>{
test('Compaction', async () => {
const jsonLd = jsonLdService.use();

View File

@ -17,8 +17,6 @@ import { ServerModule } from '@/server/ServerModule.js';
import { ServerService } from '@/server/ServerService.js';
import { IdService } from '@/core/IdService.js';
// TODO: uploadableFileTypes で許可されていないファイルが弾かれるかのテスト
describe('/drive/files/create', () => {
let module: TestingModule;
let server: FastifyInstance;
@ -27,8 +25,6 @@ describe('/drive/files/create', () => {
let root: MiUser;
let role_tinyAttachment: MiRole;
let role_imageOnly: MiRole;
let role_allowAllTypes: MiRole;
let folder: MiDriveFolder;
@ -68,34 +64,10 @@ describe('/drive/files/create', () => {
});
roleService = module.get<RoleService>(RoleService);
role_imageOnly = await roleService.create({
role_tinyAttachment = await roleService.create({
name: 'test-role001',
description: 'Test role001 description',
target: 'manual',
policies: {
uploadableFileTypes: {
useDefault: false,
priority: 1,
value: ['image/png'],
},
},
});
role_allowAllTypes = await roleService.create({
name: 'test-role002',
description: 'Test role002 description',
target: 'manual',
policies: {
uploadableFileTypes: {
useDefault: false,
priority: 1,
value: ['*/*'],
},
},
});
role_tinyAttachment = await roleService.create({
name: 'test-role003',
description: 'Test role003 description',
target: 'manual',
policies: {
maxFileSizeMb: {
useDefault: false,
@ -110,10 +82,6 @@ describe('/drive/files/create', () => {
beforeEach(async () => {
await roleService.unassign(root.id, role_tinyAttachment.id).catch(() => {
});
await roleService.unassign(root.id, role_imageOnly.id).catch(() => {
});
await roleService.unassign(root.id, role_allowAllTypes.id).catch(() => {
});
});
afterAll(async () => {
@ -142,9 +110,7 @@ describe('/drive/files/create', () => {
.field('i', root.token ?? '');
}
test('200 ok (all types allowed)', async () => {
await roleService.assign(root.id, role_allowAllTypes.id);
test('200 ok', async () => {
const name = randomString();
const comment = randomString();
const result = await postFile({
@ -161,24 +127,7 @@ describe('/drive/files/create', () => {
expect(result.body.folderId).toBe(folder.id);
});
test('400 when not allowed type', async () => {
await roleService.assign(root.id, role_imageOnly.id);
const name = randomString();
const comment = randomString();
const result = await postFile({
name: name,
comment: comment,
isSensitive: true,
force: true,
fileContent: Buffer.from('a'.repeat(10)),
});
expect(result.statusCode).toBe(400);
expect(result.body.error.code).toBe('UNALLOWED_FILE_TYPE');
});
test('200 ok (with size limited role)', async () => {
await roleService.assign(root.id, role_allowAllTypes.id);
test('200 ok(with role)', async () => {
await roleService.assign(root.id, role_tinyAttachment.id);
const name = randomString();
@ -198,7 +147,6 @@ describe('/drive/files/create', () => {
});
test('413 too large', async () => {
await roleService.assign(root.id, role_allowAllTypes.id);
await roleService.assign(root.id, role_tinyAttachment.id);
const name = randomString();

View File

@ -10,8 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
tail === 'left' ? $style.left : $style.right,
negativeMargin === true && $style.negativeMargin,
shadow === true && $style.shadow,
accented === true && $style.accented,
fullWidth === true && $style.fullWidth,
accented === true && $style.accented
]"
>
<div :class="$style.bg">
@ -33,13 +32,11 @@ withDefaults(defineProps<{
negativeMargin?: boolean;
shadow?: boolean;
accented?: boolean;
fullWidth?: boolean;
}>(), {
tail: 'right',
negativeMargin: false,
shadow: false,
accented: false,
fullWidth: false,
});
</script>
@ -76,14 +73,6 @@ withDefaults(defineProps<{
margin-right: calc(calc(var(--fukidashi-radius) * .13) * -1);
}
}
&.fullWidth {
width: 100%;
&.content {
width: 100%;
}
}
}
.bg {
@ -96,7 +85,6 @@ withDefaults(defineProps<{
.content {
position: relative;
padding: 10px 14px;
box-sizing: border-box;
}
@container (max-width: 450px) {

View File

@ -6,9 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div :class="[$style.root, { [$style.isMe]: isMe }]">
<MkAvatar :class="$style.avatar" :user="message.fromUser!" :link="!isMe" :preview="false"/>
<div :class="[$style.body, message.file != null ? $style.fullWidth : null]" @contextmenu.stop="onContextmenu">
<div :class="$style.body" @contextmenu.stop="onContextmenu">
<div :class="$style.header"><MkUserName v-if="!isMe && prefer.s['chat.showSenderName'] && message.fromUser != null" :user="message.fromUser"/></div>
<MkFukidashi :class="$style.fukidashi" :tail="isMe ? 'right' : 'left'" :fullWidth="message.file != null" :accented="isMe">
<MkFukidashi :class="$style.fukidashi" :tail="isMe ? 'right' : 'left'" :accented="isMe">
<Mfm
v-if="message.text"
ref="text"
@ -259,10 +259,6 @@ function showMenu(ev: MouseEvent, contextmenu = false) {
.body {
margin: 0 12px;
&.fullWidth {
width: 100%;
}
}
.header {

View File

@ -9,7 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-adaptive-border class="rfqxtzch _panel">
<div class="toggle">
<div class="toggleWrapper">
<div class="toggle" :class="store.r.darkMode.value ? 'checked' : null" @click="toggleDarkMode()">
<input id="dn" v-model="darkMode" type="checkbox" class="dn"/>
<label for="dn" class="toggle">
<span class="before">{{ i18n.ts.light }}</span>
<span class="after">{{ i18n.ts.dark }}</span>
<span class="toggle__handler">
@ -23,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span class="star star--4"></span>
<span class="star star--5"></span>
<span class="star star--6"></span>
</div>
</label>
</div>
</div>
<div class="sync">
@ -36,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div class="_gaps">
<template v-if="!store.r.darkMode.value">
<template v-if="!darkMode">
<SearchMarker :keywords="['light', 'theme']">
<MkFolder :defaultOpen="true" :max-height="500">
<template #icon><i class="ti ti-sun"></i></template>
@ -204,7 +205,6 @@ import JSON5 from 'json5';
import defaultLightTheme from '@@/themes/l-light.json5';
import defaultDarkTheme from '@@/themes/d-green-lime.json5';
import type { Theme } from '@/theme.js';
import * as os from '@/os.js';
import MkSwitch from '@/components/MkSwitch.vue';
import FormSection from '@/components/form/section.vue';
import FormLink from '@/components/form/link.vue';
@ -257,6 +257,7 @@ const lightThemeId = computed({
},
});
const darkMode = computed(store.makeGetterSetter('darkMode'));
const syncDeviceDarkMode = prefer.model('syncDeviceDarkMode');
const themesCount = installedThemes.value.length;
@ -266,21 +267,6 @@ watch(syncDeviceDarkMode, () => {
}
});
async function toggleDarkMode() {
const value = !store.r.darkMode.value;
if (syncDeviceDarkMode.value) {
const { canceled } = await os.confirm({
text: i18n.tsx.switchDarkModeManuallyWhenSyncEnabledConfirm({ x: i18n.ts.syncDeviceDarkMode }),
});
if (canceled) return;
syncDeviceDarkMode.value = false;
store.set('darkMode', value);
} else {
store.set('darkMode', value);
}
}
const themesSyncEnabled = ref(prefer.isSyncEnabled('themes'));
function changeThemesSyncEnabled(value: boolean) {
@ -379,6 +365,16 @@ definePage(() => ({
overflow: clip;
padding: 0 100px;
vertical-align: bottom;
input {
position: absolute;
left: -99em;
}
}
.dn:focus-visible ~ .toggle {
outline: 2px solid var(--MI_THEME-focus);
outline-offset: 2px;
}
.toggle {
@ -407,61 +403,6 @@ definePage(() => ({
right: -68px;
color: var(--MI_THEME-fg);
}
&.checked {
background-color: #749DD6;
> .before {
color: var(--MI_THEME-fg);
}
> .after {
color: var(--MI_THEME-accent);
}
.toggle__handler {
background-color: #FFE5B5;
transform: translate3d(40px, 0, 0) rotate(0);
.crater { opacity: 1; }
}
.star--1 {
width: 2px;
height: 2px;
}
.star--2 {
width: 4px;
height: 4px;
transform: translate3d(-5px, 0, 0);
}
.star--3 {
width: 2px;
height: 2px;
transform: translate3d(-7px, 0, 0);
}
.star--4,
.star--5,
.star--6 {
opacity: 1;
transform: translate3d(0,0,0);
}
.star--4 {
transition: all 300ms 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
}
.star--5 {
transition: all 300ms 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
}
.star--6 {
transition: all 300ms 400ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
}
}
}
.toggle__handler {
@ -572,6 +513,63 @@ definePage(() => ({
height: 2px;
transform: translate3d(3px,0,0);
}
input:checked {
+ .toggle {
background-color: #749DD6;
> .before {
color: var(--MI_THEME-fg);
}
> .after {
color: var(--MI_THEME-accent);
}
.toggle__handler {
background-color: #FFE5B5;
transform: translate3d(40px, 0, 0) rotate(0);
.crater { opacity: 1; }
}
.star--1 {
width: 2px;
height: 2px;
}
.star--2 {
width: 4px;
height: 4px;
transform: translate3d(-5px, 0, 0);
}
.star--3 {
width: 2px;
height: 2px;
transform: translate3d(-7px, 0, 0);
}
.star--4,
.star--5,
.star--6 {
opacity: 1;
transform: translate3d(0,0,0);
}
.star--4 {
transition: all 300ms 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
}
.star--5 {
transition: all 300ms 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
}
.star--6 {
transition: all 300ms 400ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
}
}
}
}
> .sync {

View File

@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2025.5.1-beta.5",
"version": "2025.5.1-beta.3",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",