Compare commits

..

51 Commits

Author SHA1 Message Date
syuilo
77498f84d8 wip 2023-10-03 20:16:00 +09:00
syuilo
0575207463 Merge branch 'develop' into tl-push 2023-10-03 18:34:26 +09:00
syuilo
7a3dd400d8 Update NoteCreateService.ts 2023-10-03 18:17:06 +09:00
syuilo
e840544dd2 refactor: UserListJoining -> UserListMembership 2023-10-03 18:07:58 +09:00
syuilo
0e58f515fd Update activitypub.ts 2023-10-03 17:08:05 +09:00
syuilo
c3714c02ba test 2023-10-03 16:26:58 +09:00
syuilo
9de11da170 Merge branch 'develop' into tl-push 2023-10-03 15:33:33 +09:00
syuilo
e12943c15b wip 2023-10-03 15:32:29 +09:00
syuilo
7022b16bce Update timelines.ts 2023-10-03 14:10:49 +09:00
syuilo
ee3d40bc1b wip 2023-10-03 14:07:49 +09:00
syuilo
878e73cd37 Update timelines.ts 2023-10-03 13:56:10 +09:00
syuilo
96da6e28ea wip 2023-10-03 13:47:50 +09:00
syuilo
45c3ab2142 wip 2023-10-03 13:35:31 +09:00
syuilo
58eec94250 wip 2023-10-03 11:03:23 +09:00
syuilo
b66df850e5 Update timelines.ts 2023-10-03 10:58:10 +09:00
syuilo
72d5b1f4ae Update timelines.ts 2023-10-03 10:44:09 +09:00
syuilo
15caa375a5 Update misskey-js.api.md 2023-10-03 10:37:50 +09:00
syuilo
0e302c69bd Update timelines.ts 2023-10-03 10:36:10 +09:00
syuilo
236eed94bb wip 2023-10-03 10:28:01 +09:00
syuilo
6d68cfd1e3 wip 2023-10-03 10:22:57 +09:00
syuilo
ea0d050b71 wip 2023-10-03 10:13:26 +09:00
syuilo
d6ff810560 Update timelines.ts 2023-10-03 09:55:44 +09:00
syuilo
aad48b4b24 Update timelines.ts 2023-10-03 09:42:37 +09:00
syuilo
2f00e4b2b1 Update timelines.ts 2023-10-03 09:38:32 +09:00
syuilo
152047ca14 wip 2023-10-03 09:29:20 +09:00
syuilo
58d2512d0e Update timeline.ts 2023-10-03 09:28:37 +09:00
syuilo
bbcda73af8 Update timeline.ts 2023-10-03 09:24:11 +09:00
syuilo
880448d068 Update timeline.ts 2023-10-02 21:39:40 +09:00
syuilo
55e5056216 Update timeline.ts 2023-10-02 21:34:02 +09:00
syuilo
d40f35b3ad Update timeline.ts 2023-10-02 21:25:57 +09:00
syuilo
8843669684 wip 2023-10-02 21:18:05 +09:00
syuilo
c7c4c7807a Update NoteCreateService.ts 2023-10-02 18:04:34 +09:00
syuilo
79e5075564 wip 2023-10-02 18:02:25 +09:00
syuilo
caca0da912 wip 2023-10-02 17:23:01 +09:00
syuilo
35e743c955 wip 2023-10-02 15:37:09 +09:00
syuilo
6f17993cba Update user-notes.ts 2023-10-02 13:24:51 +09:00
syuilo
e4de402ca1 wip 2023-10-02 12:59:03 +09:00
syuilo
0db117b0ab Update NoteCreateService.ts 2023-10-02 10:45:48 +09:00
syuilo
cb821d42a6 wip 2023-10-02 10:42:26 +09:00
syuilo
b4c1de11f5 Update NoteCreateService.ts 2023-10-02 10:38:42 +09:00
syuilo
3924a9e494 wip 2023-10-02 08:31:33 +09:00
syuilo
d9aac112d3 wip 2023-10-02 08:21:34 +09:00
syuilo
7f4c00541c wip 2023-10-02 08:15:21 +09:00
syuilo
85430fd889 wip 2023-10-02 08:04:06 +09:00
syuilo
f0a2c3ce76 Update NoteCreateService.ts 2023-10-02 02:34:04 +09:00
syuilo
c019e9cad5 wip 2023-10-02 02:27:28 +09:00
syuilo
167aaabf20 wip 2023-10-02 02:21:18 +09:00
syuilo
72f7413f40 wip 2023-10-01 21:11:12 +09:00
syuilo
783a97fe06 wip 2023-10-01 20:38:18 +09:00
syuilo
3dd3c69303 wip 2023-10-01 20:25:51 +09:00
syuilo
06cfe618bb wip 2023-10-01 20:12:46 +09:00
41 changed files with 307 additions and 489 deletions

View File

@@ -15,23 +15,21 @@
## 2023.10.0
### NOTE
- muted_noteテーブルは使われなくなったため手動で削除を行ってください。
- 2023.9.2で導入されたノート編集機能はクオリティの高い実装が困難であることが判明したため撤回されました
### Changes
- API: users/notes, notes/local-timeline で fileType 指定はできなくなりました
- API: notes/global-timeline は現在常に `[]` を返します
### General
- Feat: ユーザーごとに他ユーザーへの返信をタイムラインに含めるか設定可能になりました
- Feat: ユーザーリスト内のメンバーごとに他ユーザーへの返信をユーザーリストタイムラインに含めるか設定可能になりました
- Enhance: ソフトワードミュートとハードワードミュートは統合されました
- ユーザーごとに他ユーザーへの返信をタイムラインに含めるか設定可能になりました
- ユーザーリスト内のメンバーごとに他ユーザーへの返信をユーザーリストタイムラインに含めるか設定可能になりました
- ソフトワードミュートとハードワードミュートは統合されました
### Client
- Enhance: 二要素認証のバックアップコード一覧をテキストファイルでダウンロード可能に
- Fix: リアクションしたユーザ一覧のUIが稀に左上に残ってしまう不具合を修正
### Server
- Enhance: タイムライン取得時のパフォーマンスを改善
- タイムライン取得時のパフォーマンスを改善
## 2023.9.3
### General

2
locales/index.d.ts vendored
View File

@@ -1131,7 +1131,6 @@ export interface Locale {
"fileAttachedOnly": string;
"showRepliesToOthersInTimeline": string;
"hideRepliesToOthersInTimeline": string;
"externalServices": string;
"_announcement": {
"forExistingUsers": string;
"forExistingUsersDescription": string;
@@ -1545,6 +1544,7 @@ export interface Locale {
"gtlAvailable": string;
"ltlAvailable": string;
"canPublicNote": string;
"canEditNote": string;
"canInvite": string;
"inviteLimit": string;
"inviteLimitCycle": string;

View File

@@ -1128,7 +1128,6 @@ mutualFollow: "相互フォロー"
fileAttachedOnly: "ファイル付きのみ"
showRepliesToOthersInTimeline: "TLに他の人への返信を含める"
hideRepliesToOthersInTimeline: "TLに他の人への返信を含めない"
externalServices: "外部サービス"
_announcement:
forExistingUsers: "既存ユーザーのみ"
@@ -1466,6 +1465,7 @@ _role:
gtlAvailable: "グローバルタイムラインの閲覧"
ltlAvailable: "ローカルタイムラインの閲覧"
canPublicNote: "パブリック投稿の許可"
canEditNote: "ノートの編集"
canInvite: "サーバー招待コードの発行"
inviteLimit: "招待コードの作成可能数"
inviteLimitCycle: "招待コードの発行間隔"

View File

@@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2023.10.0-beta.2",
"version": "2023.9.3",
"codename": "nasubi",
"repository": {
"type": "git",

View File

@@ -1,33 +0,0 @@
export class Clean1696332072038 {
name = 'Clean1696332072038'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_list_membership" DROP CONSTRAINT "FK_d844bfc6f3f523a05189076efaa"`);
await queryRunner.query(`ALTER TABLE "user_list_membership" DROP CONSTRAINT "FK_605472305f26818cc93d1baaa74"`);
await queryRunner.query(`DROP INDEX "public"."IDX_d844bfc6f3f523a05189076efa"`);
await queryRunner.query(`DROP INDEX "public"."IDX_605472305f26818cc93d1baaa7"`);
await queryRunner.query(`DROP INDEX "public"."IDX_90f7da835e4c10aca6853621e1"`);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "preservedUsernames" SET DEFAULT '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }'`);
await queryRunner.query(`COMMENT ON COLUMN "user_list_membership"."createdAt" IS 'The created date of the UserListMembership.'`);
await queryRunner.query(`CREATE INDEX "IDX_021015e6683570ae9f6b0c62be" ON "user_list_membership" ("userId") `);
await queryRunner.query(`CREATE INDEX "IDX_cddcaf418dc4d392ecfcca842a" ON "user_list_membership" ("userListId") `);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_e4f3094c43f2d665e6030b0337" ON "user_list_membership" ("userId", "userListId") `);
await queryRunner.query(`ALTER TABLE "user_list_membership" ADD CONSTRAINT "FK_021015e6683570ae9f6b0c62bee" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "user_list_membership" ADD CONSTRAINT "FK_cddcaf418dc4d392ecfcca842a7" FOREIGN KEY ("userListId") REFERENCES "user_list"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_list_membership" DROP CONSTRAINT "FK_cddcaf418dc4d392ecfcca842a7"`);
await queryRunner.query(`ALTER TABLE "user_list_membership" DROP CONSTRAINT "FK_021015e6683570ae9f6b0c62bee"`);
await queryRunner.query(`DROP INDEX "public"."IDX_e4f3094c43f2d665e6030b0337"`);
await queryRunner.query(`DROP INDEX "public"."IDX_cddcaf418dc4d392ecfcca842a"`);
await queryRunner.query(`DROP INDEX "public"."IDX_021015e6683570ae9f6b0c62be"`);
await queryRunner.query(`COMMENT ON COLUMN "user_list_membership"."createdAt" IS 'The created date of the UserListJoining.'`);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "preservedUsernames" SET DEFAULT '{admin,administrator,root,system,maintainer,host,mod,moderator,owner,superuser,staff,auth,i,me,everyone,all,mention,mentions,example,user,users,account,accounts,official,help,helps,support,supports,info,information,informations,announce,announces,announcement,announcements,notice,notification,notifications,dev,developer,developers,tech,misskey}'`);
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_90f7da835e4c10aca6853621e1" ON "user_list_membership" ("userId", "userListId") `);
await queryRunner.query(`CREATE INDEX "IDX_605472305f26818cc93d1baaa7" ON "user_list_membership" ("userListId") `);
await queryRunner.query(`CREATE INDEX "IDX_d844bfc6f3f523a05189076efa" ON "user_list_membership" ("userId") `);
await queryRunner.query(`ALTER TABLE "user_list_membership" ADD CONSTRAINT "FK_605472305f26818cc93d1baaa74" FOREIGN KEY ("userListId") REFERENCES "user_list"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "user_list_membership" ADD CONSTRAINT "FK_d844bfc6f3f523a05189076efaa" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
}

View File

@@ -1,22 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class MetaCacheSettings1696373953614 {
name = 'MetaCacheSettings1696373953614'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "perLocalUserUserTimelineCacheMax" integer NOT NULL DEFAULT '300'`);
await queryRunner.query(`ALTER TABLE "meta" ADD "perRemoteUserUserTimelineCacheMax" integer NOT NULL DEFAULT '100'`);
await queryRunner.query(`ALTER TABLE "meta" ADD "perUserHomeTimelineCacheMax" integer NOT NULL DEFAULT '300'`);
await queryRunner.query(`ALTER TABLE "meta" ADD "perUserListTimelineCacheMax" integer NOT NULL DEFAULT '300'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "perUserListTimelineCacheMax"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "perUserHomeTimelineCacheMax"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "perRemoteUserUserTimelineCacheMax"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "perLocalUserUserTimelineCacheMax"`);
}
}

View File

@@ -1,16 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class RevertNoteEdit1696388600237 {
name = 'RevertNoteEdit1696388600237'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "updatedAt"`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" ADD "updatedAt" TIMESTAMP WITH TIME ZONE`);
}
}

View File

@@ -481,10 +481,12 @@ export class NoteCreateService implements OnApplicationShutdown {
// Increment notes count (user)
this.incNotesCountOfUser(user);
if (data.visibility === 'specified') {
// TODO?
} else {
if (data.visibility === 'public' || data.visibility === 'home') {
this.pushToTl(note, user);
} else if (data.visibility === 'followers') {
this.pushToTl(note, user);
} else if (data.visibility === 'specified') {
// TODO
}
this.antennaService.addNoteToAntennas(note, user);
@@ -801,8 +803,6 @@ export class NoteCreateService implements OnApplicationShutdown {
@bindThis
private async pushToTl(note: MiNote, user: { id: MiUser['id']; host: MiUser['host']; }) {
const meta = await this.metaService.fetch();
const redisPipeline = this.redisForTimelines.pipeline();
if (note.channelId) {
@@ -816,14 +816,14 @@ export class NoteCreateService implements OnApplicationShutdown {
for (const channelFollowing of channelFollowings) {
redisPipeline.xadd(
`homeTimeline:${channelFollowing.followerId}`,
'MAXLEN', '~', meta.perUserHomeTimelineCacheMax.toString(),
'MAXLEN', '~', '200',
'*',
'note', note.id);
if (note.fileIds.length > 0) {
redisPipeline.xadd(
`homeTimelineWithFiles:${channelFollowing.followerId}`,
'MAXLEN', '~', (meta.perUserHomeTimelineCacheMax / 2).toString(),
'MAXLEN', '~', '100',
'*',
'note', note.id);
}
@@ -855,14 +855,14 @@ export class NoteCreateService implements OnApplicationShutdown {
redisPipeline.xadd(
`homeTimeline:${following.followerId}`,
'MAXLEN', '~', meta.perUserHomeTimelineCacheMax.toString(),
'MAXLEN', '~', '200',
'*',
'note', note.id);
if (note.fileIds.length > 0) {
redisPipeline.xadd(
`homeTimelineWithFiles:${following.followerId}`,
'MAXLEN', '~', (meta.perUserHomeTimelineCacheMax / 2).toString(),
'MAXLEN', '~', '100',
'*',
'note', note.id);
}
@@ -882,14 +882,14 @@ export class NoteCreateService implements OnApplicationShutdown {
redisPipeline.xadd(
`userListTimeline:${userListMembership.userListId}`,
'MAXLEN', '~', meta.perUserListTimelineCacheMax.toString(),
'MAXLEN', '~', '200',
'*',
'note', note.id);
if (note.fileIds.length > 0) {
redisPipeline.xadd(
`userListTimelineWithFiles:${userListMembership.userListId}`,
'MAXLEN', '~', (meta.perUserListTimelineCacheMax / 2).toString(),
'MAXLEN', '~', '100',
'*',
'note', note.id);
}
@@ -898,55 +898,57 @@ export class NoteCreateService implements OnApplicationShutdown {
{ // 自分自身のHTL
redisPipeline.xadd(
`homeTimeline:${user.id}`,
'MAXLEN', '~', meta.perUserHomeTimelineCacheMax.toString(),
'MAXLEN', '~', '200',
'*',
'note', note.id);
if (note.fileIds.length > 0) {
redisPipeline.xadd(
`homeTimelineWithFiles:${user.id}`,
'MAXLEN', '~', (meta.perUserHomeTimelineCacheMax / 2).toString(),
'MAXLEN', '~', '100',
'*',
'note', note.id);
}
}
// 自分自身以外への返信
if (note.replyId && note.replyUserId !== note.userId) {
redisPipeline.xadd(
`userTimelineWithReplies:${user.id}`,
'MAXLEN', '~', note.userHost == null ? meta.perLocalUserUserTimelineCacheMax.toString() : meta.perRemoteUserUserTimelineCacheMax.toString(),
'*',
'note', note.id);
} else {
redisPipeline.xadd(
`userTimeline:${user.id}`,
'MAXLEN', '~', note.userHost == null ? meta.perLocalUserUserTimelineCacheMax.toString() : meta.perRemoteUserUserTimelineCacheMax.toString(),
'*',
'note', note.id);
if (note.fileIds.length > 0) {
if (note.visibility === 'public' || note.visibility === 'home') {
// 自分自身以外への返信
if (note.replyId && note.replyUserId !== note.userId) {
redisPipeline.xadd(
`userTimelineWithFiles:${user.id}`,
'MAXLEN', '~', note.userHost == null ? (meta.perLocalUserUserTimelineCacheMax / 2).toString() : (meta.perRemoteUserUserTimelineCacheMax / 2).toString(),
`userTimelineWithReplies:${user.id}`,
'MAXLEN', '~', '1000',
'*',
'note', note.id);
}
if (note.visibility === 'public' && note.userHost == null) {
} else {
redisPipeline.xadd(
'localTimeline',
`userTimeline:${user.id}`,
'MAXLEN', '~', '1000',
'*',
'note', note.id);
if (note.fileIds.length > 0) {
redisPipeline.xadd(
'localTimelineWithFiles',
`userTimelineWithFiles:${user.id}`,
'MAXLEN', '~', '500',
'*',
'note', note.id);
}
if (note.visibility === 'public' && note.userHost == null) {
redisPipeline.xadd(
'localTimeline',
'MAXLEN', '~', '1000',
'*',
'note', note.id);
if (note.fileIds.length > 0) {
redisPipeline.xadd(
'localTimelineWithFiles',
'MAXLEN', '~', '500',
'*',
'note', note.id);
}
}
}
}

View File

@@ -26,6 +26,7 @@ export type RolePolicies = {
gtlAvailable: boolean;
ltlAvailable: boolean;
canPublicNote: boolean;
canEditNote: boolean;
canInvite: boolean;
inviteLimit: number;
inviteLimitCycle: number;
@@ -51,6 +52,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
gtlAvailable: true,
ltlAvailable: true,
canPublicNote: true,
canEditNote: true,
canInvite: false,
inviteLimit: 0,
inviteLimitCycle: 60 * 24 * 7,
@@ -296,6 +298,7 @@ export class RoleService implements OnApplicationShutdown {
gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)),
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
canEditNote: calc('canEditNote', vs => vs.some(v => v === true)),
canInvite: calc('canInvite', vs => vs.some(v => v === true)),
inviteLimit: calc('inviteLimit', vs => Math.max(...vs)),
inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)),

View File

@@ -308,6 +308,7 @@ export class NoteEntityService implements OnModuleInit {
const packed: Packed<'Note'> = await awaitAll({
id: note.id,
createdAt: note.createdAt.toISOString(),
updatedAt: note.updatedAt ? note.updatedAt.toISOString() : undefined,
userId: note.userId,
user: this.userEntityService.pack(note.user ?? note.userId, me, {
detail: false,

View File

@@ -471,24 +471,4 @@ export class MiMeta {
length: 1024, array: true, default: '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }',
})
public preservedUsernames: string[];
@Column('integer', {
default: 300,
})
public perLocalUserUserTimelineCacheMax: number;
@Column('integer', {
default: 100,
})
public perRemoteUserUserTimelineCacheMax: number;
@Column('integer', {
default: 300,
})
public perUserHomeTimelineCacheMax: number;
@Column('integer', {
default: 300,
})
public perUserListTimelineCacheMax: number;
}

View File

@@ -24,6 +24,11 @@ export class MiNote {
})
public createdAt: Date;
@Column('timestamp with time zone', {
default: null,
})
public updatedAt: Date | null;
@Index()
@Column({
...id(),

View File

@@ -17,6 +17,11 @@ export const packedNoteSchema = {
optional: false, nullable: false,
format: 'date-time',
},
updatedAt: {
type: 'string',
optional: true, nullable: true,
format: 'date-time',
},
deletedAt: {
type: 'string',
optional: true, nullable: true,

View File

@@ -257,6 +257,7 @@ import * as ep___notes_clips from './endpoints/notes/clips.js';
import * as ep___notes_conversation from './endpoints/notes/conversation.js';
import * as ep___notes_create from './endpoints/notes/create.js';
import * as ep___notes_delete from './endpoints/notes/delete.js';
import * as ep___notes_update from './endpoints/notes/update.js';
import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js';
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
import * as ep___notes_featured from './endpoints/notes/featured.js';
@@ -606,6 +607,7 @@ const $notes_clips: Provider = { provide: 'ep:notes/clips', useClass: ep___notes
const $notes_conversation: Provider = { provide: 'ep:notes/conversation', useClass: ep___notes_conversation.default };
const $notes_create: Provider = { provide: 'ep:notes/create', useClass: ep___notes_create.default };
const $notes_delete: Provider = { provide: 'ep:notes/delete', useClass: ep___notes_delete.default };
const $notes_update: Provider = { provide: 'ep:notes/update', useClass: ep___notes_update.default };
const $notes_favorites_create: Provider = { provide: 'ep:notes/favorites/create', useClass: ep___notes_favorites_create.default };
const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete', useClass: ep___notes_favorites_delete.default };
const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default };
@@ -959,6 +961,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$notes_conversation,
$notes_create,
$notes_delete,
$notes_update,
$notes_favorites_create,
$notes_favorites_delete,
$notes_featured,
@@ -1306,6 +1309,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$notes_conversation,
$notes_create,
$notes_delete,
$notes_update,
$notes_favorites_create,
$notes_favorites_delete,
$notes_featured,

View File

@@ -257,6 +257,7 @@ import * as ep___notes_clips from './endpoints/notes/clips.js';
import * as ep___notes_conversation from './endpoints/notes/conversation.js';
import * as ep___notes_create from './endpoints/notes/create.js';
import * as ep___notes_delete from './endpoints/notes/delete.js';
import * as ep___notes_update from './endpoints/notes/update.js';
import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js';
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
import * as ep___notes_featured from './endpoints/notes/featured.js';
@@ -604,6 +605,7 @@ const eps = [
['notes/conversation', ep___notes_conversation],
['notes/create', ep___notes_create],
['notes/delete', ep___notes_delete],
['notes/update', ep___notes_update],
['notes/favorites/create', ep___notes_favorites_create],
['notes/favorites/delete', ep___notes_favorites_delete],
['notes/featured', ep___notes_featured],

View File

@@ -105,32 +105,40 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
userStarForReactionFallback: {
type: 'boolean',
optional: true, nullable: false,
},
pinnedUsers: {
type: 'array',
optional: false, nullable: false,
optional: true, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
hiddenTags: {
type: 'array',
optional: false, nullable: false,
optional: true, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
blockedHosts: {
type: 'array',
optional: false, nullable: false,
optional: true, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
sensitiveWords: {
type: 'array',
optional: false, nullable: false,
optional: true, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
preservedUsernames: {
@@ -138,124 +146,129 @@ export const meta = {
optional: false, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
hcaptchaSecretKey: {
type: 'string',
optional: false, nullable: true,
optional: true, nullable: true,
},
recaptchaSecretKey: {
type: 'string',
optional: false, nullable: true,
optional: true, nullable: true,
},
turnstileSecretKey: {
type: 'string',
optional: false, nullable: true,
optional: true, nullable: true,
},
sensitiveMediaDetection: {
type: 'string',
optional: false, nullable: false,
optional: true, nullable: false,
},
sensitiveMediaDetectionSensitivity: {
type: 'string',
optional: false, nullable: false,
optional: true, nullable: false,
},
setSensitiveFlagAutomatically: {
type: 'boolean',
optional: false, nullable: false,
optional: true, nullable: false,
},
enableSensitiveMediaDetectionForVideos: {
type: 'boolean',
optional: false, nullable: false,
optional: true, nullable: false,
},
proxyAccountId: {
type: 'string',
optional: false, nullable: true,
optional: true, nullable: true,
format: 'id',
},
summaryProxy: {
type: 'string',
optional: true, nullable: true,
},
email: {
type: 'string',
optional: false, nullable: true,
optional: true, nullable: true,
},
smtpSecure: {
type: 'boolean',
optional: false, nullable: false,
optional: true, nullable: false,
},
smtpHost: {
type: 'string',
optional: false, nullable: true,
optional: true, nullable: true,
},
smtpPort: {
type: 'number',
optional: false, nullable: true,
optional: true, nullable: true,
},
smtpUser: {
type: 'string',
optional: false, nullable: true,
optional: true, nullable: true,
},
smtpPass: {
type: 'string',
optional: false, nullable: true,
optional: true, nullable: true,
},
swPrivateKey: {
type: 'string',
optional: false, nullable: true,
optional: true, nullable: true,
},
useObjectStorage: {
type: 'boolean',
optional: false, nullable: false,
optional: true, nullable: false,
},
objectStorageBaseUrl: {
type: 'string',
optional: false, nullable: true,
optional: true, nullable: true,
},
objectStorageBucket: {
type: 'string',
optional: false, nullable: true,
optional: true, nullable: true,
},
objectStoragePrefix: {
type: 'string',
optional: false, nullable: true,
optional: true, nullable: true,
},
objectStorageEndpoint: {
type: 'string',
optional: false, nullable: true,
optional: true, nullable: true,
},
objectStorageRegion: {
type: 'string',
optional: false, nullable: true,
optional: true, nullable: true,
},
objectStoragePort: {
type: 'number',
optional: false, nullable: true,
optional: true, nullable: true,
},
objectStorageAccessKey: {
type: 'string',
optional: false, nullable: true,
optional: true, nullable: true,
},
objectStorageSecretKey: {
type: 'string',
optional: false, nullable: true,
optional: true, nullable: true,
},
objectStorageUseSSL: {
type: 'boolean',
optional: false, nullable: false,
optional: true, nullable: false,
},
objectStorageUseProxy: {
type: 'boolean',
optional: false, nullable: false,
optional: true, nullable: false,
},
objectStorageSetPublicRead: {
type: 'boolean',
optional: false, nullable: false,
optional: true, nullable: false,
},
enableIpLogging: {
type: 'boolean',
optional: false, nullable: false,
optional: true, nullable: false,
},
enableActiveEmailValidation: {
type: 'boolean',
optional: false, nullable: false,
optional: true, nullable: false,
},
enableChartsForRemoteUser: {
type: 'boolean',
@@ -275,28 +288,12 @@ export const meta = {
},
manifestJsonOverride: {
type: 'string',
optional: false, nullable: false,
optional: true, nullable: false,
},
policies: {
type: 'object',
optional: false, nullable: false,
},
perLocalUserUserTimelineCacheMax: {
type: 'number',
optional: false, nullable: false,
},
perRemoteUserUserTimelineCacheMax: {
type: 'number',
optional: false, nullable: false,
},
perUserHomeTimelineCacheMax: {
type: 'number',
optional: false, nullable: false,
},
perUserListTimelineCacheMax: {
type: 'number',
optional: false, nullable: false,
},
},
},
} as const;
@@ -316,7 +313,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private metaService: MetaService,
) {
super(meta, paramDef, async () => {
super(meta, paramDef, async (ps, me) => {
const instance = await this.metaService.fetch(true);
return {
@@ -402,10 +399,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
enableIdenticonGeneration: instance.enableIdenticonGeneration,
policies: { ...DEFAULT_POLICIES, ...instance.policies },
manifestJsonOverride: instance.manifestJsonOverride,
perLocalUserUserTimelineCacheMax: instance.perLocalUserUserTimelineCacheMax,
perRemoteUserUserTimelineCacheMax: instance.perRemoteUserUserTimelineCacheMax,
perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax,
perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax,
};
});
}

View File

@@ -108,10 +108,6 @@ export const paramDef = {
serverRules: { type: 'array', items: { type: 'string' } },
preservedUsernames: { type: 'array', items: { type: 'string' } },
manifestJsonOverride: { type: 'string' },
perLocalUserUserTimelineCacheMax: { type: 'integer' },
perRemoteUserUserTimelineCacheMax: { type: 'integer' },
perUserHomeTimelineCacheMax: { type: 'integer' },
perUserListTimelineCacheMax: { type: 'integer' },
},
required: [],
} as const;
@@ -445,22 +441,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.manifestJsonOverride = ps.manifestJsonOverride;
}
if (ps.perLocalUserUserTimelineCacheMax !== undefined) {
set.perLocalUserUserTimelineCacheMax = ps.perLocalUserUserTimelineCacheMax;
}
if (ps.perRemoteUserUserTimelineCacheMax !== undefined) {
set.perRemoteUserUserTimelineCacheMax = ps.perRemoteUserUserTimelineCacheMax;
}
if (ps.perUserHomeTimelineCacheMax !== undefined) {
set.perUserHomeTimelineCacheMax = ps.perUserHomeTimelineCacheMax;
}
if (ps.perUserListTimelineCacheMax !== undefined) {
set.perUserListTimelineCacheMax = ps.perUserListTimelineCacheMax;
}
const before = await this.metaService.fetch(true);
await this.metaService.update(set);

View File

@@ -214,11 +214,11 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
localTimeline: {
localTimeLine: {
type: 'boolean',
optional: false, nullable: false,
},
globalTimeline: {
globalTimeLine: {
type: 'boolean',
optional: false, nullable: false,
},

View File

@@ -96,18 +96,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
let ltlNoteIdsRes: [string, string[]][] = [];
if (!ps.sinceId && !ps.sinceDate) {
[htlNoteIdsRes, ltlNoteIdsRes] = await Promise.all([
this.redisForTimelines.xrevrange(
ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`,
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
'-',
'COUNT', limit),
this.redisForTimelines.xrevrange(
ps.withFiles ? 'localTimelineWithFiles' : 'localTimeline',
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
'-',
'COUNT', limit),
]);
htlNoteIdsRes = await this.redisForTimelines.xrevrange(
ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`,
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
'-',
'COUNT', limit);
ltlNoteIdsRes = await this.redisForTimelines.xrevrange(
ps.withFiles ? 'localTimelineWithFiles' : 'localTimeline',
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
'-',
'COUNT', limit);
}
const htlNoteIds = htlNoteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);

View File

@@ -0,0 +1,89 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import ms from 'ms';
import { Inject, Injectable } from '@nestjs/common';
import type { UsersRepository, NotesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteDeleteService } from '@/core/NoteDeleteService.js';
import { DI } from '@/di-symbols.js';
import { GetterService } from '@/server/api/GetterService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['notes'],
requireCredential: true,
requireRolePolicy: 'canEditNote',
kind: 'write:notes',
limit: {
duration: ms('1hour'),
max: 10,
minInterval: ms('1sec'),
},
errors: {
noSuchNote: {
message: 'No such note.',
code: 'NO_SUCH_NOTE',
id: 'a6584e14-6e01-4ad3-b566-851e7bf0d474',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
noteId: { type: 'string', format: 'misskey:id' },
text: {
type: 'string',
minLength: 1,
maxLength: MAX_NOTE_TEXT_LENGTH,
nullable: false,
},
cw: { type: 'string', nullable: true, maxLength: 100 },
},
required: ['noteId', 'text', 'cw'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
private getterService: GetterService,
private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
const note = await this.getterService.getNote(ps.noteId).catch(err => {
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
throw err;
});
if (note.userId !== me.id) {
throw new ApiError(meta.errors.noSuchNote);
}
await this.notesRepository.update({ id: note.id }, {
updatedAt: new Date(),
cw: ps.cw,
text: ps.text,
});
this.globalEventService.publishNoteStream(note.id, 'updated', {
cw: ps.cw,
text: ps.text,
});
});
}
}

View File

@@ -18,6 +18,8 @@ import { ApiError } from '../../error.js';
export const meta = {
tags: ['users', 'notes'],
description: 'Show all notes that this user created.',
res: {
type: 'array',
optional: false, nullable: false,
@@ -74,31 +76,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
let noteIdsRes: [string, string[]][] = [];
let repliesNoteIdsRes: [string, string[]][] = [];
if (!ps.sinceId && !ps.sinceDate) {
[noteIdsRes, repliesNoteIdsRes] = await Promise.all([
this.redisForTimelines.xrevrange(
ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : `userTimeline:${ps.userId}`,
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
'-',
'COUNT', limit),
ps.withReplies
? this.redisForTimelines.xrevrange(
`userTimelineWithReplies:${ps.userId}`,
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
'-',
'COUNT', limit)
: Promise.resolve([]),
]);
noteIdsRes = await this.redisForTimelines.xrevrange(
ps.withFiles ? `userTimelineWithFiles:${ps.userId}` : ps.withReplies ? `userTimelineWithReplies:${ps.userId}` : `userTimeline:${ps.userId}`,
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+',
'-',
'COUNT', limit);
}
let noteIds = Array.from(new Set([
...noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId),
...repliesNoteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId),
]));
noteIds.sort((a, b) => a > b ? -1 : 1);
noteIds = noteIds.slice(0, ps.limit);
const noteIds = noteIdsRes.map(x => x[1][1]).filter(x => x !== ps.untilId);
if (noteIds.length === 0) {
return [];

View File

@@ -7,14 +7,10 @@ process.env.NODE_ENV = 'test';
process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING = 'true';
import * as assert from 'assert';
import { signup, api, post, react, startServer, waitFire, sleep, uploadUrl, randomString } from '../utils.js';
import { signup, api, post, react, startServer, waitFire, sleep, uploadUrl } from '../utils.js';
import type { INestApplicationContext } from '@nestjs/common';
import type * as misskey from 'misskey-js';
function genHost() {
return randomString() + '.example.com';
}
let app: INestApplicationContext;
beforeAll(async () => {
@@ -294,7 +290,7 @@ describe('Timelines', () => {
});
test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
const [alice, bob] = await Promise.all([signup(), signup({ host: 'example.com' })]);
await api('/following/create', { userId: bob.id }, alice);
const bobNote = await post(bob, { text: 'hi' });
@@ -307,7 +303,7 @@ describe('Timelines', () => {
});
test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
const [alice, bob] = await Promise.all([signup(), signup({ host: 'example.com' })]);
await api('/following/create', { userId: bob.id }, alice);
const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
@@ -359,7 +355,7 @@ describe('Timelines', () => {
});
test.concurrent('リモートユーザーのノートが含まれない', async () => {
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
const [alice, bob] = await Promise.all([signup(), signup({ host: 'example.com' })]);
const bobNote = await post(bob, { text: 'hi' });
@@ -491,7 +487,7 @@ describe('Timelines', () => {
});
test.concurrent('リモートユーザーのノートが含まれない', async () => {
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
const [alice, bob] = await Promise.all([signup(), signup({ host: 'example.com' })]);
const bobNote = await post(bob, { text: 'hi' });
@@ -503,7 +499,7 @@ describe('Timelines', () => {
});
test.concurrent('フォローしているリモートユーザーのノートが含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
const [alice, bob] = await Promise.all([signup(), signup({ host: 'example.com' })]);
await api('/following/create', { userId: bob.id }, alice);
const bobNote = await post(bob, { text: 'hi' });
@@ -516,7 +512,7 @@ describe('Timelines', () => {
});
test.concurrent('フォローしているリモートユーザーの visibility: home なノートが含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup({ host: genHost() })]);
const [alice, bob] = await Promise.all([signup(), signup({ host: 'example.com' })]);
await api('/following/create', { userId: bob.id }, alice);
const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
@@ -700,92 +696,6 @@ describe('Timelines', () => {
}, 1000 * 10);
});
describe('User TL', () => {
test.concurrent('ノートが含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
const bobNote = await post(bob, { text: 'hi' });
await sleep(100); // redisに追加されるのを待つ
const res = await api('/users/notes', { userId: bob.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
});
test.concurrent('フォローしていないユーザーの visibility: followers なノートが含まれない', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
await sleep(100); // redisに追加されるのを待つ
const res = await api('/users/notes', { userId: bob.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false);
});
test.concurrent('フォローしているユーザーの visibility: followers なノートが含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
await api('/following/create', { userId: bob.id }, alice);
await sleep(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
await sleep(100); // redisに追加されるのを待つ
const res = await api('/users/notes', { userId: bob.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), true);
assert.strictEqual(res.body.find((note: any) => note.id === bobNote.id).text, 'hi');
});
test.concurrent('[withReplies: false] 他人への返信が含まれない', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
const carolNote = await post(carol, { text: 'hi' });
const bobNote1 = await post(bob, { text: 'hi' });
const bobNote2 = await post(bob, { text: 'hi', replyId: carolNote.id });
await sleep(100); // redisに追加されるのを待つ
const res = await api('/users/notes', { userId: bob.id }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), false);
});
test.concurrent('[withReplies: true] 他人への返信が含まれる', async () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
const carolNote = await post(carol, { text: 'hi' });
const bobNote1 = await post(bob, { text: 'hi' });
const bobNote2 = await post(bob, { text: 'hi', replyId: carolNote.id });
await sleep(100); // redisに追加されるのを待つ
const res = await api('/users/notes', { userId: bob.id, withReplies: true }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), true);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
});
test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]);
const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png');
const bobNote1 = await post(bob, { text: 'hi' });
const bobNote2 = await post(bob, { fileIds: [file.id] });
await sleep(100); // redisに追加されるのを待つ
const res = await api('/users/notes', { userId: bob.id, withFiles: true }, alice);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote1.id), false);
assert.strictEqual(res.body.some((note: any) => note.id === bobNote2.id), true);
}, 1000 * 10);
});
// TODO: リノートミュート済みユーザーのテスト
// TODO: ページネーションのテスト
});

View File

@@ -99,7 +99,7 @@ export const relativeFetch = async (path: string, init?: RequestInit | undefined
return await fetch(new URL(path, `http://127.0.0.1:${port}/`).toString(), init);
};
export function randomString(chars = 'abcdefghijklmnopqrstuvwxyz0123456789', length = 16) {
function randomString(chars = 'abcdefghijklmnopqrstuvwxyz0123456789', length = 16) {
let randomString = '';
for (let i = 0; i < length; i++) {
randomString += chars[Math.floor(Math.random() * chars.length)];

View File

@@ -93,6 +93,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<footer>
<div :class="$style.noteFooterInfo">
<div v-if="appearNote.updatedAt">
{{ i18n.ts.edited }}: <MkTime :time="appearNote.updatedAt" mode="detail"/>
</div>
<MkA :to="notePage(appearNote)">
<MkTime :time="appearNote.createdAt" mode="detail"/>
</MkA>

View File

@@ -14,6 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<img v-for="role in note.user.badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl"/>
</div>
<div :class="$style.info">
<span v-if="note.updatedAt" style="margin-right: 0.5em;" :title="i18n.ts.edited"><i class="ti ti-pencil"></i></span>
<MkA :to="notePage(note)">
<MkTime :time="note.createdAt"/>
</MkA>

View File

@@ -143,6 +143,7 @@ const props = withDefaults(defineProps<{
fixed?: boolean;
autofocus?: boolean;
freezeAfterPosted?: boolean;
updateMode?: boolean;
}>(), {
initialVisibleUsers: () => [],
autofocus: true,
@@ -709,6 +710,7 @@ async function post(ev?: MouseEvent) {
visibility: visibility,
visibleUserIds: visibility === 'specified' ? visibleUsers.map(u => u.id) : undefined,
reactionAcceptance,
noteId: props.updateMode ? props.initialNote?.id : undefined,
};
if (withHashtags && hashtags && hashtags.trim() !== '') {
@@ -731,7 +733,7 @@ async function post(ev?: MouseEvent) {
}
posting = true;
os.api('notes/create', postData, token).then(() => {
os.api(props.updateMode ? 'notes/update' : 'notes/create', postData, token).then(() => {
if (props.freezeAfterPosted) {
posted = true;
} else {

View File

@@ -30,6 +30,7 @@ const props = defineProps<{
instant?: boolean;
fixed?: boolean;
autofocus?: boolean;
updateMode?: boolean;
}>();
const emit = defineEmits<{

View File

@@ -61,6 +61,7 @@ export const ROLE_POLICIES = [
'gtlAvailable',
'ltlAvailable',
'canPublicNote',
'canEditNote',
'canInvite',
'inviteLimit',
'inviteLimitCycle',

View File

@@ -1,81 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkStickyContainer>
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
<FormSuspense :p="init">
<FormSection>
<template #label>DeepL Translation</template>
<div class="_gaps_m">
<MkInput v-model="deeplAuthKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>DeepL Auth Key</template>
</MkInput>
<MkSwitch v-model="deeplIsPro">
<template #label>Pro account</template>
</MkSwitch>
</div>
</FormSection>
</FormSuspense>
</MkSpacer>
<template #footer>
<div :class="$style.footer">
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="16">
<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
</MkSpacer>
</div>
</template>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { } from 'vue';
import XHeader from './_header_.vue';
import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue';
import FormSuspense from '@/components/form/suspense.vue';
import FormSection from '@/components/form/section.vue';
import * as os from '@/os.js';
import { fetchInstance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
let deeplAuthKey: string = $ref('');
let deeplIsPro: boolean = $ref(false);
async function init() {
const meta = await os.api('admin/meta');
deeplAuthKey = meta.deeplAuthKey;
deeplIsPro = meta.deeplIsPro;
}
function save() {
os.apiWithDialog('admin/update-meta', {
deeplAuthKey,
deeplIsPro,
}).then(() => {
fetchInstance();
});
}
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.instanceBlocking,
icon: 'ti ti-ban',
});
</script>
<style lang="scss" module>
.footer {
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
}
</style>

View File

@@ -198,11 +198,6 @@ const menuDef = $computed(() => [{
text: i18n.ts.proxyAccount,
to: '/admin/proxy-account',
active: currentPage?.route.name === 'proxy-account',
}, {
icon: 'ti ti-link',
text: i18n.ts.externalServices,
to: '/admin/external-services',
active: currentPage?.route.name === 'external-services',
}, {
icon: 'ti ti-adjustments',
text: i18n.ts.other,

View File

@@ -160,6 +160,26 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canEditNote, 'canEditNote'])">
<template #label>{{ i18n.ts._role._options.canEditNote }}</template>
<template #suffix>
<span v-if="role.policies.canEditNote.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.canEditNote.value ? i18n.ts.yes : i18n.ts.no }}</span>
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canEditNote)"></i></span>
</template>
<div class="_gaps">
<MkSwitch v-model="role.policies.canEditNote.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkSwitch v-model="role.policies.canEditNote.value" :disabled="role.policies.canEditNote.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
<MkRange v-model="role.policies.canEditNote.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
<template #label>{{ i18n.ts._role.priority }}</template>
</MkRange>
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])">
<template #label>{{ i18n.ts._role._options.canInvite }}</template>
<template #suffix>

View File

@@ -48,6 +48,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canEditNote, 'canEditNote'])">
<template #label>{{ i18n.ts._role._options.canEditNote }}</template>
<template #suffix>{{ policies.canEditNote ? i18n.ts.yes : i18n.ts.no }}</template>
<MkSwitch v-model="policies.canEditNote">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])">
<template #label>{{ i18n.ts._role._options.canInvite }}</template>
<template #suffix>{{ policies.canInvite ? i18n.ts.yes : i18n.ts.no }}</template>

View File

@@ -81,24 +81,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</FormSection>
<FormSection>
<template #label>Timeline caching</template>
<template #label>DeepL Translation</template>
<div class="_gaps_m">
<MkInput v-model="perLocalUserUserTimelineCacheMax" type="number">
<template #label>perLocalUserUserTimelineCacheMax</template>
</MkInput>
<MkInput v-model="perRemoteUserUserTimelineCacheMax" type="number">
<template #label>perRemoteUserUserTimelineCacheMax</template>
</MkInput>
<MkInput v-model="perUserHomeTimelineCacheMax" type="number">
<template #label>perUserHomeTimelineCacheMax</template>
</MkInput>
<MkInput v-model="perUserListTimelineCacheMax" type="number">
<template #label>perUserListTimelineCacheMax</template>
<MkInput v-model="deeplAuthKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>DeepL Auth Key</template>
</MkInput>
<MkSwitch v-model="deeplIsPro">
<template #label>Pro account</template>
</MkSwitch>
</div>
</FormSection>
</div>
@@ -141,10 +133,8 @@ let cacheRemoteSensitiveFiles: boolean = $ref(false);
let enableServiceWorker: boolean = $ref(false);
let swPublicKey: any = $ref(null);
let swPrivateKey: any = $ref(null);
let perLocalUserUserTimelineCacheMax: number = $ref(0);
let perRemoteUserUserTimelineCacheMax: number = $ref(0);
let perUserHomeTimelineCacheMax: number = $ref(0);
let perUserListTimelineCacheMax: number = $ref(0);
let deeplAuthKey: string = $ref('');
let deeplIsPro: boolean = $ref(false);
async function init(): Promise<void> {
const meta = await os.api('admin/meta');
@@ -159,10 +149,8 @@ async function init(): Promise<void> {
enableServiceWorker = meta.enableServiceWorker;
swPublicKey = meta.swPublickey;
swPrivateKey = meta.swPrivateKey;
perLocalUserUserTimelineCacheMax = meta.perLocalUserUserTimelineCacheMax;
perRemoteUserUserTimelineCacheMax = meta.perRemoteUserUserTimelineCacheMax;
perUserHomeTimelineCacheMax = meta.perUserHomeTimelineCacheMax;
perUserListTimelineCacheMax = meta.perUserListTimelineCacheMax;
deeplAuthKey = meta.deeplAuthKey;
deeplIsPro = meta.deeplIsPro;
}
function save(): void {
@@ -178,10 +166,8 @@ function save(): void {
enableServiceWorker,
swPublicKey,
swPrivateKey,
perLocalUserUserTimelineCacheMax,
perRemoteUserUserTimelineCacheMax,
perUserHomeTimelineCacheMax,
perUserListTimelineCacheMax,
deeplAuthKey,
deeplIsPro,
}).then(() => {
fetchInstance();
});

View File

@@ -83,8 +83,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #value><code class="_monospace">{{ code }}</code></template>
</MkKeyValue>
</div>
<MkButton primary rounded gradate @click="downloadBackupCodes"><i class="ti ti-download"></i> {{ i18n.ts.download }}</MkButton>
</div>
</MkFolder>
</div>
@@ -110,7 +108,6 @@ import * as os from '@/os.js';
import MkFolder from '@/components/MkFolder.vue';
import MkInfo from '@/components/MkInfo.vue';
import { confetti } from '@/scripts/confetti.js';
import { $i } from '@/account.js';
defineProps<{
twoFactorData: {
@@ -146,16 +143,6 @@ async function tokenDone() {
});
}
function downloadBackupCodes() {
if (backupCodes.value !== undefined) {
const txtBlob = new Blob([backupCodes.value.join('\n')], { type: 'text/plain' });
const dummya = document.createElement('a');
dummya.href = URL.createObjectURL(txtBlob);
dummya.download = `${$i?.username}-2fa-backup-codes.txt`;
dummya.click();
}
}
function allDone() {
dialog.value.close();
}

View File

@@ -435,10 +435,6 @@ export const routes = [{
path: '/proxy-account',
name: 'proxy-account',
component: page(() => import('./pages/admin/proxy-account.vue')),
}, {
path: '/external-services',
name: 'external-services',
component: page(() => import('./pages/admin/external-services.vue')),
}, {
path: '/other-settings',
name: 'other-settings',

View File

@@ -172,6 +172,10 @@ export function getNoteMenu(props: {
});
}
function edit(): void {
os.post({ initialNote: appearNote, renote: appearNote.renote, reply: appearNote.reply, channel: appearNote.channel, updateMode: true });
}
function toggleFavorite(favorite: boolean): void {
claimAchievement('noteFavorited1');
os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', {
@@ -352,6 +356,11 @@ export function getNoteMenu(props: {
),
...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [
null,
appearNote.userId === $i.id && $i.policies.canEditNote ? {
icon: 'ti ti-edit',
text: i18n.ts.edit,
action: edit,
} : undefined,
appearNote.userId === $i.id ? {
icon: 'ti ti-edit',
text: i18n.ts.deleteAndEdit,

View File

@@ -71,6 +71,13 @@ export function useNoteCapture(props: {
break;
}
case 'updated': {
note.value.updatedAt = new Date().toISOString();
note.value.cw = body.cw;
note.value.text = body.text;
break;
}
case 'deleted': {
props.isDeletedRef.value = true;
break;

View File

@@ -9,12 +9,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ti ti-list"></i><span style="margin-left: 8px;">{{ column.name }}</span>
</template>
<MkTimeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" :withRenotes="withRenotes"/>
<MkTimeline v-if="column.listId" ref="timeline" src="list" :list="column.listId"/>
</XColumn>
</template>
<script lang="ts" setup>
import { watch } from 'vue';
import { } from 'vue';
import XColumn from './column.vue';
import { updateColumn, Column } from './deck-store';
import MkTimeline from '@/components/MkTimeline.vue';
@@ -27,18 +27,11 @@ const props = defineProps<{
}>();
let timeline = $shallowRef<InstanceType<typeof MkTimeline>>();
const withRenotes = $ref(props.column.withRenotes ?? true);
if (props.column.listId == null) {
setList();
}
watch($$(withRenotes), v => {
updateColumn(props.column.id, {
withRenotes: v,
});
});
async function setList() {
const lists = await os.api('users/lists/list');
const { canceled, result: list } = await os.select({
@@ -69,10 +62,5 @@ const menu = [
text: i18n.ts.editList,
action: editList,
},
{
type: 'switch',
text: i18n.ts.showRenotes,
ref: $$(withRenotes),
},
];
</script>

View File

@@ -2639,6 +2639,7 @@ export const mutedNoteReasons: readonly ["word", "manual", "spam", "other"];
type Note = {
id: ID;
createdAt: DateString;
updatedAt?: DateString | null;
text: string | null;
cw: string | null;
user: User;
@@ -2978,7 +2979,7 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u
// src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
// src/api.types.ts:630:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
// src/entities.ts:107:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts
// src/entities.ts:594:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
// src/entities.ts:595:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
// (No @packageDocumentation comment for this package)

View File

@@ -177,6 +177,7 @@ export type GalleryPost = {
export type Note = {
id: ID;
createdAt: DateString;
updatedAt?: DateString | null;
text: string | null;
cw: string | null;
user: User;

View File

@@ -133,6 +133,13 @@ export type NoteUpdatedEvent = {
body: {
deletedAt: string;
};
} | {
id: Note['id'];
type: 'updated';
body: {
cw: string | null;
text: string;
};
} | {
id: Note['id'];
type: 'pollVoted';