Merge branch 'develop' into fix-7311

This commit is contained in:
かっこかり 2024-06-23 16:49:05 +09:00 committed by GitHub
commit f3d2956a19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 90 additions and 96 deletions

View File

@ -3,6 +3,7 @@
### General
- Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705
- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
- Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題
- Fix: センシティブな画像をアイコン・バナーに指定できないように
### Client
@ -25,6 +26,9 @@
- Enhance: エンドポイント`admin/ad/update`の必須項目を`id`のみに
- Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059)
- Fix: FTT有効時、タイムライン用エンドポイントで`sinceId`にキャッシュ内最古のものより古いものを指定した場合に正しく結果が返ってこない問題を修正
- Fix: 自分以外のクリップ内のノート個数が見えることがあるのを修正
- Fix: 空文字列のリアクションはフォールバックされるように
- Fix: リノートにリアクションできないように
## 2024.5.0

View File

@ -82,6 +82,10 @@ RUN apt-get update \
USER misskey
WORKDIR /misskey
# add package.json to add pnpm
COPY --chown=misskey:misskey ./package.json ./package.json
RUN corepack install
COPY --chown=misskey:misskey --from=target-builder /misskey/node_modules ./node_modules
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/backend/node_modules ./packages/backend/node_modules
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-js/node_modules ./packages/misskey-js/node_modules

View File

@ -2601,16 +2601,16 @@ _externalResourceInstaller:
_dataSaver:
_media:
title: "メディアの読み込み"
title: "メディアの読み込みを無効化"
description: "画像・動画が自動で読み込まれるのを防止します。隠れている画像・動画はタップすると読み込まれます。"
_avatar:
title: "アイコン画像"
title: "アイコン画像のアニメーションを無効化"
description: "アイコン画像のアニメーションが停止します。アニメーション画像は通常の画像よりファイルサイズが大きいことがあるので、データ通信量をさらに削減できます。"
_urlPreview:
title: "URLプレビューのサムネイル"
title: "URLプレビューのサムネイルを非表示"
description: "URLプレビューのサムネイル画像が読み込まれなくなります。"
_code:
title: "コードハイライト"
title: "コードハイライトを非表示"
description: "MFMなどでコードハイライト記法が使われている場合、タップするまで読み込まれなくなります。コードハイライトではハイライトする言語ごとにその定義ファイルを読み込む必要がありますが、それらが自動で読み込まれなくなるため、通信量の削減が見込めます。"
_hemisphere:

View File

@ -207,7 +207,6 @@
"@types/mime-types": "2.1.4",
"@types/ms": "0.7.34",
"@types/node": "20.12.7",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.15",
"@types/oauth": "0.9.4",
"@types/oauth2orize": "1.11.5",

View File

@ -29,6 +29,7 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { RoleService } from '@/core/RoleService.js';
import { FeaturedService } from '@/core/FeaturedService.js';
import { trackPromise } from '@/misc/promise-tracker.js';
import { isQuote, isRenote } from '@/misc/is-renote.js';
const FALLBACK = '\u2764';
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
@ -117,11 +118,16 @@ export class ReactionService {
throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.');
}
// Check if note is Renote
if (isRenote(note) && !isQuote(note)) {
throw new IdentifiableError('12c35529-3c79-4327-b1cc-e2cf63a71925', 'You cannot react to Renote.');
}
let reaction = _reaction ?? FALLBACK;
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && (user.host != null))) {
reaction = '\u2764';
} else if (_reaction) {
} else if (_reaction != null) {
const custom = reaction.match(isCustomEmojiRegexp);
if (custom) {
const reacterHost = this.utilityService.toPunyNullable(user.host);

View File

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

View File

@ -65,44 +65,6 @@ export function maximum(xs: number[]): number {
return Math.max(...xs);
}
/**
* Splits an array based on the equivalence relation.
* The concatenation of the result is equal to the argument.
*/
export function groupBy<T>(f: EndoRelation<T>, xs: T[]): T[][] {
const groups = [] as T[][];
for (const x of xs) {
const lastGroup = groups.at(-1);
if (lastGroup !== undefined && f(lastGroup[0], x)) {
lastGroup.push(x);
} else {
groups.push([x]);
}
}
return groups;
}
/**
* Splits an array based on the equivalence relation induced by the function.
* The concatenation of the result is equal to the argument.
*/
export function groupOn<T, S>(f: (x: T) => S, xs: T[]): T[][] {
return groupBy((a, b) => f(a) === f(b), xs);
}
export function groupByX<T>(collections: T[], keySelector: (x: T) => string) {
return collections.reduce((obj: Record<string, T[]>, item: T) => {
const key = keySelector(item);
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
obj[key] = [];
}
obj[key].push(item);
return obj;
}, {});
}
/**
* Compare two arrays by lexicographical order
*/

View File

@ -36,6 +36,12 @@ export const meta = {
code: 'YOU_HAVE_BEEN_BLOCKED',
id: '20ef5475-9f38-4e4c-bd33-de6d979498ec',
},
cannotReactToRenote: {
message: 'You cannot react to Renote.',
code: 'CANNOT_REACT_TO_RENOTE',
id: 'eaccdc08-ddef-43fe-908f-d108faad57f5',
},
},
} as const;
@ -62,6 +68,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
await this.reactionService.create(me, note, ps.reaction).catch(err => {
if (err.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') throw new ApiError(meta.errors.alreadyReacted);
if (err.id === 'e70412a4-7197-4726-8e74-f3e0deb92aa7') throw new ApiError(meta.errors.youHaveBeenBlocked);
if (err.id === '12c35529-3c79-4327-b1cc-e2cf63a71925') throw new ApiError(meta.errors.cannotReactToRenote);
throw err;
});
return;

View File

@ -266,6 +266,67 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 400);
});
test('リノートにリアクションできない', async () => {
const bobNote = await post(bob, { text: 'hi' });
const bobRenote = await post(bob, { renoteId: bobNote.id });
const res = await api('notes/reactions/create', {
noteId: bobRenote.id,
reaction: '🚀',
}, alice);
assert.strictEqual(res.status, 400);
assert.strictEqual(res.body.error.code, 'CANNOT_REACT_TO_RENOTE');
});
test('引用にリアクションできる', async () => {
const bobNote = await post(bob, { text: 'hi' });
const bobRenote = await post(bob, { text: 'hi again', renoteId: bobNote.id });
const res = await api('notes/reactions/create', {
noteId: bobRenote.id,
reaction: '🚀',
}, alice);
assert.strictEqual(res.status, 204);
});
test('空文字列のリアクションは\u2764にフォールバックされる', async () => {
const bobNote = await post(bob, { text: 'hi' });
const res = await api('notes/reactions/create', {
noteId: bobNote.id,
reaction: '',
}, alice);
assert.strictEqual(res.status, 204);
const reaction = await api('notes/reactions', {
noteId: bobNote.id,
});
assert.strictEqual(reaction.body.length, 1);
assert.strictEqual(reaction.body[0].type, '\u2764');
});
test('絵文字ではない文字列のリアクションは\u2764にフォールバックされる', async () => {
const bobNote = await post(bob, { text: 'hi' });
const res = await api('notes/reactions/create', {
noteId: bobNote.id,
reaction: 'Hello!',
}, alice);
assert.strictEqual(res.status, 204);
const reaction = await api('notes/reactions', {
noteId: bobNote.id,
});
assert.strictEqual(reaction.body.length, 1);
assert.strictEqual(reaction.body[0].type, '\u2764');
});
test('空のパラメータで怒られる', async () => {
// @ts-expect-error param must not be empty
const res = await api('notes/reactions/create', {}, alice);

View File

@ -6,7 +6,7 @@
import * as Misskey from 'misskey-js';
import { url } from '@/config.js';
export const acct = (user: misskey.Acct) => {
export const acct = (user: Misskey.Acct) => {
return Misskey.acct.toString(user);
};
@ -14,6 +14,6 @@ export const userName = (user: Misskey.entities.User) => {
return user.name || user.username;
};
export const userPage = (user: misskey.Acct, path?, absolute = false) => {
export const userPage = (user: Misskey.Acct, path?: string, absolute = false) => {
return `${absolute ? url : ''}/@${acct(user)}${(path ? `/${path}` : '')}`;
};

View File

@ -77,44 +77,6 @@ export function maximum(xs: number[]): number {
return Math.max(...xs);
}
/**
* Splits an array based on the equivalence relation.
* The concatenation of the result is equal to the argument.
*/
export function groupBy<T>(f: EndoRelation<T>, xs: T[]): T[][] {
const groups = [] as T[][];
for (const x of xs) {
const lastGroup = groups.at(-1);
if (lastGroup !== undefined && f(lastGroup[0], x)) {
lastGroup.push(x);
} else {
groups.push([x]);
}
}
return groups;
}
/**
* Splits an array based on the equivalence relation induced by the function.
* The concatenation of the result is equal to the argument.
*/
export function groupOn<T, S>(f: (x: T) => S, xs: T[]): T[][] {
return groupBy((a, b) => f(a) === f(b), xs);
}
export function groupByX<T>(collections: T[], keySelector: (x: T) => string) {
return collections.reduce((obj: Record<string, T[]>, item: T) => {
const key = keySelector(item);
if (typeof obj[key] === 'undefined') {
obj[key] = [];
}
obj[key].push(item);
return obj;
}, {});
}
/**
* Compare two arrays by lexicographical order
*/

View File

@ -586,9 +586,6 @@ importers:
'@types/node':
specifier: 20.12.7
version: 20.12.7
'@types/node-fetch':
specifier: 3.0.3
version: 3.0.3
'@types/nodemailer':
specifier: 6.4.15
version: 6.4.15
@ -4625,10 +4622,6 @@ packages:
'@types/node-fetch@2.6.4':
resolution: {integrity: sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==}
'@types/node-fetch@3.0.3':
resolution: {integrity: sha512-HhggYPH5N+AQe/OmN6fmhKmRRt2XuNJow+R3pQwJxOOF9GuwM7O2mheyGeIrs5MOIeNjDEdgdoyHBOrFeJBR3g==}
deprecated: This is a stub types definition. node-fetch provides its own type definitions, so you do not need this installed.
'@types/node@18.17.15':
resolution: {integrity: sha512-2yrWpBk32tvV/JAd3HNHWuZn/VDN1P+72hWirHnvsvTGSqbANi+kSeuQR9yAHnbvaBvHDsoTdXV0Fe+iRtHLKA==}
@ -16025,10 +16018,6 @@ snapshots:
'@types/node': 20.12.7
form-data: 3.0.1
'@types/node-fetch@3.0.3':
dependencies:
node-fetch: 3.3.2
'@types/node@18.17.15': {}
'@types/node@20.11.5':