This commit is contained in:
Tatsuya Koishi 2024-11-04 20:47:05 +09:00 committed by GitHub
commit 860275d116
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 83 additions and 4 deletions

View file

@ -219,3 +219,7 @@ signToActivityPubGet: true
# Upload or download file size limits (bytes)
#maxFileSize: 262144000
defaultTag:
tag: null
append: true

View file

@ -321,3 +321,7 @@ signToActivityPubGet: true
# PID File of master process
#pidFile: /tmp/misskey.pid
defaultTag:
tag: null
append: true

View file

@ -60,6 +60,10 @@ type Source = {
};
sentryForBackend?: { options: Partial<Sentry.NodeOptions>; enableNodeProfiling: boolean; };
sentryForFrontend?: { options: Partial<Sentry.NodeOptions> };
defaultTag: {
tag: string;
append: boolean;
};
publishTarballInsteadOfProvideRepositoryUrl?: boolean;
@ -132,6 +136,10 @@ export type Config = {
index: string;
scope?: 'local' | 'global' | string[];
} | undefined;
defaultTag: {
tag: string;
append: boolean;
};
proxy: string | undefined;
proxySmtp: string | undefined;
proxyBypassHosts: string[] | undefined;
@ -293,6 +301,7 @@ export function loadConfig(): Config {
perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500,
deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),
pidFile: config.pidFile,
defaultTag: config.defaultTag,
};
}

View file

@ -330,6 +330,16 @@ export class NoteCreateService implements OnApplicationShutdown {
data.localOnly = true;
}
// デフォルトハッシュタグを本文末尾に書き足す
if (this.config.defaultTag?.append && ['public', 'home'].includes(data.visibility)) {
if (this.config.defaultTag?.tag != null) {
const tag = `#${this.config.defaultTag?.tag}`;
if (String(data.text).match(tag)) {
data.text = `${data.text}\n\n${tag}`;
}
}
}
if (data.text) {
if (data.text.length > DB_MAX_NOTE_TEXT_LENGTH) {
data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH);
@ -944,6 +954,18 @@ export class NoteCreateService implements OnApplicationShutdown {
}
}
// デフォルトハッシュタグを含む投稿は、リモートであってもローカルタイムラインに含める
if (this.config.defaultTag?.tag != null) {
const noteTags = note.tags ? note.tags.map((t: string) => t.toLowerCase()) : [];
if (note.visibility === 'public' && noteTags.includes(normalizeForSearch(this.config.defaultTag?.tag))) {
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 ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r);

View file

@ -19,6 +19,8 @@ import { UserFollowingService } from '@/core/UserFollowingService.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'],
@ -207,10 +209,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();
const defaultTag: string | null = config.defaultTag?.tag;
if (defaultTag == null) {
qb.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
} else {
qb.orWhere(`(note.visibility = 'public') AND (:t <@ note.tags`, { t: normalizeForSearch(defaultTag) });
}
}))
.innerJoinAndSelect('note.user', 'user')

View file

@ -16,6 +16,8 @@ import { QueryService } from '@/core/QueryService.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'],
@ -146,9 +148,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
withFiles: boolean,
withReplies: boolean,
}, me: MiLocalUser | null) {
const config = loadConfig();
const defaultTag: string | null = config.defaultTag?.tag;
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(new Brackets(qb => {
qb.andWhere('note.visibility = \'public\'');
qb.andWhere('note.channelId IS NULL');
if (defaultTag == null) {
qb.andWhere('note.userHost IS NULL');
} else {
qb.andWhere(`:t <@ note.tags`, { t: normalizeForSearch(defaultTag) });
}
}))
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')

View file

@ -12,6 +12,8 @@ import { RoleService } from '@/core/RoleService.js';
import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.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';
@ -21,6 +23,7 @@ class HybridTimelineChannel extends Channel {
private withRenotes: boolean;
private withReplies: boolean;
private withFiles: boolean;
private defaultTag: string | null;
constructor(
private metaService: MetaService,
@ -42,6 +45,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.defaultTag?.tag;
// Subscribe events
this.subscriber.on('notesStream', this.onNote);
@ -49,6 +54,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;
@ -60,7 +70,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 { RoleService } from '@/core/RoleService.js';
import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js';
import type { JsonObject } from '@/misc/json-value.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 | null;
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.defaultTag?.tag;
// 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;