Compare commits
6 Commits
2025.2.1-b
...
2025.2.1-b
Author | SHA1 | Date | |
---|---|---|---|
![]() |
96fc7e307a | ||
![]() |
bd13fb626c | ||
![]() |
d87488a5f0 | ||
![]() |
495db27433 | ||
![]() |
39c487e1d1 | ||
![]() |
b5799351d0 |
@@ -18,15 +18,19 @@
|
||||
- Fix: 埋め込みプレイヤーから外部ページに移動できない問題を修正
|
||||
- Fix: Play の再読込時に UI が以前の状態を引き継いでしまう問題を修正 `#14378`
|
||||
- Fix: カスタム絵文字管理画面(beta)にてisSensitive/localOnlyの絞り込みが上手くいかない問題の修正 ( #15445 )
|
||||
- Fix: ユーザのサジェスト中に@を入力してもサジェスト結果が消えないように `#14385`
|
||||
- Fix: CWの注釈が100文字を超えている場合、ノート投稿ボタンを非アクティブに
|
||||
|
||||
### Server
|
||||
- Enhance: 成り済まし対策として、ActivityPub照会された時にリモートのリダイレクトを拒否できるように (config.disallowExternalApRedirect)
|
||||
- Fix: `following/invalidate`でフォロワーを解除しようとしているユーザーの情報を返すように
|
||||
- Fix: オブジェクトストレージの設定でPrefixを設定していなかった場合nullまたは空文字になる問題を修正
|
||||
- Fix: HTTPプロキシとその除外設定を行った状態でカスタム絵文字の一括インポートをしたとき、除外設定が効かないのを修正( #8766 )
|
||||
- Fix: pgroongaでの検索時にはじめのキーワードのみが検索に使用される問題を修正
|
||||
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/886)
|
||||
- Fix: メールアドレスの形式が正しくなければ以降の処理を行わないように
|
||||
- Fix: `update-meta`でobjectStoragePrefixにS3_SAFEかつURL-safeでない文字列を使えないように
|
||||
- Fix: クリップの説明欄を更新する際に空にできない問題を修正
|
||||
- Fix: フォロワーではないユーザーにリノートもしくは返信された場合にノートのDeleteアクティビティが送られていない問題を修正
|
||||
|
||||
## 2025.2.0
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "2025.2.1-beta.1",
|
||||
"version": "2025.2.1-beta.2",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@@ -60,8 +60,8 @@ export class DownloadService {
|
||||
request: operationTimeout, // whole operation timeout
|
||||
},
|
||||
agent: {
|
||||
http: this.httpRequestService.httpAgent,
|
||||
https: this.httpRequestService.httpsAgent,
|
||||
http: this.httpRequestService.getAgentForHttp(urlObj, true),
|
||||
https: this.httpRequestService.getAgentForHttps(urlObj, true),
|
||||
},
|
||||
http2: false, // default
|
||||
retry: {
|
||||
|
@@ -115,32 +115,32 @@ export class HttpRequestService {
|
||||
/**
|
||||
* Get http non-proxy agent (without local address filtering)
|
||||
*/
|
||||
private httpNative: http.Agent;
|
||||
private readonly httpNative: http.Agent;
|
||||
|
||||
/**
|
||||
* Get https non-proxy agent (without local address filtering)
|
||||
*/
|
||||
private httpsNative: https.Agent;
|
||||
private readonly httpsNative: https.Agent;
|
||||
|
||||
/**
|
||||
* Get http non-proxy agent
|
||||
*/
|
||||
private http: http.Agent;
|
||||
private readonly http: http.Agent;
|
||||
|
||||
/**
|
||||
* Get https non-proxy agent
|
||||
*/
|
||||
private https: https.Agent;
|
||||
private readonly https: https.Agent;
|
||||
|
||||
/**
|
||||
* Get http proxy or non-proxy agent
|
||||
*/
|
||||
public httpAgent: http.Agent;
|
||||
public readonly httpAgent: http.Agent;
|
||||
|
||||
/**
|
||||
* Get https proxy or non-proxy agent
|
||||
*/
|
||||
public httpsAgent: https.Agent;
|
||||
public readonly httpsAgent: https.Agent;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
@@ -197,7 +197,8 @@ export class HttpRequestService {
|
||||
/**
|
||||
* Get agent by URL
|
||||
* @param url URL
|
||||
* @param bypassProxy Allways bypass proxy
|
||||
* @param bypassProxy Always bypass proxy
|
||||
* @param isLocalAddressAllowed
|
||||
*/
|
||||
@bindThis
|
||||
public getAgentByUrl(url: URL, bypassProxy = false, isLocalAddressAllowed = false): http.Agent | https.Agent {
|
||||
@@ -214,6 +215,38 @@ export class HttpRequestService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agent for http by URL
|
||||
* @param url URL
|
||||
* @param isLocalAddressAllowed
|
||||
*/
|
||||
@bindThis
|
||||
public getAgentForHttp(url: URL, isLocalAddressAllowed = false): http.Agent {
|
||||
if ((this.config.proxyBypassHosts ?? []).includes(url.hostname)) {
|
||||
return isLocalAddressAllowed
|
||||
? this.httpNative
|
||||
: this.http;
|
||||
} else {
|
||||
return this.httpAgent;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agent for https by URL
|
||||
* @param url URL
|
||||
* @param isLocalAddressAllowed
|
||||
*/
|
||||
@bindThis
|
||||
public getAgentForHttps(url: URL, isLocalAddressAllowed = false): https.Agent {
|
||||
if ((this.config.proxyBypassHosts ?? []).includes(url.hostname)) {
|
||||
return isLocalAddressAllowed
|
||||
? this.httpsNative
|
||||
: this.https;
|
||||
} else {
|
||||
return this.httpsAgent;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getActivityJson(url: string, isLocalAddressAllowed = false, allowSoftfail: FetchAllowSoftFailMask = FetchAllowSoftFailMask.Strict): Promise<IObject> {
|
||||
const res = await this.send(url, {
|
||||
|
@@ -74,7 +74,7 @@ export class RemoteUserResolveService {
|
||||
if (user == null) {
|
||||
const self = await this.resolveSelf(acctLower);
|
||||
|
||||
if (self.href.startsWith(this.config.url)) {
|
||||
if (this.utilityService.isUriLocal(self.href)) {
|
||||
const local = this.apDbResolverService.parseUri(self.href);
|
||||
if (local.local && local.type === 'users') {
|
||||
// the LR points to local
|
||||
|
@@ -27,6 +27,7 @@ import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFil
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { JsonLdService } from './JsonLdService.js';
|
||||
import { ApMfmService } from './ApMfmService.js';
|
||||
import { CONTEXT } from './misc/contexts.js';
|
||||
@@ -61,6 +62,7 @@ export class ApRendererService {
|
||||
private apMfmService: ApMfmService,
|
||||
private mfmService: MfmService,
|
||||
private idService: IdService,
|
||||
private utilityService: UtilityService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -577,7 +579,7 @@ export class ApRendererService {
|
||||
|
||||
@bindThis
|
||||
public renderUndo(object: string | IObject, user: { id: MiUser['id'] }): IUndo {
|
||||
const id = typeof object !== 'string' && typeof object.id === 'string' && object.id.startsWith(this.config.url) ? `${object.id}/undo` : undefined;
|
||||
const id = typeof object !== 'string' && typeof object.id === 'string' && this.utilityService.isUriLocal(object.id) ? `${object.id}/undo` : undefined;
|
||||
|
||||
return {
|
||||
type: 'Undo',
|
||||
|
@@ -118,7 +118,7 @@ export const paramDef = {
|
||||
useObjectStorage: { type: 'boolean' },
|
||||
objectStorageBaseUrl: { type: 'string', nullable: true },
|
||||
objectStorageBucket: { type: 'string', nullable: true },
|
||||
objectStoragePrefix: { type: 'string', nullable: true },
|
||||
objectStoragePrefix: { type: 'string', pattern: /^[a-zA-Z0-9-._]*$/.source, nullable: true },
|
||||
objectStorageEndpoint: { type: 'string', nullable: true },
|
||||
objectStorageRegion: { type: 'string', nullable: true },
|
||||
objectStoragePort: { type: 'integer', nullable: true },
|
||||
|
@@ -39,7 +39,7 @@ export const paramDef = {
|
||||
properties: {
|
||||
name: { type: 'string', minLength: 1, maxLength: 100 },
|
||||
isPublic: { type: 'boolean', default: false },
|
||||
description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 },
|
||||
description: { type: 'string', nullable: true, maxLength: 2048 },
|
||||
},
|
||||
required: ['name'],
|
||||
} as const;
|
||||
@@ -53,7 +53,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
let clip: MiClip;
|
||||
try {
|
||||
clip = await this.clipService.create(me, ps.name, ps.isPublic, ps.description ?? null);
|
||||
// 空文字列をnullにしたいので??は使わない
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
clip = await this.clipService.create(me, ps.name, ps.isPublic, ps.description || null);
|
||||
} catch (e) {
|
||||
if (e instanceof ClipService.TooManyClipsError) {
|
||||
throw new ApiError(meta.errors.tooManyClips);
|
||||
|
@@ -39,7 +39,7 @@ export const paramDef = {
|
||||
clipId: { type: 'string', format: 'misskey:id' },
|
||||
name: { type: 'string', minLength: 1, maxLength: 100 },
|
||||
isPublic: { type: 'boolean' },
|
||||
description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 },
|
||||
description: { type: 'string', nullable: true, maxLength: 2048 },
|
||||
},
|
||||
required: ['clipId'],
|
||||
} as const;
|
||||
@@ -53,7 +53,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
try {
|
||||
await this.clipService.update(me, ps.clipId, ps.name, ps.isPublic, ps.description);
|
||||
// 空文字列をnullにしたいので??は使わない
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
await this.clipService.update(me, ps.clipId, ps.name, ps.isPublic, ps.description || null);
|
||||
} catch (e) {
|
||||
if (e instanceof ClipService.NoSuchClipError) {
|
||||
throw new ApiError(meta.errors.noSuchClip);
|
||||
|
@@ -182,7 +182,6 @@ describe('クリップ', () => {
|
||||
{ label: 'nameがnull', parameters: { name: null } },
|
||||
{ label: 'nameが最大長+1', parameters: { name: 'x'.repeat(101) } },
|
||||
{ label: 'isPublicがboolじゃない', parameters: { isPublic: 'true' } },
|
||||
{ label: 'descriptionがゼロ長', parameters: { description: '' } },
|
||||
{ label: 'descriptionが最大長+1', parameters: { description: 'a'.repeat(2049) } },
|
||||
];
|
||||
test.each(createClipDenyPattern)('の作成は$labelならできない', async ({ parameters }) => failedApiCall({
|
||||
@@ -199,6 +198,23 @@ describe('クリップ', () => {
|
||||
id: '3d81ceae-475f-4600-b2a8-2bc116157532',
|
||||
}));
|
||||
|
||||
test('の作成はdescriptionが空文字ならnullになる', async () => {
|
||||
const clip = await successfulApiCall({
|
||||
endpoint: 'clips/create',
|
||||
parameters: {
|
||||
...defaultCreate(),
|
||||
description: '',
|
||||
},
|
||||
user: alice,
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(clip, {
|
||||
...clip,
|
||||
...defaultCreate(),
|
||||
description: null,
|
||||
});
|
||||
});
|
||||
|
||||
test('の更新ができる', async () => {
|
||||
const res = await update({
|
||||
clipId: (await create()).id,
|
||||
@@ -249,6 +265,24 @@ describe('クリップ', () => {
|
||||
...assertion,
|
||||
}));
|
||||
|
||||
test('の更新はdescriptionが空文字ならnullになる', async () => {
|
||||
const clip = await successfulApiCall({
|
||||
endpoint: 'clips/update',
|
||||
parameters: {
|
||||
clipId: (await create()).id,
|
||||
name: 'updated',
|
||||
description: '',
|
||||
},
|
||||
user: alice,
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(clip, {
|
||||
...clip,
|
||||
name: 'updated',
|
||||
description: null,
|
||||
});
|
||||
});
|
||||
|
||||
test('の削除ができる', async () => {
|
||||
await deleteClip({
|
||||
clipId: (await create()).id,
|
||||
|
@@ -3,6 +3,8 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import { jest } from '@jest/globals';
|
||||
@@ -40,6 +42,7 @@ describe('RelayService', () => {
|
||||
ApRendererService,
|
||||
RelayService,
|
||||
UserEntityService,
|
||||
UtilityService,
|
||||
],
|
||||
})
|
||||
.useMocker((token) => {
|
||||
|
@@ -49,6 +49,7 @@ import sanitizeHtml from 'sanitize-html';
|
||||
import { emojilist, getEmojiName } from '@@/js/emojilist.js';
|
||||
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@@/js/emoji-base.js';
|
||||
import { MFM_TAGS, MFM_PARAMS } from '@@/js/const.js';
|
||||
import type { EmojiDef } from '@/scripts/search-emoji.js';
|
||||
import contains from '@/scripts/contains.js';
|
||||
import { acct } from '@/filters/user.js';
|
||||
import * as os from '@/os.js';
|
||||
@@ -58,7 +59,6 @@ import { i18n } from '@/i18n.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { customEmojis } from '@/custom-emojis.js';
|
||||
import { searchEmoji } from '@/scripts/search-emoji.js';
|
||||
import type { EmojiDef } from '@/scripts/search-emoji.js';
|
||||
|
||||
const lib = emojilist.filter(x => x.category !== 'flags');
|
||||
|
||||
@@ -198,8 +198,10 @@ function exec() {
|
||||
users.value = JSON.parse(cache);
|
||||
fetching.value = false;
|
||||
} else {
|
||||
const [username, host] = props.q.toString().split('@');
|
||||
misskeyApi('users/search-by-username-and-host', {
|
||||
username: props.q,
|
||||
username: username,
|
||||
host: host,
|
||||
limit: 10,
|
||||
detail: false,
|
||||
}).then(searchedUsers => {
|
||||
|
@@ -4,9 +4,9 @@
|
||||
*/
|
||||
|
||||
import { nextTick, ref, defineAsyncComponent } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import getCaretCoordinates from 'textarea-caret';
|
||||
import { toASCII } from 'punycode.js';
|
||||
import type { Ref } from 'vue';
|
||||
import { popup } from '@/os.js';
|
||||
|
||||
export type SuggestionType = 'user' | 'hashtag' | 'emoji' | 'mfmTag' | 'mfmParam';
|
||||
@@ -98,15 +98,21 @@ export class Autocomplete {
|
||||
|
||||
const isMention = mentionIndex !== -1;
|
||||
const isHashtag = hashtagIndex !== -1;
|
||||
const isMfmParam = mfmParamIndex !== -1 && afterLastMfmParam?.includes('.') && !afterLastMfmParam?.includes(' ');
|
||||
const isMfmParam = mfmParamIndex !== -1 && afterLastMfmParam?.includes('.') && !afterLastMfmParam.includes(' ');
|
||||
const isMfmTag = mfmTagIndex !== -1 && !isMfmParam;
|
||||
const isEmoji = emojiIndex !== -1 && text.split(/:[a-z0-9_+\-]+:/).pop()!.includes(':');
|
||||
|
||||
let opened = false;
|
||||
|
||||
if (isMention && this.onlyType.includes('user')) {
|
||||
const username = text.substring(mentionIndex + 1);
|
||||
if (username !== '' && username.match(/^[a-zA-Z0-9_]+$/)) {
|
||||
// ユーザのサジェスト中に@を入力すると、その位置から新たにユーザ名を取りなおそうとしてしまう
|
||||
// この動きはリモートユーザのサジェストを阻害するので、@を検知したらその位置よりも前の@を探し、
|
||||
// ホスト名を含むリモートのユーザ名を全て拾えるようにする
|
||||
const mentionIndexAlt = text.lastIndexOf('@', mentionIndex - 1);
|
||||
const username = mentionIndexAlt === -1
|
||||
? text.substring(mentionIndex + 1)
|
||||
: text.substring(mentionIndexAlt + 1);
|
||||
if (username !== '' && username.match(/^[a-zA-Z0-9_@.]+$/)) {
|
||||
this.open('user', username);
|
||||
opened = true;
|
||||
} else if (username === '') {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"type": "module",
|
||||
"name": "misskey-js",
|
||||
"version": "2025.2.1-beta.1",
|
||||
"version": "2025.2.1-beta.2",
|
||||
"description": "Misskey SDK for JavaScript",
|
||||
"license": "MIT",
|
||||
"main": "./built/index.js",
|
||||
|
Reference in New Issue
Block a user