デフォルトハッシュタグタイムライン

This commit is contained in:
Tatsuya Koishi 2024-01-27 12:26:30 +09:00
parent 15727088be
commit 594193ba8a
8 changed files with 68 additions and 8 deletions

View file

@ -20,6 +20,8 @@ import { MetaService } from '@/core/MetaService.js';
import { MiLocalUser } from '@/models/User.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import { ApiError } from '../../error.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { loadConfig } from '@/config.js';
export const meta = {
tags: ['notes'],
@ -194,10 +196,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (followees.length > 0) {
const meOrFolloweeIds = [me.id, ...followees.map(f => f.followeeId)];
qb.where('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds });
qb.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
} else {
qb.where('note.userId = :meId', { meId: me.id });
}
const config = loadConfig();
let defaultTag:string | null = config.tagging.defaultTag;
if (defaultTag == null) {
qb.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
} else {
qb.orWhere(`(note.visibility = 'public') AND ('${normalizeForSearch(defaultTag)}' = any(note.tags)`);
}
}))
.innerJoinAndSelect('note.user', 'user')

View file

@ -18,6 +18,8 @@ import { MetaService } from '@/core/MetaService.js';
import { MiLocalUser } from '@/models/User.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import { ApiError } from '../../error.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { loadConfig } from '@/config.js';
export const meta = {
tags: ['notes'],
@ -149,9 +151,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
withFiles: boolean,
withReplies: boolean,
}, me: MiLocalUser | null) {
const config = loadConfig();
let defaultTag:string | null = config.tagging.defaultTag;
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
ps.sinceId, ps.untilId)
.andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL) AND (note.channelId IS NULL)')
.andWhere(
(defaultTag == null)
? '(note.visibility = \'public\') AND (note.userHost IS NULL) AND (note.channelId IS NULL)'
: `(note.visibility = 'public') AND ('${normalizeForSearch(defaultTag)}' = any(note.tags) AND (note.channelId IS NULL)`
)
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')

View file

@ -13,6 +13,8 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import Channel, { type MiChannelService } from '../channel.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { loadConfig } from '@/config.js';
class HybridTimelineChannel extends Channel {
public readonly chName = 'hybridTimeline';
@ -22,6 +24,7 @@ class HybridTimelineChannel extends Channel {
private withRenotes: boolean;
private withReplies: boolean;
private withFiles: boolean;
private defaultTag: string;
constructor(
private metaService: MetaService,
@ -43,6 +46,8 @@ class HybridTimelineChannel extends Channel {
this.withRenotes = params.withRenotes ?? true;
this.withReplies = params.withReplies ?? false;
this.withFiles = params.withFiles ?? false;
const config = loadConfig();
this.defaultTag = config.tagging.defaultTag;
// Subscribe events
this.subscriber.on('notesStream', this.onNote);
@ -50,6 +55,11 @@ class HybridTimelineChannel extends Channel {
@bindThis
private async onNote(note: Packed<'Note'>) {
let matched = false;
if (this.defaultTag != null) {
const noteTags = note.tags ? note.tags.map((t: string) => t.toLowerCase()) : [];
matched = noteTags.includes(normalizeForSearch(this.defaultTag));
}
const isMe = this.user!.id === note.userId;
if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
@ -61,7 +71,7 @@ class HybridTimelineChannel extends Channel {
if (!(
(note.channelId == null && isMe) ||
(note.channelId == null && Object.hasOwn(this.following, note.userId)) ||
(note.channelId == null && (note.user.host == null && note.visibility === 'public')) ||
(note.channelId == null && ((note.user.host == null || matched) && note.visibility === 'public')) ||
(note.channelId != null && this.followingChannels.has(note.channelId))
)) return;

View file

@ -12,6 +12,8 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import Channel, { type MiChannelService } from '../channel.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { loadConfig } from '@/config.js';
class LocalTimelineChannel extends Channel {
public readonly chName = 'localTimeline';
@ -20,6 +22,7 @@ class LocalTimelineChannel extends Channel {
private withRenotes: boolean;
private withReplies: boolean;
private withFiles: boolean;
private defaultTag: string;
constructor(
private metaService: MetaService,
@ -41,6 +44,8 @@ class LocalTimelineChannel extends Channel {
this.withRenotes = params.withRenotes ?? true;
this.withReplies = params.withReplies ?? false;
this.withFiles = params.withFiles ?? false;
const config = loadConfig();
this.defaultTag = config.tagging.defaultTag;
// Subscribe events
this.subscriber.on('notesStream', this.onNote);
@ -50,7 +55,12 @@ class LocalTimelineChannel extends Channel {
private async onNote(note: Packed<'Note'>) {
if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
if (note.user.host !== null) return;
if (this.defaultTag == null) {
if (note.user.host !== null) return;
} else {
const noteTags = note.tags ? note.tags.map((t: string) => t.toLowerCase()) : [];
if (!noteTags.includes(normalizeForSearch(this.defaultTag))) return;
}
if (note.visibility !== 'public') return;
if (note.channelId != null) return;