Merge branch 'develop' into feat-mijs-expose-error-types
This commit is contained in:
commit
db4082a258
|
@ -3,6 +3,7 @@
|
||||||
### General
|
### General
|
||||||
- Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705
|
- Feat: 通報を受けた際、または解決した際に、予め登録した宛先に通知を飛ばせるように(mail or webhook) #13705
|
||||||
- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
|
- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
|
||||||
|
- Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
- Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正
|
- Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正
|
||||||
|
@ -23,6 +24,9 @@
|
||||||
- Enhance: エンドポイント`admin/ad/update`の必須項目を`id`のみに
|
- Enhance: エンドポイント`admin/ad/update`の必須項目を`id`のみに
|
||||||
- Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059)
|
- Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059)
|
||||||
- Fix: FTT有効時、タイムライン用エンドポイントで`sinceId`にキャッシュ内最古のものより古いものを指定した場合に正しく結果が返ってこない問題を修正
|
- Fix: FTT有効時、タイムライン用エンドポイントで`sinceId`にキャッシュ内最古のものより古いものを指定した場合に正しく結果が返ってこない問題を修正
|
||||||
|
- Fix: 自分以外のクリップ内のノート個数が見えることがあるのを修正
|
||||||
|
- Fix: 空文字列のリアクションはフォールバックされるように
|
||||||
|
- Fix: リノートにリアクションできないように
|
||||||
|
|
||||||
### Misskey.js
|
### Misskey.js
|
||||||
- Feat: reject時のエラーに型情報を追加
|
- Feat: reject時のエラーに型情報を追加
|
||||||
|
|
|
@ -82,6 +82,10 @@ RUN apt-get update \
|
||||||
USER misskey
|
USER misskey
|
||||||
WORKDIR /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/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/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
|
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-js/node_modules ./packages/misskey-js/node_modules
|
||||||
|
|
|
@ -2599,16 +2599,16 @@ _externalResourceInstaller:
|
||||||
|
|
||||||
_dataSaver:
|
_dataSaver:
|
||||||
_media:
|
_media:
|
||||||
title: "メディアの読み込み"
|
title: "メディアの読み込みを無効化"
|
||||||
description: "画像・動画が自動で読み込まれるのを防止します。隠れている画像・動画はタップすると読み込まれます。"
|
description: "画像・動画が自動で読み込まれるのを防止します。隠れている画像・動画はタップすると読み込まれます。"
|
||||||
_avatar:
|
_avatar:
|
||||||
title: "アイコン画像"
|
title: "アイコン画像のアニメーションを無効化"
|
||||||
description: "アイコン画像のアニメーションが停止します。アニメーション画像は通常の画像よりファイルサイズが大きいことがあるので、データ通信量をさらに削減できます。"
|
description: "アイコン画像のアニメーションが停止します。アニメーション画像は通常の画像よりファイルサイズが大きいことがあるので、データ通信量をさらに削減できます。"
|
||||||
_urlPreview:
|
_urlPreview:
|
||||||
title: "URLプレビューのサムネイル"
|
title: "URLプレビューのサムネイルを非表示"
|
||||||
description: "URLプレビューのサムネイル画像が読み込まれなくなります。"
|
description: "URLプレビューのサムネイル画像が読み込まれなくなります。"
|
||||||
_code:
|
_code:
|
||||||
title: "コードハイライト"
|
title: "コードハイライトを非表示"
|
||||||
description: "MFMなどでコードハイライト記法が使われている場合、タップするまで読み込まれなくなります。コードハイライトではハイライトする言語ごとにその定義ファイルを読み込む必要がありますが、それらが自動で読み込まれなくなるため、通信量の削減が見込めます。"
|
description: "MFMなどでコードハイライト記法が使われている場合、タップするまで読み込まれなくなります。コードハイライトではハイライトする言語ごとにその定義ファイルを読み込む必要がありますが、それらが自動で読み込まれなくなるため、通信量の削減が見込めます。"
|
||||||
|
|
||||||
_hemisphere:
|
_hemisphere:
|
||||||
|
|
|
@ -207,7 +207,6 @@
|
||||||
"@types/mime-types": "2.1.4",
|
"@types/mime-types": "2.1.4",
|
||||||
"@types/ms": "0.7.34",
|
"@types/ms": "0.7.34",
|
||||||
"@types/node": "20.12.7",
|
"@types/node": "20.12.7",
|
||||||
"@types/node-fetch": "3.0.3",
|
|
||||||
"@types/nodemailer": "6.4.15",
|
"@types/nodemailer": "6.4.15",
|
||||||
"@types/oauth": "0.9.4",
|
"@types/oauth": "0.9.4",
|
||||||
"@types/oauth2orize": "1.11.5",
|
"@types/oauth2orize": "1.11.5",
|
||||||
|
|
|
@ -29,6 +29,7 @@ import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { FeaturedService } from '@/core/FeaturedService.js';
|
import { FeaturedService } from '@/core/FeaturedService.js';
|
||||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||||
|
import { isQuote, isRenote } from '@/misc/is-renote.js';
|
||||||
|
|
||||||
const FALLBACK = '\u2764';
|
const FALLBACK = '\u2764';
|
||||||
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
|
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.');
|
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;
|
let reaction = _reaction ?? FALLBACK;
|
||||||
|
|
||||||
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && (user.host != null))) {
|
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && (user.host != null))) {
|
||||||
reaction = '\u2764';
|
reaction = '\u2764';
|
||||||
} else if (_reaction) {
|
} else if (_reaction != null) {
|
||||||
const custom = reaction.match(isCustomEmojiRegexp);
|
const custom = reaction.match(isCustomEmojiRegexp);
|
||||||
if (custom) {
|
if (custom) {
|
||||||
const reacterHost = this.utilityService.toPunyNullable(user.host);
|
const reacterHost = this.utilityService.toPunyNullable(user.host);
|
||||||
|
|
|
@ -53,7 +53,7 @@ export class ClipEntityService {
|
||||||
isPublic: clip.isPublic,
|
isPublic: clip.isPublic,
|
||||||
favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }),
|
favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }),
|
||||||
isFavorited: meId ? await this.clipFavoritesRepository.exists({ where: { clipId: clip.id, userId: meId } }) : undefined,
|
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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -65,44 +65,6 @@ export function maximum(xs: number[]): number {
|
||||||
return Math.max(...xs);
|
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
|
* Compare two arrays by lexicographical order
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -36,6 +36,12 @@ export const meta = {
|
||||||
code: 'YOU_HAVE_BEEN_BLOCKED',
|
code: 'YOU_HAVE_BEEN_BLOCKED',
|
||||||
id: '20ef5475-9f38-4e4c-bd33-de6d979498ec',
|
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;
|
} 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 => {
|
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 === '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 === '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;
|
throw err;
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -266,6 +266,67 @@ describe('Endpoints', () => {
|
||||||
assert.strictEqual(res.status, 400);
|
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 () => {
|
test('空のパラメータで怒られる', async () => {
|
||||||
// @ts-expect-error param must not be empty
|
// @ts-expect-error param must not be empty
|
||||||
const res = await api('notes/reactions/create', {}, alice);
|
const res = await api('notes/reactions/create', {}, alice);
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { url } from '@/config.js';
|
import { url } from '@/config.js';
|
||||||
|
|
||||||
export const acct = (user: misskey.Acct) => {
|
export const acct = (user: Misskey.Acct) => {
|
||||||
return Misskey.acct.toString(user);
|
return Misskey.acct.toString(user);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -14,6 +14,6 @@ export const userName = (user: Misskey.entities.User) => {
|
||||||
return user.name || user.username;
|
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}` : '')}`;
|
return `${absolute ? url : ''}/@${acct(user)}${(path ? `/${path}` : '')}`;
|
||||||
};
|
};
|
||||||
|
|
|
@ -77,44 +77,6 @@ export function maximum(xs: number[]): number {
|
||||||
return Math.max(...xs);
|
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
|
* Compare two arrays by lexicographical order
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -586,9 +586,6 @@ importers:
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: 20.12.7
|
specifier: 20.12.7
|
||||||
version: 20.12.7
|
version: 20.12.7
|
||||||
'@types/node-fetch':
|
|
||||||
specifier: 3.0.3
|
|
||||||
version: 3.0.3
|
|
||||||
'@types/nodemailer':
|
'@types/nodemailer':
|
||||||
specifier: 6.4.15
|
specifier: 6.4.15
|
||||||
version: 6.4.15
|
version: 6.4.15
|
||||||
|
@ -4625,10 +4622,6 @@ packages:
|
||||||
'@types/node-fetch@2.6.4':
|
'@types/node-fetch@2.6.4':
|
||||||
resolution: {integrity: sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==}
|
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':
|
'@types/node@18.17.15':
|
||||||
resolution: {integrity: sha512-2yrWpBk32tvV/JAd3HNHWuZn/VDN1P+72hWirHnvsvTGSqbANi+kSeuQR9yAHnbvaBvHDsoTdXV0Fe+iRtHLKA==}
|
resolution: {integrity: sha512-2yrWpBk32tvV/JAd3HNHWuZn/VDN1P+72hWirHnvsvTGSqbANi+kSeuQR9yAHnbvaBvHDsoTdXV0Fe+iRtHLKA==}
|
||||||
|
|
||||||
|
@ -16025,10 +16018,6 @@ snapshots:
|
||||||
'@types/node': 20.12.7
|
'@types/node': 20.12.7
|
||||||
form-data: 3.0.1
|
form-data: 3.0.1
|
||||||
|
|
||||||
'@types/node-fetch@3.0.3':
|
|
||||||
dependencies:
|
|
||||||
node-fetch: 3.3.2
|
|
||||||
|
|
||||||
'@types/node@18.17.15': {}
|
'@types/node@18.17.15': {}
|
||||||
|
|
||||||
'@types/node@20.11.5':
|
'@types/node@20.11.5':
|
||||||
|
|
Loading…
Reference in New Issue