From f47a564819500d801c76fe327fec276b4bfa9989 Mon Sep 17 00:00:00 2001 From: MeiMei <30769358+mei23@users.noreply.github.com> Date: Sun, 31 Oct 2021 15:18:46 +0900 Subject: [PATCH 01/16] fix: Fix #7895 (#7937) * Fix #7895 * CHANGELOG --- CHANGELOG.md | 7 +++++++ src/remote/activitypub/renderer/delete.ts | 3 ++- src/remote/activitypub/renderer/undo.ts | 3 ++- src/remote/activitypub/renderer/update.ts | 3 ++- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13dccdc38b..bf62565b8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ --> +## 12.x.x (unreleased) + +### Improvements + +### Bugfixes +- リレー向けのActivityが一部実装で除外されてしまうことがあるのを修正 + ## 12.94.1 (2021/10/25) ### Improvements diff --git a/src/remote/activitypub/renderer/delete.ts b/src/remote/activitypub/renderer/delete.ts index 83b27fa866..176a6f7e27 100644 --- a/src/remote/activitypub/renderer/delete.ts +++ b/src/remote/activitypub/renderer/delete.ts @@ -4,5 +4,6 @@ import { User } from '@/models/entities/user'; export default (object: any, user: { id: User['id']; host: null }) => ({ type: 'Delete', actor: `${config.url}/users/${user.id}`, - object + object, + published: new Date().toISOString(), }); diff --git a/src/remote/activitypub/renderer/undo.ts b/src/remote/activitypub/renderer/undo.ts index f9082ffdfc..14115b788d 100644 --- a/src/remote/activitypub/renderer/undo.ts +++ b/src/remote/activitypub/renderer/undo.ts @@ -7,6 +7,7 @@ export default (object: any, user: { id: User['id'] }) => { return { type: 'Undo', actor: `${config.url}/users/${user.id}`, - object + object, + published: new Date().toISOString(), }; }; diff --git a/src/remote/activitypub/renderer/update.ts b/src/remote/activitypub/renderer/update.ts index d9a8149af3..8bb415d117 100644 --- a/src/remote/activitypub/renderer/update.ts +++ b/src/remote/activitypub/renderer/update.ts @@ -7,7 +7,8 @@ export default (object: any, user: { id: User['id'] }) => { actor: `${config.url}/users/${user.id}`, type: 'Update', to: [ 'https://www.w3.org/ns/activitystreams#Public' ], - object + object, + published: new Date().toISOString(), } as any; return activity; From fc65190ef7b687650018cccfee2219bf00827f70 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 31 Oct 2021 15:30:22 +0900 Subject: [PATCH 02/16] feat: thread mute (#7930) * feat: thread mute * chore: fix comment * fix test * fix * refactor --- CHANGELOG.md | 1 + locales/ja-JP.yml | 2 + migration/1635500777168-note-thread-mute.ts | 26 +++++ src/client/components/note-detailed.vue | 15 +++ src/client/components/note.vue | 15 +++ src/db/postgre.ts | 2 + src/models/entities/note-thread-muting.ts | 33 ++++++ src/models/entities/note.ts | 6 + src/models/index.ts | 2 + .../generate-muted-note-thread-query.ts | 17 +++ src/server/api/endpoints/notes/mentions.ts | 2 + src/server/api/endpoints/notes/state.ts | 28 +++-- .../endpoints/notes/thread-muting/create.ts | 54 +++++++++ .../endpoints/notes/thread-muting/delete.ts | 40 +++++++ src/services/note/create.ts | 29 ++++- src/services/note/unread.ts | 11 +- test/thread-mute.ts | 103 ++++++++++++++++++ test/utils.ts | 3 +- 18 files changed, 375 insertions(+), 14 deletions(-) create mode 100644 migration/1635500777168-note-thread-mute.ts create mode 100644 src/models/entities/note-thread-muting.ts create mode 100644 src/server/api/common/generate-muted-note-thread-query.ts create mode 100644 src/server/api/endpoints/notes/thread-muting/create.ts create mode 100644 src/server/api/endpoints/notes/thread-muting/delete.ts create mode 100644 test/thread-mute.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index bf62565b8c..ae652c310a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ## 12.x.x (unreleased) ### Improvements +- スレッドミュート機能 ### Bugfixes - リレー向けのActivityが一部実装で除外されてしまうことがあるのを修正 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index dbb0bf1664..1326369f83 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -800,6 +800,8 @@ manageAccounts: "アカウントを管理" makeReactionsPublic: "リアクション一覧を公開する" makeReactionsPublicDescription: "あなたがしたリアクション一覧を誰でも見れるようにします。" classic: "クラシック" +muteThread: "スレッドをミュート" +unmuteThread: "スレッドのミュートを解除" _signup: almostThere: "ほとんど完了です" diff --git a/migration/1635500777168-note-thread-mute.ts b/migration/1635500777168-note-thread-mute.ts new file mode 100644 index 0000000000..aed10d18d7 --- /dev/null +++ b/migration/1635500777168-note-thread-mute.ts @@ -0,0 +1,26 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class noteThreadMute1635500777168 implements MigrationInterface { + name = 'noteThreadMute1635500777168' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "note_thread_muting" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "threadId" character varying(256) NOT NULL, CONSTRAINT "PK_ec5936d94d1a0369646d12a3a47" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_29c11c7deb06615076f8c95b80" ON "note_thread_muting" ("userId") `); + await queryRunner.query(`CREATE INDEX "IDX_c426394644267453e76f036926" ON "note_thread_muting" ("threadId") `); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_ae7aab18a2641d3e5f25e0c4ea" ON "note_thread_muting" ("userId", "threadId") `); + await queryRunner.query(`ALTER TABLE "note" ADD "threadId" character varying(256)`); + await queryRunner.query(`CREATE INDEX "IDX_d4ebdef929896d6dc4a3c5bb48" ON "note" ("threadId") `); + await queryRunner.query(`ALTER TABLE "note_thread_muting" ADD CONSTRAINT "FK_29c11c7deb06615076f8c95b80a" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "note_thread_muting" DROP CONSTRAINT "FK_29c11c7deb06615076f8c95b80a"`); + await queryRunner.query(`DROP INDEX "public"."IDX_d4ebdef929896d6dc4a3c5bb48"`); + await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "threadId"`); + await queryRunner.query(`DROP INDEX "public"."IDX_ae7aab18a2641d3e5f25e0c4ea"`); + await queryRunner.query(`DROP INDEX "public"."IDX_c426394644267453e76f036926"`); + await queryRunner.query(`DROP INDEX "public"."IDX_29c11c7deb06615076f8c95b80"`); + await queryRunner.query(`DROP TABLE "note_thread_muting"`); + } + +} diff --git a/src/client/components/note-detailed.vue b/src/client/components/note-detailed.vue index 40b0a68c58..568a2360d1 100644 --- a/src/client/components/note-detailed.vue +++ b/src/client/components/note-detailed.vue @@ -601,6 +601,12 @@ export default defineComponent({ }); }, + toggleThreadMute(mute: boolean) { + os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', { + noteId: this.appearNote.id + }); + }, + getMenu() { let menu; if (this.$i) { @@ -657,6 +663,15 @@ export default defineComponent({ text: this.$ts.watch, action: () => this.toggleWatch(true) }) : undefined, + statePromise.then(state => state.isMutedThread ? { + icon: 'fas fa-comment-slash', + text: this.$ts.unmuteThread, + action: () => this.toggleThreadMute(false) + } : { + icon: 'fas fa-comment-slash', + text: this.$ts.muteThread, + action: () => this.toggleThreadMute(true) + }), this.appearNote.userId == this.$i.id ? (this.$i.pinnedNoteIds || []).includes(this.appearNote.id) ? { icon: 'fas fa-thumbtack', text: this.$ts.unpin, diff --git a/src/client/components/note.vue b/src/client/components/note.vue index 91a3e3b87d..681e819a22 100644 --- a/src/client/components/note.vue +++ b/src/client/components/note.vue @@ -576,6 +576,12 @@ export default defineComponent({ }); }, + toggleThreadMute(mute: boolean) { + os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', { + noteId: this.appearNote.id + }); + }, + getMenu() { let menu; if (this.$i) { @@ -632,6 +638,15 @@ export default defineComponent({ text: this.$ts.watch, action: () => this.toggleWatch(true) }) : undefined, + statePromise.then(state => state.isMutedThread ? { + icon: 'fas fa-comment-slash', + text: this.$ts.unmuteThread, + action: () => this.toggleThreadMute(false) + } : { + icon: 'fas fa-comment-slash', + text: this.$ts.muteThread, + action: () => this.toggleThreadMute(true) + }), this.appearNote.userId == this.$i.id ? (this.$i.pinnedNoteIds || []).includes(this.appearNote.id) ? { icon: 'fas fa-thumbtack', text: this.$ts.unpin, diff --git a/src/db/postgre.ts b/src/db/postgre.ts index 4f4047b613..f52c2ab722 100644 --- a/src/db/postgre.ts +++ b/src/db/postgre.ts @@ -17,6 +17,7 @@ import { PollVote } from '@/models/entities/poll-vote'; import { Note } from '@/models/entities/note'; import { NoteReaction } from '@/models/entities/note-reaction'; import { NoteWatching } from '@/models/entities/note-watching'; +import { NoteThreadMuting } from '@/models/entities/note-thread-muting'; import { NoteUnread } from '@/models/entities/note-unread'; import { Notification } from '@/models/entities/notification'; import { Meta } from '@/models/entities/meta'; @@ -138,6 +139,7 @@ export const entities = [ NoteFavorite, NoteReaction, NoteWatching, + NoteThreadMuting, NoteUnread, Page, PageLike, diff --git a/src/models/entities/note-thread-muting.ts b/src/models/entities/note-thread-muting.ts new file mode 100644 index 0000000000..b438522a4c --- /dev/null +++ b/src/models/entities/note-thread-muting.ts @@ -0,0 +1,33 @@ +import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { User } from './user'; +import { Note } from './note'; +import { id } from '../id'; + +@Entity() +@Index(['userId', 'threadId'], { unique: true }) +export class NoteThreadMuting { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone', { + }) + public createdAt: Date; + + @Index() + @Column({ + ...id(), + }) + public userId: User['id']; + + @ManyToOne(type => User, { + onDelete: 'CASCADE' + }) + @JoinColumn() + public user: User | null; + + @Index() + @Column('varchar', { + length: 256, + }) + public threadId: string; +} diff --git a/src/models/entities/note.ts b/src/models/entities/note.ts index 9a85532637..4a5411f93d 100644 --- a/src/models/entities/note.ts +++ b/src/models/entities/note.ts @@ -47,6 +47,12 @@ export class Note { @JoinColumn() public renote: Note | null; + @Index() + @Column('varchar', { + length: 256, nullable: true + }) + public threadId: string | null; + @Column('varchar', { length: 8192, nullable: true }) diff --git a/src/models/index.ts b/src/models/index.ts index 4c6f19eaff..7154cca550 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -7,6 +7,7 @@ import { PollVote } from './entities/poll-vote'; import { Meta } from './entities/meta'; import { SwSubscription } from './entities/sw-subscription'; import { NoteWatching } from './entities/note-watching'; +import { NoteThreadMuting } from './entities/note-thread-muting'; import { NoteUnread } from './entities/note-unread'; import { RegistrationTicket } from './entities/registration-tickets'; import { UserRepository } from './repositories/user'; @@ -69,6 +70,7 @@ export const Apps = getCustomRepository(AppRepository); export const Notes = getCustomRepository(NoteRepository); export const NoteFavorites = getCustomRepository(NoteFavoriteRepository); export const NoteWatchings = getRepository(NoteWatching); +export const NoteThreadMutings = getRepository(NoteThreadMuting); export const NoteReactions = getCustomRepository(NoteReactionRepository); export const NoteUnreads = getRepository(NoteUnread); export const Polls = getRepository(Poll); diff --git a/src/server/api/common/generate-muted-note-thread-query.ts b/src/server/api/common/generate-muted-note-thread-query.ts new file mode 100644 index 0000000000..7e2cbd498b --- /dev/null +++ b/src/server/api/common/generate-muted-note-thread-query.ts @@ -0,0 +1,17 @@ +import { User } from '@/models/entities/user'; +import { NoteThreadMutings } from '@/models/index'; +import { Brackets, SelectQueryBuilder } from 'typeorm'; + +export function generateMutedNoteThreadQuery(q: SelectQueryBuilder, me: { id: User['id'] }) { + const mutedQuery = NoteThreadMutings.createQueryBuilder('threadMuted') + .select('threadMuted.threadId') + .where('threadMuted.userId = :userId', { userId: me.id }); + + q.andWhere(`note.id NOT IN (${ mutedQuery.getQuery() })`); + q.andWhere(new Brackets(qb => { qb + .where(`note.threadId IS NULL`) + .orWhere(`note.threadId NOT IN (${ mutedQuery.getQuery() })`); + })); + + q.setParameters(mutedQuery.getParameters()); +} diff --git a/src/server/api/endpoints/notes/mentions.ts b/src/server/api/endpoints/notes/mentions.ts index 74f7911bfe..ffaebd6c95 100644 --- a/src/server/api/endpoints/notes/mentions.ts +++ b/src/server/api/endpoints/notes/mentions.ts @@ -8,6 +8,7 @@ import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; import { makePaginationQuery } from '../../common/make-pagination-query'; import { Brackets } from 'typeorm'; import { generateBlockedUserQuery } from '../../common/generate-block-query'; +import { generateMutedNoteThreadQuery } from '../../common/generate-muted-note-thread-query'; export const meta = { tags: ['notes'], @@ -67,6 +68,7 @@ export default define(meta, async (ps, user) => { generateVisibilityQuery(query, user); generateMutedUserQuery(query, user); + generateMutedNoteThreadQuery(query, user); generateBlockedUserQuery(query, user); if (ps.visibility) { diff --git a/src/server/api/endpoints/notes/state.ts b/src/server/api/endpoints/notes/state.ts index 489902435d..b3913a5e79 100644 --- a/src/server/api/endpoints/notes/state.ts +++ b/src/server/api/endpoints/notes/state.ts @@ -1,7 +1,7 @@ import $ from 'cafy'; import { ID } from '@/misc/cafy-id'; import define from '../../define'; -import { NoteFavorites, NoteWatchings } from '@/models/index'; +import { NoteFavorites, Notes, NoteThreadMutings, NoteWatchings } from '@/models/index'; export const meta = { tags: ['notes'], @@ -25,31 +25,45 @@ export const meta = { isWatching: { type: 'boolean' as const, optional: false as const, nullable: false as const - } + }, + isMutedThread: { + type: 'boolean' as const, + optional: false as const, nullable: false as const + }, } } }; export default define(meta, async (ps, user) => { - const [favorite, watching] = await Promise.all([ + const note = await Notes.findOneOrFail(ps.noteId); + + const [favorite, watching, threadMuting] = await Promise.all([ NoteFavorites.count({ where: { userId: user.id, - noteId: ps.noteId + noteId: note.id, }, take: 1 }), NoteWatchings.count({ where: { userId: user.id, - noteId: ps.noteId + noteId: note.id, }, take: 1 - }) + }), + NoteThreadMutings.count({ + where: { + userId: user.id, + threadId: note.threadId || note.id, + }, + take: 1 + }), ]); return { isFavorited: favorite !== 0, - isWatching: watching !== 0 + isWatching: watching !== 0, + isMutedThread: threadMuting !== 0, }; }); diff --git a/src/server/api/endpoints/notes/thread-muting/create.ts b/src/server/api/endpoints/notes/thread-muting/create.ts new file mode 100644 index 0000000000..2010d54331 --- /dev/null +++ b/src/server/api/endpoints/notes/thread-muting/create.ts @@ -0,0 +1,54 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../../define'; +import { getNote } from '../../../common/getters'; +import { ApiError } from '../../../error'; +import { Notes, NoteThreadMutings } from '@/models'; +import { genId } from '@/misc/gen-id'; +import readNote from '@/services/note/read'; + +export const meta = { + tags: ['notes'], + + requireCredential: true as const, + + kind: 'write:account', + + params: { + noteId: { + validator: $.type(ID), + } + }, + + errors: { + noSuchNote: { + message: 'No such note.', + code: 'NO_SUCH_NOTE', + id: '5ff67ada-ed3b-2e71-8e87-a1a421e177d2' + } + } +}; + +export default define(meta, async (ps, user) => { + const note = await getNote(ps.noteId).catch(e => { + if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw e; + }); + + const mutedNotes = await Notes.find({ + where: [{ + id: note.threadId || note.id, + }, { + threadId: note.threadId || note.id, + }], + }); + + await readNote(user.id, mutedNotes); + + await NoteThreadMutings.insert({ + id: genId(), + createdAt: new Date(), + threadId: note.threadId || note.id, + userId: user.id, + }); +}); diff --git a/src/server/api/endpoints/notes/thread-muting/delete.ts b/src/server/api/endpoints/notes/thread-muting/delete.ts new file mode 100644 index 0000000000..05d5691870 --- /dev/null +++ b/src/server/api/endpoints/notes/thread-muting/delete.ts @@ -0,0 +1,40 @@ +import $ from 'cafy'; +import { ID } from '@/misc/cafy-id'; +import define from '../../../define'; +import { getNote } from '../../../common/getters'; +import { ApiError } from '../../../error'; +import { NoteThreadMutings } from '@/models'; + +export const meta = { + tags: ['notes'], + + requireCredential: true as const, + + kind: 'write:account', + + params: { + noteId: { + validator: $.type(ID), + } + }, + + errors: { + noSuchNote: { + message: 'No such note.', + code: 'NO_SUCH_NOTE', + id: 'bddd57ac-ceb3-b29d-4334-86ea5fae481a' + } + } +}; + +export default define(meta, async (ps, user) => { + const note = await getNote(ps.noteId).catch(e => { + if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw e; + }); + + await NoteThreadMutings.delete({ + threadId: note.threadId || note.id, + userId: user.id, + }); +}); diff --git a/src/services/note/create.ts b/src/services/note/create.ts index 8c996bdba6..69d854ab1a 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -10,13 +10,13 @@ import { resolveUser } from '@/remote/resolve-user'; import config from '@/config/index'; import { updateHashtags } from '../update-hashtag'; import { concat } from '@/prelude/array'; -import insertNoteUnread from './unread'; +import { insertNoteUnread } from '@/services/note/unread'; import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc'; import { extractMentions } from '@/misc/extract-mentions'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm'; import { extractHashtags } from '@/misc/extract-hashtags'; import { Note, IMentionedRemoteUsers } from '@/models/entities/note'; -import { Mutings, Users, NoteWatchings, Notes, Instances, UserProfiles, Antennas, Followings, MutedNotes, Channels, ChannelFollowings, Blockings } from '@/models/index'; +import { Mutings, Users, NoteWatchings, Notes, Instances, UserProfiles, Antennas, Followings, MutedNotes, Channels, ChannelFollowings, Blockings, NoteThreadMutings } from '@/models/index'; import { DriveFile } from '@/models/entities/drive-file'; import { App } from '@/models/entities/app'; import { Not, getConnection, In } from 'typeorm'; @@ -344,8 +344,15 @@ export default async (user: { id: User['id']; username: User['username']; host: // 通知 if (data.reply.userHost === null) { - nm.push(data.reply.userId, 'reply'); - publishMainStream(data.reply.userId, 'reply', noteObj); + const threadMuted = await NoteThreadMutings.findOne({ + userId: data.reply.userId, + threadId: data.reply.threadId || data.reply.id, + }); + + if (!threadMuted) { + nm.push(data.reply.userId, 'reply'); + publishMainStream(data.reply.userId, 'reply', noteObj); + } } } @@ -459,6 +466,11 @@ async function insertNote(user: { id: User['id']; host: User['host']; }, data: O replyId: data.reply ? data.reply.id : null, renoteId: data.renote ? data.renote.id : null, channelId: data.channel ? data.channel.id : null, + threadId: data.reply + ? data.reply.threadId + ? data.reply.threadId + : data.reply.id + : null, name: data.name, text: data.text, hasPoll: data.poll != null, @@ -581,6 +593,15 @@ async function notifyToWatchersOfReplyee(reply: Note, user: { id: User['id']; }, async function createMentionedEvents(mentionedUsers: User[], note: Note, nm: NotificationManager) { for (const u of mentionedUsers.filter(u => Users.isLocalUser(u))) { + const threadMuted = await NoteThreadMutings.findOne({ + userId: u.id, + threadId: note.threadId || note.id, + }); + + if (threadMuted) { + continue; + } + const detailPackedNote = await Notes.pack(note, u, { detail: true }); diff --git a/src/services/note/unread.ts b/src/services/note/unread.ts index 4a9df6083c..29d2b54af8 100644 --- a/src/services/note/unread.ts +++ b/src/services/note/unread.ts @@ -1,10 +1,10 @@ import { Note } from '@/models/entities/note'; import { publishMainStream } from '@/services/stream'; import { User } from '@/models/entities/user'; -import { Mutings, NoteUnreads } from '@/models/index'; +import { Mutings, NoteThreadMutings, NoteUnreads } from '@/models/index'; import { genId } from '@/misc/gen-id'; -export default async function(userId: User['id'], note: Note, params: { +export async function insertNoteUnread(userId: User['id'], note: Note, params: { // NOTE: isSpecifiedがtrueならisMentionedは必ずfalse isSpecified: boolean; isMentioned: boolean; @@ -17,6 +17,13 @@ export default async function(userId: User['id'], note: Note, params: { if (mute.map(m => m.muteeId).includes(note.userId)) return; //#endregion + // スレッドミュート + const threadMute = await NoteThreadMutings.findOne({ + userId: userId, + threadId: note.threadId || note.id, + }); + if (threadMute) return; + const unread = { id: genId(), noteId: note.id, diff --git a/test/thread-mute.ts b/test/thread-mute.ts new file mode 100644 index 0000000000..95601cd903 --- /dev/null +++ b/test/thread-mute.ts @@ -0,0 +1,103 @@ +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import * as childProcess from 'child_process'; +import { async, signup, request, post, react, connectStream, startServer, shutdownServer } from './utils'; + +describe('Note thread mute', () => { + let p: childProcess.ChildProcess; + + let alice: any; + let bob: any; + let carol: any; + + before(async () => { + p = await startServer(); + alice = await signup({ username: 'alice' }); + bob = await signup({ username: 'bob' }); + carol = await signup({ username: 'carol' }); + }); + + after(async () => { + await shutdownServer(p); + }); + + it('notes/mentions にミュートしているスレッドの投稿が含まれない', async(async () => { + const bobNote = await post(bob, { text: '@alice @carol root note' }); + const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' }); + + await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice); + + const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); + const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' }); + + const res = await request('/notes/mentions', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.some((note: any) => note.id === bobNote.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolReply.id), false); + assert.strictEqual(res.body.some((note: any) => note.id === carolReplyWithoutMention.id), false); + })); + + it('ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない', async(async () => { + // 状態リセット + await request('/i/read-all-unread-notes', {}, alice); + + const bobNote = await post(bob, { text: '@alice @carol root note' }); + + await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice); + + const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); + + const res = await request('/i', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(res.body.hasUnreadMentions, false); + })); + + it('ミュートしているスレッドからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise(async done => { + // 状態リセット + await request('/i/read-all-unread-notes', {}, alice); + + const bobNote = await post(bob, { text: '@alice @carol root note' }); + + await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice); + + let fired = false; + + const ws = await connectStream(alice, 'main', async ({ type, body }) => { + if (type === 'unreadMention') { + if (body === bobNote.id) return; + fired = true; + } + }); + + const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); + + setTimeout(() => { + assert.strictEqual(fired, false); + ws.close(); + done(); + }, 5000); + })); + + it('i/notifications にミュートしているスレッドの通知が含まれない', async(async () => { + const bobNote = await post(bob, { text: '@alice @carol root note' }); + const aliceReply = await post(alice, { replyId: bobNote.id, text: '@bob @carol child note' }); + + await request('/notes/thread-muting/create', { noteId: bobNote.id }, alice); + + const carolReply = await post(carol, { replyId: bobNote.id, text: '@bob @alice child note' }); + const carolReplyWithoutMention = await post(carol, { replyId: aliceReply.id, text: 'child note' }); + + const res = await request('/i/notifications', {}, alice); + + assert.strictEqual(res.status, 200); + assert.strictEqual(Array.isArray(res.body), true); + assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReply.id), false); + assert.strictEqual(res.body.some((notification: any) => notification.note.id === carolReplyWithoutMention.id), false); + + // NOTE: bobの投稿はスレッドミュート前に行われたため通知に含まれていてもよい + })); +}); diff --git a/test/utils.ts b/test/utils.ts index 1a0c54463d..54bcf65ab1 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -1,5 +1,6 @@ import * as fs from 'fs'; import * as WebSocket from 'ws'; +import * as misskey from 'misskey-js'; import fetch from 'node-fetch'; const FormData = require('form-data'); import * as childProcess from 'child_process'; @@ -52,7 +53,7 @@ export const signup = async (params?: any): Promise => { return res.body; }; -export const post = async (user: any, params?: any): Promise => { +export const post = async (user: any, params?: misskey.Endpoints['notes/create']['req']): Promise => { const q = Object.assign({ text: 'test' }, params); From 303c5abfb42939011dd1bd48f5516a4ec2f2ac1d Mon Sep 17 00:00:00 2001 From: tamaina Date: Sun, 31 Oct 2021 16:01:50 +0900 Subject: [PATCH 03/16] =?UTF-8?q?feat:=20=E3=82=AF=E3=83=A9=E3=82=A4?= =?UTF-8?q?=E3=82=A2=E3=83=B3=E3=83=88=E3=81=A7=E3=83=AD=E3=82=B0=E3=82=A4?= =?UTF-8?q?=E3=83=B3=E3=81=99=E3=82=8B=E3=82=A2=E3=82=AB=E3=82=A6=E3=83=B3?= =?UTF-8?q?=E3=83=88id=E3=82=92=E6=8C=87=E5=AE=9A=E3=81=99=E3=82=8B?= =?UTF-8?q?=E3=82=AF=E3=82=A8=E3=83=AA(loginId=3D:userId)=20(#7929)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: ログインするアカウントのIDをクエリ文字列で指定する機能 * await? * rename --- src/client/init.ts | 21 +++++++++++++++++++++ src/client/scripts/login-id.ts | 11 +++++++++++ 2 files changed, 32 insertions(+) create mode 100644 src/client/scripts/login-id.ts diff --git a/src/client/init.ts b/src/client/init.ts index 123d4020e1..654e176398 100644 --- a/src/client/init.ts +++ b/src/client/init.ts @@ -37,6 +37,8 @@ import { isMobile } from '@client/scripts/is-mobile'; import { initializeSw } from '@client/scripts/initialize-sw'; import { reloadChannel } from '@client/scripts/unison-reload'; import { reactionPicker } from '@client/scripts/reaction-picker'; +import { getUrlWithoutLoginId } from '@client/scripts/login-id'; +import { getAccountFromId } from '@client/scripts/get-account-from-id'; console.info(`Misskey v${version}`); @@ -116,6 +118,25 @@ const html = document.documentElement; html.setAttribute('lang', lang); //#endregion +//#region loginId +const params = new URLSearchParams(location.search); +const loginId = params.get('loginId'); + +if (loginId) { + const target = getUrlWithoutLoginId(location.href); + + if (!$i || $i.id !== loginId) { + const account = await getAccountFromId(loginId); + if (account) { + await login(account.token, target); + } + } + + history.replaceState({ misskey: 'loginId' }, '', target); +} + +//#endregion + //#region Fetch user if ($i && $i.token) { if (_DEV_) { diff --git a/src/client/scripts/login-id.ts b/src/client/scripts/login-id.ts new file mode 100644 index 0000000000..0f9c6be4a9 --- /dev/null +++ b/src/client/scripts/login-id.ts @@ -0,0 +1,11 @@ +export function getUrlWithLoginId(url: string, loginId: string) { + const u = new URL(url, origin); + u.searchParams.append('loginId', loginId); + return u.toString(); +} + +export function getUrlWithoutLoginId(url: string) { + const u = new URL(url); + u.searchParams.delete('loginId'); + return u.toString(); +} From 9236a8fd6c32e6b33ad53f3d82c177fe5060d352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B7=E3=82=A2=E3=83=8E=E3=83=B3?= Date: Sun, 31 Oct 2021 16:55:25 +0900 Subject: [PATCH 04/16] =?UTF-8?q?=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC?= =?UTF-8?q?=E6=83=85=E5=A0=B1=E3=81=AEhasUnreadChannel=E3=81=8C=E5=B8=B8?= =?UTF-8?q?=E3=81=ABfalse=E3=81=AB=E3=81=AA=E3=81=A3=E3=81=A6=E3=81=97?= =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A6=E3=81=84=E3=82=8B=E3=81=AE=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20(#7938)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/models/repositories/user.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts index 72cefbaac5..9598e87191 100644 --- a/src/models/repositories/user.ts +++ b/src/models/repositories/user.ts @@ -112,7 +112,7 @@ export class UserRepository extends Repository { const unread = channels.length > 0 ? await NoteUnreads.findOne({ userId: userId, - noteChannelId: In(channels.map(x => x.id)), + noteChannelId: In(channels.map(x => x.followeeId)), }) : null; return unread != null; From e2556189decf5d6e6c9923a0c81c6214749b40b6 Mon Sep 17 00:00:00 2001 From: MeiMei <30769358+mei23@users.noreply.github.com> Date: Sun, 31 Oct 2021 18:01:16 +0900 Subject: [PATCH 05/16] =?UTF-8?q?fix:=20=E5=89=8A=E9=99=A4=E3=81=97?= =?UTF-8?q?=E3=81=9F=E3=83=8E=E3=83=BC=E3=83=88=E3=82=84=E3=83=A6=E3=83=BC?= =?UTF-8?q?=E3=82=B6=E3=83=BC=E3=81=8C=E3=83=AA=E3=83=A2=E3=83=BC=E3=83=88?= =?UTF-8?q?=E3=81=8B=E3=82=89=E5=8F=82=E7=85=A7=E3=81=95=E3=82=8C=E3=82=8B?= =?UTF-8?q?=E3=81=A8=E5=BE=A9=E6=B4=BB=E3=81=99=E3=82=8B=E3=81=93=E3=81=A8?= =?UTF-8?q?=E3=81=8C=E3=81=82=E3=82=8B=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=20(#7918)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix #7557 * CHANGELOG * Fix user * CHANGELOG * Tune CHANGELOG * Tune CHANGELOG * resolver * Remove check * Remove import * CHANGELOG * Tune Co-authored-by: syuilo --- CHANGELOG.md | 1 + src/remote/activitypub/models/note.ts | 4 ++++ src/remote/activitypub/models/person.ts | 5 +++++ 3 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae652c310a..260d15af15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ ### Bugfixes - リレー向けのActivityが一部実装で除外されてしまうことがあるのを修正 +- 削除したノートやユーザーがリモートから参照されると復活することがあるのを修正 ## 12.94.1 (2021/10/25) diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts index cf68f3005d..492dc05248 100644 --- a/src/remote/activitypub/models/note.ts +++ b/src/remote/activitypub/models/note.ts @@ -288,6 +288,10 @@ export async function resolveNote(value: string | IObject, resolver?: Resolver): } //#endregion + if (uri.startsWith(config.url)) { + throw new StatusError('cannot resolve local note', 400, 'cannot resolve local note'); + } + // リモートサーバーからフェッチしてきて登録 // ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが // 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。 diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts index 84b2f0c51c..eb8c00a10b 100644 --- a/src/remote/activitypub/models/person.ts +++ b/src/remote/activitypub/models/person.ts @@ -29,6 +29,7 @@ import { toArray } from '@/prelude/array'; import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata'; import { normalizeForSearch } from '@/misc/normalize-for-search'; import { truncate } from '@/misc/truncate'; +import { StatusError } from '@/misc/fetch'; const logger = apLogger; @@ -116,6 +117,10 @@ export async function fetchPerson(uri: string, resolver?: Resolver): Promise { if (typeof uri !== 'string') throw new Error('uri is not string'); + if (uri.startsWith(config.url)) { + throw new StatusError('cannot resolve local user', 400, 'cannot resolve local user'); + } + if (resolver == null) resolver = new Resolver(); const object = await resolver.resolve(uri) as any; From 15cd56361289590e09f68e8161fdb2dce8e6b9e5 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Sun, 31 Oct 2021 11:12:19 +0100 Subject: [PATCH 06/16] stop context menu handling for videos (#7927) --- src/client/components/media-video.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client/components/media-video.vue b/src/client/components/media-video.vue index 44367ee999..4d4a551653 100644 --- a/src/client/components/media-video.vue +++ b/src/client/components/media-video.vue @@ -11,6 +11,7 @@ :title="video.name" preload="none" controls + @contextmenu.stop > Date: Sun, 31 Oct 2021 19:19:28 +0900 Subject: [PATCH 07/16] chore(client): Fix #7923 Close #7924 --- src/client/components/forgot-password.vue | 25 +++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/client/components/forgot-password.vue b/src/client/components/forgot-password.vue index cb2380f483..7fcf9aa720 100644 --- a/src/client/components/forgot-password.vue +++ b/src/client/components/forgot-password.vue @@ -7,21 +7,21 @@ > -
-
- + +
+ - + - {{ $ts.send }} + {{ $ts.send }}
-
+
{{ $ts._forgotPassword.ifNoEmail }}
@@ -69,3 +69,16 @@ export default defineComponent({ } }); + + From baf3d8f3eef57f4032b2a047349d180353263d28 Mon Sep 17 00:00:00 2001 From: okpierre <1679025+okpierre@users.noreply.github.com> Date: Sun, 31 Oct 2021 06:22:19 -0400 Subject: [PATCH 08/16] Update emojis.vue (#7915) --- src/client/pages/admin/emojis.vue | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/client/pages/admin/emojis.vue b/src/client/pages/admin/emojis.vue index 80e0e00ba9..b265530802 100644 --- a/src/client/pages/admin/emojis.vue +++ b/src/client/pages/admin/emojis.vue @@ -168,6 +168,10 @@ export default defineComponent({