Merge remote-tracking branch 'mi-dev/develop' into emoji-request

# Conflicts:
#	package.json
This commit is contained in:
mattyatea 2023-10-19 22:34:59 +09:00
commit f5658cd47d
No known key found for this signature in database
GPG key ID: 068E54E2C33BEF9A
106 changed files with 2220 additions and 1561 deletions

View file

@ -16,7 +16,7 @@ import type { AntennasRepository, UserListMembershipsRepository } from '@/models
import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { RedisTimelineService } from '@/core/RedisTimelineService.js';
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable()
@ -39,7 +39,7 @@ export class AntennaService implements OnApplicationShutdown {
private utilityService: UtilityService,
private globalEventService: GlobalEventService,
private redisTimelineService: RedisTimelineService,
private funoutTimelineService: FunoutTimelineService,
) {
this.antennasFetched = false;
this.antennas = [];
@ -84,7 +84,7 @@ export class AntennaService implements OnApplicationShutdown {
const redisPipeline = this.redisForTimelines.pipeline();
for (const antenna of matchedAntennas) {
this.redisTimelineService.push(`antennaTimeline:${antenna.id}`, note.id, 200, redisPipeline);
this.funoutTimelineService.push(`antennaTimeline:${antenna.id}`, note.id, 200, redisPipeline);
this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
}

View file

@ -61,7 +61,7 @@ import { FileInfoService } from './FileInfoService.js';
import { SearchService } from './SearchService.js';
import { ClipService } from './ClipService.js';
import { FeaturedService } from './FeaturedService.js';
import { RedisTimelineService } from './RedisTimelineService.js';
import { FunoutTimelineService } from './FunoutTimelineService.js';
import { ChartLoggerService } from './chart/ChartLoggerService.js';
import FederationChart from './chart/charts/federation.js';
import NotesChart from './chart/charts/notes.js';
@ -190,7 +190,7 @@ const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: Fi
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService };
const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService };
const $RedisTimelineService: Provider = { provide: 'RedisTimelineService', useExisting: RedisTimelineService };
const $FunoutTimelineService: Provider = { provide: 'FunoutTimelineService', useExisting: FunoutTimelineService };
const $ChartLoggerService: Provider = { provide: 'ChartLoggerService', useExisting: ChartLoggerService };
const $FederationChart: Provider = { provide: 'FederationChart', useExisting: FederationChart };
@ -323,7 +323,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
SearchService,
ClipService,
FeaturedService,
RedisTimelineService,
FunoutTimelineService,
ChartLoggerService,
FederationChart,
NotesChart,
@ -449,7 +449,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$SearchService,
$ClipService,
$FeaturedService,
$RedisTimelineService,
$FunoutTimelineService,
$ChartLoggerService,
$FederationChart,
$NotesChart,
@ -576,7 +576,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
SearchService,
ClipService,
FeaturedService,
RedisTimelineService,
FunoutTimelineService,
FederationChart,
NotesChart,
UsersChart,
@ -701,7 +701,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$SearchService,
$ClipService,
$FeaturedService,
$RedisTimelineService,
$FunoutTimelineService,
$FederationChart,
$NotesChart,
$UsersChart,

View file

@ -353,7 +353,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
const queryOrNull = async () => (await this.emojisRepository.findOneBy({
name,
host: host ?? IsNull(),
host,
})) ?? null;
const emoji = await this.cache.fetch(`${name} ${host}`, queryOrNull);

View file

@ -10,7 +10,7 @@ import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
@Injectable()
export class RedisTimelineService {
export class FunoutTimelineService {
constructor(
@Inject(DI.redisForTimelines)
private redisForTimelines: Redis.Redis,
@ -77,4 +77,9 @@ export class RedisTimelineService {
);
});
}
@bindThis
public purge(name: string) {
return this.redisForTimelines.del('list:' + name);
}
}

View file

@ -45,7 +45,7 @@ export class HashtagService {
await this.updateHashtag(user, tag, true, true);
}
for (const tag of (user.tags ?? []).filter(x => !tags.includes(x))) {
for (const tag of user.tags.filter(x => !tags.includes(x))) {
await this.updateHashtag(user, tag, true, false);
}
}

View file

@ -54,8 +54,9 @@ import { RoleService } from '@/core/RoleService.js';
import { MetaService } from '@/core/MetaService.js';
import { SearchService } from '@/core/SearchService.js';
import { FeaturedService } from '@/core/FeaturedService.js';
import { RedisTimelineService } from '@/core/RedisTimelineService.js';
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
import { nyaize } from '@/misc/nyaize.js';
import { UtilityService } from '@/core/UtilityService.js';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@ -196,7 +197,7 @@ export class NoteCreateService implements OnApplicationShutdown {
private idService: IdService,
private globalEventService: GlobalEventService,
private queueService: QueueService,
private redisTimelineService: RedisTimelineService,
private funoutTimelineService: FunoutTimelineService,
private noteReadService: NoteReadService,
private notificationService: NotificationService,
private relayService: RelayService,
@ -215,6 +216,7 @@ export class NoteCreateService implements OnApplicationShutdown {
private perUserNotesChart: PerUserNotesChart,
private activeUsersChart: ActiveUsersChart,
private instanceChart: InstanceChart,
private utilityService: UtilityService,
) { }
@bindThis
@ -225,8 +227,6 @@ export class NoteCreateService implements OnApplicationShutdown {
isBot: MiUser['isBot'];
isCat: MiUser['isCat'];
}, data: Option, silent = false): Promise<MiNote> {
let patsedText: mfm.MfmNode[] | null = null;
// チャンネル外にリプライしたら対象のスコープに合わせる
// (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで)
if (data.reply && data.channel && data.reply.channelId !== data.channel.id) {
@ -250,8 +250,10 @@ export class NoteCreateService implements OnApplicationShutdown {
if (data.channel != null) data.visibleUsers = [];
if (data.channel != null) data.localOnly = true;
const meta = await this.metaService.fetch();
if (data.visibility === 'public' && data.channel == null) {
const sensitiveWords = (await this.metaService.fetch()).sensitiveWords;
const sensitiveWords = meta.sensitiveWords;
if (this.isSensitive(data, sensitiveWords)) {
data.visibility = 'home';
} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) {
@ -259,6 +261,12 @@ export class NoteCreateService implements OnApplicationShutdown {
}
}
const inSilencedInstance = this.utilityService.isSilencedHost(meta.silencedHosts, user.host);
if (data.visibility === 'public' && inSilencedInstance && user.host !== null) {
data.visibility = 'home';
}
if (data.renote) {
switch (data.renote.visibility) {
case 'public':
@ -305,25 +313,6 @@ export class NoteCreateService implements OnApplicationShutdown {
data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH);
}
data.text = data.text.trim();
if (user.isCat) {
patsedText = patsedText ?? mfm.parse(data.text);
function nyaizeNode(node: mfm.MfmNode) {
if (node.type === 'quote') return;
if (node.type === 'text') {
node.props.text = nyaize(node.props.text);
}
if (node.children) {
for (const child of node.children) {
nyaizeNode(child);
}
}
}
for (const node of patsedText) {
nyaizeNode(node);
}
data.text = mfm.toString(patsedText);
}
} else {
data.text = null;
}
@ -334,7 +323,7 @@ export class NoteCreateService implements OnApplicationShutdown {
// Parse MFM if needed
if (!tags || !emojis || !mentionedUsers) {
const tokens = patsedText ?? (data.text ? mfm.parse(data.text)! : []);
const tokens = (data.text ? mfm.parse(data.text)! : []);
const cwTokens = data.cw ? mfm.parse(data.cw)! : [];
const choiceTokens = data.poll && data.poll.choices
? concat(data.poll.choices.map(choice => mfm.parse(choice)!))
@ -349,7 +338,7 @@ export class NoteCreateService implements OnApplicationShutdown {
mentionedUsers = data.apMentions ?? await this.extractMentionedUsers(user, combinedTokens);
}
tags = tags.filter(tag => Array.from(tag ?? '').length <= 128).splice(0, 32);
tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32);
if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply!.userId)) {
mentionedUsers.push(await this.usersRepository.findOneByOrFail({ id: data.reply!.userId }));
@ -574,7 +563,7 @@ export class NoteCreateService implements OnApplicationShutdown {
}
// Pack the note
const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true });
const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true, withReactionAndUserPairCache: true });
this.globalEventService.publishNotesStream(noteObj);
@ -841,9 +830,9 @@ export class NoteCreateService implements OnApplicationShutdown {
const r = this.redisForTimelines.pipeline();
if (note.channelId) {
this.redisTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r);
this.funoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r);
this.redisTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
this.funoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
const channelFollowings = await this.channelFollowingsRepository.find({
where: {
@ -853,9 +842,9 @@ export class NoteCreateService implements OnApplicationShutdown {
});
for (const channelFollowing of channelFollowings) {
this.redisTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
this.funoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
if (note.fileIds.length > 0) {
this.redisTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
this.funoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
}
}
} else {
@ -893,9 +882,9 @@ export class NoteCreateService implements OnApplicationShutdown {
if (!following.withReplies) continue;
}
this.redisTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
this.funoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r);
if (note.fileIds.length > 0) {
this.redisTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
this.funoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
}
}
@ -911,36 +900,36 @@ export class NoteCreateService implements OnApplicationShutdown {
if (!userListMembership.withReplies) continue;
}
this.redisTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r);
this.funoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r);
if (note.fileIds.length > 0) {
this.redisTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r);
this.funoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r);
}
}
if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { // 自分自身のHTL
this.redisTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r);
this.funoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r);
if (note.fileIds.length > 0) {
this.redisTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
this.funoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r);
}
}
// 自分自身以外への返信
if (note.replyId && note.replyUserId !== note.userId) {
this.redisTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
this.funoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
if (note.visibility === 'public' && note.userHost == null) {
this.redisTimelineService.push('localTimelineWithReplies', note.id, 300, r);
this.funoutTimelineService.push('localTimelineWithReplies', note.id, 300, r);
}
} else {
this.redisTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
this.funoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r);
if (note.fileIds.length > 0) {
this.redisTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r);
this.funoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r);
}
if (note.visibility === 'public' && note.userHost == null) {
this.redisTimelineService.push('localTimeline', note.id, 1000, r);
this.funoutTimelineService.push('localTimeline', note.id, 1000, r);
if (note.fileIds.length > 0) {
this.redisTimelineService.push('localTimelineWithFiles', note.id, 500, r);
this.funoutTimelineService.push('localTimelineWithFiles', note.id, 500, r);
}
}
}

View file

@ -237,10 +237,11 @@ export class QueueService {
}
@bindThis
public createImportFollowingJob(user: ThinUser, fileId: MiDriveFile['id']) {
public createImportFollowingJob(user: ThinUser, fileId: MiDriveFile['id'], withReplies?: boolean) {
return this.dbQueue.add('importFollowing', {
user: { id: user.id },
fileId: fileId,
withReplies,
}, {
removeOnComplete: true,
removeOnFail: true,
@ -248,8 +249,8 @@ export class QueueService {
}
@bindThis
public createImportFollowingToDbJob(user: ThinUser, targets: string[]) {
const jobs = targets.map(rel => this.generateToDbJobData('importFollowingToDb', { user, target: rel }));
public createImportFollowingToDbJob(user: ThinUser, targets: string[], withReplies?: boolean) {
const jobs = targets.map(rel => this.generateToDbJobData('importFollowingToDb', { user, target: rel, withReplies }));
return this.dbQueue.addBulk(jobs);
}
@ -342,7 +343,7 @@ export class QueueService {
}
@bindThis
public createFollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string, silent?: boolean }[]) {
public createFollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string, silent?: boolean, withReplies?: boolean }[]) {
const jobs = followings.map(rel => this.generateRelationshipJobData('follow', rel));
return this.relationshipQueue.addBulk(jobs);
}
@ -384,6 +385,7 @@ export class QueueService {
to: { id: data.to.id },
silent: data.silent,
requestId: data.requestId,
withReplies: data.withReplies,
},
opts: {
removeOnComplete: true,

View file

@ -30,6 +30,7 @@ import { RoleService } from '@/core/RoleService.js';
import { FeaturedService } from '@/core/FeaturedService.js';
const FALLBACK = '❤';
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
const legacies: Record<string, string> = {
'like': '👍',
@ -148,7 +149,7 @@ export class ReactionService {
reaction = FALLBACK;
}
} else {
reaction = this.normalize(reaction ?? null);
reaction = this.normalize(reaction);
}
}
@ -187,6 +188,9 @@ export class ReactionService {
await this.notesRepository.createQueryBuilder().update()
.set({
reactions: () => sql,
...(note.reactionAndUserPairCache.length < PER_NOTE_REACTION_USER_PAIR_CACHE_MAX ? {
reactionAndUserPairCache: () => `array_append("reactionAndUserPairCache", '${user.id}/${reaction}')`,
} : {}),
})
.where('id = :id', { id: note.id })
.execute();
@ -293,6 +297,7 @@ export class ReactionService {
await this.notesRepository.createQueryBuilder().update()
.set({
reactions: () => sql,
reactionAndUserPairCache: () => `array_remove("reactionAndUserPairCache", '${user.id}/${exist.reaction}')`,
})
.where('id = :id', { id: note.id })
.execute();

View file

@ -20,7 +20,7 @@ import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import type { Packed } from '@/misc/json-schema.js';
import { RedisTimelineService } from '@/core/RedisTimelineService.js';
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
import type { OnApplicationShutdown } from '@nestjs/common';
export type RolePolicies = {
@ -105,7 +105,7 @@ export class RoleService implements OnApplicationShutdown {
private globalEventService: GlobalEventService,
private idService: IdService,
private moderationLogService: ModerationLogService,
private redisTimelineService: RedisTimelineService,
private funoutTimelineService: FunoutTimelineService,
) {
//this.onMessage = this.onMessage.bind(this);
@ -473,7 +473,7 @@ export class RoleService implements OnApplicationShutdown {
const redisPipeline = this.redisClient.pipeline();
for (const role of roles) {
this.redisTimelineService.push(`roleTimeline:${role.id}`, note.id, 1000, redisPipeline);
this.funoutTimelineService.push(`roleTimeline:${role.id}`, note.id, 1000, redisPipeline);
this.globalEventService.publishRoleTimelineStream(role.id, 'note', note);
}

View file

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable, OnModuleInit, forwardRef } from '@nestjs/common';
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { IsNull } from 'typeorm';
import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js';
@ -28,6 +28,8 @@ import { MetaService } from '@/core/MetaService.js';
import { CacheService } from '@/core/CacheService.js';
import type { Config } from '@/config.js';
import { AccountMoveService } from '@/core/AccountMoveService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
import Logger from '../logger.js';
const logger = new Logger('following/create');
@ -71,6 +73,7 @@ export class UserFollowingService implements OnModuleInit {
private instancesRepository: InstancesRepository,
private cacheService: CacheService,
private utilityService: UtilityService,
private userEntityService: UserEntityService,
private idService: IdService,
private queueService: QueueService,
@ -81,6 +84,7 @@ export class UserFollowingService implements OnModuleInit {
private webhookService: WebhookService,
private apRendererService: ApRendererService,
private accountMoveService: AccountMoveService,
private funoutTimelineService: FunoutTimelineService,
private perUserFollowingChart: PerUserFollowingChart,
private instanceChart: InstanceChart,
) {
@ -91,7 +95,15 @@ export class UserFollowingService implements OnModuleInit {
}
@bindThis
public async follow(_follower: { id: MiUser['id'] }, _followee: { id: MiUser['id'] }, requestId?: string, silent = false): Promise<void> {
public async follow(
_follower: { id: MiUser['id'] },
_followee: { id: MiUser['id'] },
{ requestId, silent = false, withReplies }: {
requestId?: string,
silent?: boolean,
withReplies?: boolean,
} = {},
): Promise<void> {
const [follower, followee] = await Promise.all([
this.usersRepository.findOneByOrFail({ id: _follower.id }),
this.usersRepository.findOneByOrFail({ id: _followee.id }),
@ -118,15 +130,16 @@ export class UserFollowingService implements OnModuleInit {
}
const followeeProfile = await this.userProfilesRepository.findOneByOrFail({ userId: followee.id });
// フォロー対象が鍵アカウントである or
// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである
// フォロワーがローカルユーザーであり、フォロー対象がリモートユーザーである or
// フォロワーがローカルユーザーであり、フォロー対象がサイレンスされているサーバーである
// 上記のいずれかに当てはまる場合はすぐフォローせずにフォローリクエストを発行しておく
if (
followee.isLocked ||
(followeeProfile.carefulBot && follower.isBot) ||
(this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee) && process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING !== 'true')
(this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee) && process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING !== 'true') ||
(this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower) && this.utilityService.isSilencedHost((await this.metaService.fetch()).silencedHosts, follower.host))
) {
let autoAccept = false;
@ -168,12 +181,12 @@ export class UserFollowingService implements OnModuleInit {
}
if (!autoAccept) {
await this.createFollowRequest(follower, followee, requestId);
await this.createFollowRequest(follower, followee, requestId, withReplies);
return;
}
}
await this.insertFollowingDoc(followee, follower, silent);
await this.insertFollowingDoc(followee, follower, silent, withReplies);
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee));
@ -190,6 +203,7 @@ export class UserFollowingService implements OnModuleInit {
id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox']
},
silent = false,
withReplies?: boolean,
): Promise<void> {
if (follower.id === followee.id) return;
@ -199,6 +213,7 @@ export class UserFollowingService implements OnModuleInit {
id: this.idService.gen(),
followerId: follower.id,
followeeId: followee.id,
withReplies: withReplies,
// 非正規化
followerHost: follower.host,
@ -275,8 +290,8 @@ export class UserFollowingService implements OnModuleInit {
this.perUserFollowingChart.update(follower, followee, true);
}
// Publish follow event
if (this.userEntityService.isLocalUser(follower) && !silent) {
// Publish follow event
this.userEntityService.pack(followee.id, follower, {
detail: true,
}).then(async packed => {
@ -289,6 +304,8 @@ export class UserFollowingService implements OnModuleInit {
});
}
});
this.funoutTimelineService.purge(`homeTimeline:${follower.id}`);
}
// Publish followed event
@ -342,8 +359,8 @@ export class UserFollowingService implements OnModuleInit {
this.decrementFollowing(following.follower, following.followee);
// Publish unfollow event
if (!silent && this.userEntityService.isLocalUser(follower)) {
// Publish unfollow event
this.userEntityService.pack(followee.id, follower, {
detail: true,
}).then(async packed => {
@ -356,6 +373,8 @@ export class UserFollowingService implements OnModuleInit {
});
}
});
this.funoutTimelineService.purge(`homeTimeline:${follower.id}`);
}
if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
@ -451,6 +470,7 @@ export class UserFollowingService implements OnModuleInit {
id: MiUser['id']; host: MiUser['host']; uri: MiUser['host']; inbox: MiUser['inbox']; sharedInbox: MiUser['sharedInbox'];
},
requestId?: string,
withReplies?: boolean,
): Promise<void> {
if (follower.id === followee.id) return;
@ -468,6 +488,7 @@ export class UserFollowingService implements OnModuleInit {
followerId: follower.id,
followeeId: followee.id,
requestId,
withReplies,
// 非正規化
followerHost: follower.host,
@ -552,7 +573,7 @@ export class UserFollowingService implements OnModuleInit {
throw new IdentifiableError('8884c2dd-5795-4ac9-b27e-6a01d38190f9', 'No follow request.');
}
await this.insertFollowingDoc(followee, follower);
await this.insertFollowingDoc(followee, follower, false, request.withReplies);
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee as MiPartialLocalUser, request.requestId!), followee));
@ -692,4 +713,12 @@ export class UserFollowingService implements OnModuleInit {
});
}
}
@bindThis
public getFollowees(userId: MiUser['id']) {
return this.followingsRepository.createQueryBuilder('following')
.select('following.followeeId')
.where('following.followerId = :followerId', { followerId: userId })
.getMany();
}
}

View file

@ -35,6 +35,12 @@ export class UtilityService {
return blockedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
}
@bindThis
public isSilencedHost(silencedHosts: string[] | undefined, host: string | null): boolean {
if (!silencedHosts || host == null) return false;
return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
}
@bindThis
public extractDbHost(uri: string): string {
const url = new URL(uri);

View file

@ -164,7 +164,7 @@ export class ApInboxService {
}
// don't queue because the sender may attempt again when timeout
await this.userFollowingService.follow(actor, followee, activity.id);
await this.userFollowingService.follow(actor, followee, { requestId: activity.id });
return 'ok';
}

View file

@ -90,7 +90,7 @@ export class DriveFileEntityService {
if (file.type.startsWith('video')) {
if (file.thumbnailUrl) return file.thumbnailUrl;
return this.videoProcessingService.getExternalVideoThumbnailUrl(file.webpublicUrl ?? file.url ?? file.uri);
return this.videoProcessingService.getExternalVideoThumbnailUrl(file.webpublicUrl ?? file.url);
} else if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) {
// 動画ではなくリモートかつメディアプロキシ
return this.getProxiedUrl(file.uri, 'static');
@ -145,7 +145,7 @@ export class DriveFileEntityService {
.select('SUM(file.size)', 'sum')
.getRawOne();
return parseInt(sum, 10) ?? 0;
return parseInt(sum, 10) || 0;
}
@bindThis
@ -157,7 +157,7 @@ export class DriveFileEntityService {
.select('SUM(file.size)', 'sum')
.getRawOne();
return parseInt(sum, 10) ?? 0;
return parseInt(sum, 10) || 0;
}
@bindThis
@ -169,7 +169,7 @@ export class DriveFileEntityService {
.select('SUM(file.size)', 'sum')
.getRawOne();
return parseInt(sum, 10) ?? 0;
return parseInt(sum, 10) || 0;
}
@bindThis
@ -181,7 +181,7 @@ export class DriveFileEntityService {
.select('SUM(file.size)', 'sum')
.getRawOne();
return parseInt(sum, 10) ?? 0;
return parseInt(sum, 10) || 0;
}
@bindThis

View file

@ -3,9 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
import type { } from '@/models/Blocking.js';
import type { MiInstance } from '@/models/Instance.js';
import { MetaService } from '@/core/MetaService.js';
import { bindThis } from '@/decorators.js';
@ -43,6 +42,7 @@ export class InstanceEntityService {
description: instance.description,
maintainerName: instance.maintainerName,
maintainerEmail: instance.maintainerEmail,
isSilenced: this.utilityService.isSilencedHost(meta.silencedHosts, instance.host),
iconUrl: instance.iconUrl,
faviconUrl: instance.faviconUrl,
themeColor: instance.themeColor,

View file

@ -170,27 +170,37 @@ export class NoteEntityService implements OnModuleInit {
}
@bindThis
public async populateMyReaction(noteId: MiNote['id'], meId: MiUser['id'], _hint_?: {
myReactions: Map<MiNote['id'], MiNoteReaction | null>;
public async populateMyReaction(note: { id: MiNote['id']; reactions: MiNote['reactions']; reactionAndUserPairCache?: MiNote['reactionAndUserPairCache']; }, meId: MiUser['id'], _hint_?: {
myReactions: Map<MiNote['id'], string | null>;
}) {
if (_hint_?.myReactions) {
const reaction = _hint_.myReactions.get(noteId);
const reaction = _hint_.myReactions.get(note.id);
if (reaction) {
return this.reactionService.convertLegacyReaction(reaction.reaction);
} else if (reaction === null) {
return this.reactionService.convertLegacyReaction(reaction);
} else {
return undefined;
}
}
const reactionsCount = Object.values(note.reactions).reduce((a, b) => a + b, 0);
if (reactionsCount === 0) return undefined;
if (note.reactionAndUserPairCache && reactionsCount <= note.reactionAndUserPairCache.length) {
const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId));
if (pair) {
return this.reactionService.convertLegacyReaction(pair.split('/')[1]);
} else {
return undefined;
}
// 実装上抜けがあるだけかもしれないので、「ヒントに含まれてなかったら(=undefinedなら)return」のようにはしない
}
// パフォーマンスのためートが作成されてから2秒以上経っていない場合はリアクションを取得しない
if (this.idService.parse(noteId).date.getTime() + 2000 > Date.now()) {
if (this.idService.parse(note.id).date.getTime() + 2000 > Date.now()) {
return undefined;
}
const reaction = await this.noteReactionsRepository.findOneBy({
userId: meId,
noteId: noteId,
noteId: note.id,
});
if (reaction) {
@ -276,8 +286,9 @@ export class NoteEntityService implements OnModuleInit {
options?: {
detail?: boolean;
skipHide?: boolean;
withReactionAndUserPairCache?: boolean;
_hint_?: {
myReactions: Map<MiNote['id'], MiNoteReaction | null>;
myReactions: Map<MiNote['id'], string | null>;
packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>;
};
},
@ -285,6 +296,7 @@ export class NoteEntityService implements OnModuleInit {
const opts = Object.assign({
detail: true,
skipHide: false,
withReactionAndUserPairCache: false,
}, options);
const meId = me ? me.id : null;
@ -318,13 +330,14 @@ export class NoteEntityService implements OnModuleInit {
text: text,
cw: note.cw,
visibility: note.visibility,
localOnly: note.localOnly ?? undefined,
localOnly: note.localOnly,
reactionAcceptance: note.reactionAcceptance,
visibleUserIds: note.visibility === 'specified' ? note.visibleUserIds : undefined,
renoteCount: note.renoteCount,
repliesCount: note.repliesCount,
reactions: this.reactionService.convertLegacyReactions(note.reactions),
reactionEmojis: this.customEmojiService.populateEmojis(reactionEmojiNames, host),
reactionAndUserPairCache: opts.withReactionAndUserPairCache ? note.reactionAndUserPairCache : undefined,
emojis: host != null ? this.customEmojiService.populateEmojis(note.emojis, host) : undefined,
tags: note.tags.length > 0 ? note.tags : undefined,
fileIds: note.fileIds,
@ -347,18 +360,20 @@ export class NoteEntityService implements OnModuleInit {
reply: note.replyId ? this.pack(note.reply ?? note.replyId, me, {
detail: false,
withReactionAndUserPairCache: opts.withReactionAndUserPairCache,
_hint_: options?._hint_,
}) : undefined,
renote: note.renoteId ? this.pack(note.renote ?? note.renoteId, me, {
detail: true,
withReactionAndUserPairCache: opts.withReactionAndUserPairCache,
_hint_: options?._hint_,
}) : undefined,
poll: note.hasPoll ? this.populatePoll(note, meId) : undefined,
...(meId ? {
myReaction: this.populateMyReaction(note.id, meId, options?._hint_),
...(meId && Object.keys(note.reactions).length > 0 ? {
myReaction: this.populateMyReaction(note, meId, options?._hint_),
} : {}),
} : {}),
});
@ -382,19 +397,48 @@ export class NoteEntityService implements OnModuleInit {
if (notes.length === 0) return [];
const meId = me ? me.id : null;
const myReactionsMap = new Map<MiNote['id'], MiNoteReaction | null>();
const myReactionsMap = new Map<MiNote['id'], string | null>();
if (meId) {
const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!);
const idsNeedFetchMyReaction = new Set<MiNote['id']>();
// パフォーマンスのためートが作成されてから2秒以上経っていない場合はリアクションを取得しない
const oldId = this.idService.gen(Date.now() - 2000);
const targets = [...notes.filter(n => n.id < oldId).map(n => n.id), ...renoteIds];
const myReactions = await this.noteReactionsRepository.findBy({
userId: meId,
noteId: In(targets),
});
for (const target of targets) {
myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) ?? null);
for (const note of notes) {
if (note.renote && (note.text == null && note.fileIds.length === 0)) { // pure renote
const reactionsCount = Object.values(note.renote.reactions).reduce((a, b) => a + b, 0);
if (reactionsCount === 0) {
myReactionsMap.set(note.renote.id, null);
} else if (reactionsCount <= note.renote.reactionAndUserPairCache.length) {
const pair = note.renote.reactionAndUserPairCache.find(p => p.startsWith(meId));
myReactionsMap.set(note.renote.id, pair ? pair.split('/')[1] : null);
} else {
idsNeedFetchMyReaction.add(note.renote.id);
}
} else {
if (note.id < oldId) {
const reactionsCount = Object.values(note.reactions).reduce((a, b) => a + b, 0);
if (reactionsCount === 0) {
myReactionsMap.set(note.id, null);
} else if (reactionsCount <= note.reactionAndUserPairCache.length) {
const pair = note.reactionAndUserPairCache.find(p => p.startsWith(meId));
myReactionsMap.set(note.id, pair ? pair.split('/')[1] : null);
} else {
idsNeedFetchMyReaction.add(note.id);
}
} else {
myReactionsMap.set(note.id, null);
}
}
}
const myReactions = idsNeedFetchMyReaction.size > 0 ? await this.noteReactionsRepository.findBy({
userId: meId,
noteId: In(Array.from(idsNeedFetchMyReaction)),
}) : [];
for (const id of idsNeedFetchMyReaction) {
myReactionsMap.set(id, myReactions.find(reaction => reaction.noteId === id)?.reaction ?? null);
}
}

View file

@ -333,8 +333,8 @@ export class UserEntityService implements OnModuleInit {
host: user.host,
avatarUrl: user.avatarUrl ?? this.getIdenticonUrl(user),
avatarBlurhash: user.avatarBlurhash,
isBot: user.isBot ?? falsy,
isCat: user.isCat ?? falsy,
isBot: user.isBot,
isCat: user.isCat,
instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {
name: instance.name,
softwareName: instance.softwareName,
@ -367,7 +367,7 @@ export class UserEntityService implements OnModuleInit {
bannerBlurhash: user.bannerBlurhash,
isLocked: user.isLocked,
isSilenced: this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
isSuspended: user.isSuspended ?? falsy,
isSuspended: user.isSuspended,
description: profile!.description,
location: profile!.location,
birthday: profile!.birthday,