forked from misskey-dev/misskey
-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
14 changed files
with
288 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
128 changes: 128 additions & 0 deletions
128
packages/backend/src/server/api/endpoints/notes/media-timeline.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
import { Brackets } from 'typeorm'; | ||
import { Inject, Injectable } from '@nestjs/common'; | ||
import type { NotesRepository } from '@/models/index.js'; | ||
import { Endpoint } from '@/server/api/endpoint-base.js'; | ||
import { QueryService } from '@/core/QueryService.js'; | ||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; | ||
import { MetaService } from '@/core/MetaService.js'; | ||
import ActiveUsersChart from '@/core/chart/charts/active-users.js'; | ||
import { DI } from '@/di-symbols.js'; | ||
import { RoleService } from '@/core/RoleService.js'; | ||
import { IdService } from '@/core/IdService.js'; | ||
import { ApiError } from '../../error.js'; | ||
|
||
export const meta = { | ||
tags: ['notes'], | ||
|
||
res: { | ||
type: 'array', | ||
optional: false, nullable: false, | ||
items: { | ||
type: 'object', | ||
optional: false, nullable: false, | ||
ref: 'Note', | ||
}, | ||
}, | ||
|
||
errors: { | ||
ltlDisabled: { | ||
message: 'Media timeline has been disabled.', | ||
code: 'MTL_DISABLED', | ||
id: '45a6eb02-7695-4393-b023-dd4be9aaaefd', | ||
}, | ||
}, | ||
} as const; | ||
|
||
export const paramDef = { | ||
type: 'object', | ||
properties: { | ||
withFiles: { | ||
type: 'boolean', | ||
default: false, | ||
description: 'Only show notes that have attached files.', | ||
}, | ||
fileType: { type: 'array', items: { | ||
type: 'string', | ||
} }, | ||
excludeNsfw: { type: 'boolean', default: false }, | ||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, | ||
sinceId: { type: 'string', format: 'misskey:id' }, | ||
untilId: { type: 'string', format: 'misskey:id' }, | ||
sinceDate: { type: 'integer' }, | ||
untilDate: { type: 'integer' }, | ||
}, | ||
required: [], | ||
} as const; | ||
|
||
// eslint-disable-next-line import/no-default-export | ||
@Injectable() | ||
export default class extends Endpoint<typeof meta, typeof paramDef> { | ||
constructor( | ||
@Inject(DI.notesRepository) | ||
private notesRepository: NotesRepository, | ||
|
||
private noteEntityService: NoteEntityService, | ||
private queryService: QueryService, | ||
private metaService: MetaService, | ||
private roleService: RoleService, | ||
private activeUsersChart: ActiveUsersChart, | ||
private idService: IdService, | ||
) { | ||
super(meta, paramDef, async (ps, me) => { | ||
const policies = await this.roleService.getUserPolicies(me ? me.id : null); | ||
if (!policies.ltlAvailable) { | ||
throw new ApiError(meta.errors.ltlDisabled); | ||
} | ||
|
||
//#region Construct query | ||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), | ||
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) | ||
.andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで | ||
.andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)') | ||
.andWhere('note.fileIds != \'{}\'') | ||
.innerJoinAndSelect('note.user', 'user') | ||
.leftJoinAndSelect('note.reply', 'reply') | ||
.leftJoinAndSelect('note.renote', 'renote') | ||
.leftJoinAndSelect('reply.user', 'replyUser') | ||
.leftJoinAndSelect('renote.user', 'renoteUser'); | ||
|
||
this.queryService.generateChannelQuery(query, me); | ||
this.queryService.generateRepliesQuery(query, me); | ||
this.queryService.generateVisibilityQuery(query, me); | ||
if (me) this.queryService.generateMutedUserQuery(query, me); | ||
if (me) this.queryService.generateMutedNoteQuery(query, me); | ||
if (me) this.queryService.generateBlockedUserQuery(query, me); | ||
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me); | ||
|
||
if (ps.withFiles) { | ||
query.andWhere('note.fileIds != \'{}\''); | ||
} | ||
|
||
if (ps.fileType != null) { | ||
query.andWhere('note.fileIds != \'{}\''); | ||
query.andWhere(new Brackets(qb => { | ||
for (const type of ps.fileType!) { | ||
const i = ps.fileType!.indexOf(type); | ||
qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type }); | ||
} | ||
})); | ||
|
||
if (ps.excludeNsfw) { | ||
query.andWhere('note.cw IS NULL'); | ||
query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)'); | ||
} | ||
} | ||
//#endregion | ||
|
||
const timeline = await query.take(ps.limit).getMany(); | ||
|
||
process.nextTick(() => { | ||
if (me) { | ||
this.activeUsersChart.read(me); | ||
} | ||
}); | ||
|
||
return await this.noteEntityService.packMany(timeline, me); | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
111 changes: 111 additions & 0 deletions
111
packages/backend/src/server/api/stream/channels/media-timeline.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import { Injectable } from '@nestjs/common'; | ||
import { checkWordMute } from '@/misc/check-word-mute.js'; | ||
import { isUserRelated } from '@/misc/is-user-related.js'; | ||
import type { Packed } from '@/misc/json-schema.js'; | ||
import { MetaService } from '@/core/MetaService.js'; | ||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; | ||
import { bindThis } from '@/decorators.js'; | ||
import { RoleService } from '@/core/RoleService.js'; | ||
import Channel from '../channel.js'; | ||
|
||
class MediaTimelineChannel extends Channel { | ||
public readonly chName = 'mediaTimeline'; | ||
public static shouldShare = true; | ||
public static requireCredential = false; | ||
|
||
constructor( | ||
private metaService: MetaService, | ||
private roleService: RoleService, | ||
private noteEntityService: NoteEntityService, | ||
|
||
id: string, | ||
connection: Channel['connection'], | ||
) { | ||
super(id, connection); | ||
//this.onNote = this.onNote.bind(this); | ||
} | ||
|
||
@bindThis | ||
public async init(params: any) { | ||
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); | ||
if (!policies.ltlAvailable) return; | ||
|
||
// Subscribe events | ||
this.subscriber.on('notesStream', this.onNote); | ||
} | ||
|
||
@bindThis | ||
private async onNote(note: Packed<'Note'>) { | ||
if (note.user.host !== null) return; | ||
if (note.visibility !== 'public') return; | ||
if (note.channelId != null && !this.followingChannels.has(note.channelId)) return; | ||
|
||
// リプライなら再pack | ||
if (note.replyId != null) { | ||
note.reply = await this.noteEntityService.pack(note.replyId, this.user, { | ||
detail: true, | ||
}); | ||
} | ||
// Renoteなら再pack | ||
if (note.renoteId != null) { | ||
note.renote = await this.noteEntityService.pack(note.renoteId, this.user, { | ||
detail: true, | ||
}); | ||
} | ||
|
||
// 関係ない返信は除外 | ||
if (note.reply && this.user && !this.user.showTimelineReplies) { | ||
const reply = note.reply; | ||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合 | ||
if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return; | ||
} | ||
|
||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する | ||
if (isUserRelated(note, this.userIdsWhoMeMuting)) return; | ||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する | ||
if (isUserRelated(note, this.userIdsWhoBlockingMe)) return; | ||
|
||
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return; | ||
|
||
// 流れてきたNoteがミュートすべきNoteだったら無視する | ||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある) | ||
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、 | ||
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。 | ||
// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる | ||
if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return; | ||
|
||
this.connection.cacheNote(note); | ||
|
||
this.send('note', note); | ||
} | ||
|
||
@bindThis | ||
public dispose() { | ||
// Unsubscribe events | ||
this.subscriber.off('notesStream', this.onNote); | ||
} | ||
} | ||
|
||
@Injectable() | ||
export class MediaTimelineChannelService { | ||
public readonly shouldShare = MediaTimelineChannel.shouldShare; | ||
public readonly requireCredential = MediaTimelineChannel.requireCredential; | ||
|
||
constructor( | ||
private metaService: MetaService, | ||
private roleService: RoleService, | ||
private noteEntityService: NoteEntityService, | ||
) { | ||
} | ||
|
||
@bindThis | ||
public create(id: string, connection: Channel['connection']): MediaTimelineChannel { | ||
return new MediaTimelineChannel( | ||
this.metaService, | ||
this.roleService, | ||
this.noteEntityService, | ||
id, | ||
connection, | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.