summaryrefslogtreecommitdiff
path: root/packages/backend/src/server/api/endpoints/notes
diff options
context:
space:
mode:
authorMar0xy <marie@kaifa.ch>2023-12-04 02:10:51 +0100
committerMar0xy <marie@kaifa.ch>2023-12-04 02:10:51 +0100
commit2f99c7e9dc2e5e3ca06c9672a6ab4887eb094310 (patch)
treeaa9801e261ed978d553cfc8cd80fee524d6496a6 /packages/backend/src/server/api/endpoints/notes
parentupd: add additional check to visibility selector for boost (diff)
downloadsharkey-2f99c7e9dc2e5e3ca06c9672a6ab4887eb094310.tar.gz
sharkey-2f99c7e9dc2e5e3ca06c9672a6ab4887eb094310.tar.bz2
sharkey-2f99c7e9dc2e5e3ca06c9672a6ab4887eb094310.zip
add: Bubble timeline
Closes transfem-org/Sharkey#154
Diffstat (limited to 'packages/backend/src/server/api/endpoints/notes')
-rw-r--r--packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts130
1 files changed, 130 insertions, 0 deletions
diff --git a/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts b/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts
new file mode 100644
index 0000000000..0652c82a9d
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/notes/bubble-timeline.ts
@@ -0,0 +1,130 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { Brackets } from 'typeorm';
+import type { NotesRepository } from '@/models/_.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { QueryService } from '@/core/QueryService.js';
+import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
+import ActiveUsersChart from '@/core/chart/charts/active-users.js';
+import { DI } from '@/di-symbols.js';
+import { RoleService } from '@/core/RoleService.js';
+import { ApiError } from '../../error.js';
+import { CacheService } from '@/core/CacheService.js';
+import { MetaService } from '@/core/MetaService.js';
+
+export const meta = {
+ tags: ['notes'],
+
+ res: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'Note',
+ },
+ },
+
+ errors: {
+ btlDisabled: {
+ message: 'Bubble timeline has been disabled.',
+ code: 'BTL_DISABLED',
+ id: '0332fc13-6ab2-4427-ae80-a9fadffd1a6c',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ withFiles: { type: 'boolean', default: false },
+ withBots: { type: 'boolean', default: true },
+ withRenotes: { type: 'boolean', default: true },
+ 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;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
+
+ private noteEntityService: NoteEntityService,
+ private queryService: QueryService,
+ private roleService: RoleService,
+ private activeUsersChart: ActiveUsersChart,
+ private cacheService: CacheService,
+ private metaService: MetaService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const policies = await this.roleService.getUserPolicies(me ? me.id : null);
+ const instance = await this.metaService.fetch();
+ if (!policies.btlAvailable) {
+ throw new ApiError(meta.errors.btlDisabled);
+ }
+
+ const [
+ followings,
+ ] = me ? await Promise.all([
+ this.cacheService.userFollowingsCache.fetch(me.id),
+ ]) : [undefined];
+
+ //#region Construct query
+ const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
+ ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
+ .andWhere('note.visibility = \'public\'')
+ .andWhere('note.channelId IS NULL')
+ .andWhere(
+ `(note.userHost = ANY ('{"${instance.bubbleInstances.join('","')}"}'))`,
+ )
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('renote.user', 'renoteUser');
+
+ if (me) {
+ this.queryService.generateMutedUserQuery(query, me);
+ this.queryService.generateBlockedUserQuery(query, me);
+ this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
+ }
+
+ if (ps.withFiles) {
+ query.andWhere('note.fileIds != \'{}\'');
+ }
+
+ if (!ps.withBots) query.andWhere('user.isBot = FALSE');
+
+ if (ps.withRenotes === false) {
+ query.andWhere(new Brackets(qb => {
+ qb.where('note.renoteId IS NULL');
+ qb.orWhere(new Brackets(qb => {
+ qb.where('note.text IS NOT NULL');
+ qb.orWhere('note.fileIds != \'{}\'');
+ }));
+ }));
+ }
+ //#endregion
+
+ let timeline = await query.limit(ps.limit).getMany();
+
+ timeline = timeline.filter(note => {
+ if (note.user?.isSilenced && me && followings && note.userId !== me.id && !followings[note.userId]) return false;
+ return true;
+ });
+
+ process.nextTick(() => {
+ if (me) {
+ this.activeUsersChart.read(me);
+ }
+ });
+
+ return await this.noteEntityService.packMany(timeline, me);
+ });
+ }
+}