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

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

@ -106,7 +106,7 @@ redis:
# ┌───────────────────────────┐
#───┘ MeiliSearch configuration └─────────────────────────────
# You can set scope to local (default value) or global
# You can set scope to local (default value) or global
# (include notes from remote).
#meilisearch:
@ -185,7 +185,7 @@ proxyRemoteFiles: true
signToActivityPubGet: true
# For security reasons, uploading attachments from the intranet is prohibited,
# but exceptions can be made from the following settings. Default value is "undefined".
# but exceptions can be made from the following settings. Default value is "undefined".
# Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)).
#allowedPrivateNetworks: [
# '127.0.0.1/32'
@ -193,3 +193,6 @@ signToActivityPubGet: true
# Upload or download file size limits (bytes)
#maxFileSize: 262144000
tagging:
defaultTag: null

View File

@ -118,7 +118,7 @@ redis:
# ┌───────────────────────────┐
#───┘ MeiliSearch configuration └─────────────────────────────
# You can set scope to local (default value) or global
# You can set scope to local (default value) or global
# (include notes from remote).
#meilisearch:
@ -214,7 +214,7 @@ proxyRemoteFiles: true
signToActivityPubGet: true
# For security reasons, uploading attachments from the intranet is prohibited,
# but exceptions can be made from the following settings. Default value is "undefined".
# but exceptions can be made from the following settings. Default value is "undefined".
# Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)).
#allowedPrivateNetworks: [
# '127.0.0.1/32'
@ -225,3 +225,6 @@ signToActivityPubGet: true
# PID File of master process
#pidFile: /tmp/misskey.pid
tagging:
defaultTag: null

View File

@ -56,6 +56,9 @@ type Source = {
index: string;
scope?: 'local' | 'global' | string[];
};
tagging: {
defaultTag: string;
};
proxy?: string;
proxySmtp?: string;
@ -124,6 +127,9 @@ export type Config = {
index: string;
scope?: 'local' | 'global' | string[];
} | undefined;
tagging: {
defaultTag: string;
};
proxy: string | undefined;
proxySmtp: string | undefined;
proxyBypassHosts: string[] | undefined;
@ -261,6 +267,7 @@ export function loadConfig(): Config {
perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500,
deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),
pidFile: config.pidFile,
tagging: config.tagging,
};
}

View File

@ -59,6 +59,7 @@ import { UtilityService } from '@/core/UtilityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js';
import { isReply } from '@/misc/is-reply.js';
import { trackPromise } from '@/misc/promise-tracker.js';
import { loadConfig } from '@/config.js';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@ -913,6 +914,16 @@ export class NoteCreateService implements OnApplicationShutdown {
}
}
// デフォルトハッシュタグ
const config = loadConfig();
if (note.visibility === 'public' && note.tags.includes(String(config.tagging.defaultTag))) {
this.fanoutTimelineService.push('localTimelineWithReplies', note.id, 300, r);
this.fanoutTimelineService.push('localTimeline', note.id, 1000, r);
if (note.fileIds.length > 0) {
this.fanoutTimelineService.push('localTimelineWithFiles', note.id, 500, r);
}
}
// 自分自身以外への返信
if (isReply(note)) {
this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);

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;