perf(server): refactor and performance improvements

This commit is contained in:
syuilo 2022-03-25 16:27:41 +09:00
parent 22b56ac65c
commit ac8c66f5ab
71 changed files with 289 additions and 189 deletions

View file

@ -1,5 +1,5 @@
export class Cache<T> {
private cache: Map<string | null, { date: number; value: T; }>;
public cache: Map<string | null, { date: number; value: T; }>;
private lifetime: number;
constructor(lifetime: Cache<never>['lifetime']) {

View file

@ -234,3 +234,9 @@ export interface ILocalUser extends User {
export interface IRemoteUser extends User {
host: string;
}
export type CacheableLocalUser = ILocalUser;
export type CacheableRemoteUser = IRemoteUser;
export type CacheableUser = CacheableLocalUser | CacheableRemoteUser;

View file

@ -15,6 +15,8 @@ import DbResolver from '@/remote/activitypub/db-resolver.js';
import { resolvePerson } from '@/remote/activitypub/models/person.js';
import { LdSignature } from '@/remote/activitypub/misc/ld-signature.js';
import { StatusError } from '@/misc/fetch.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { UserPublickey } from '@/models/entities/user-publickey.js';
const logger = new Logger('inbox');
@ -42,11 +44,13 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
return `Old keyId is no longer supported. ${keyIdLower}`;
}
// TDOO: キャッシュ
const dbResolver = new DbResolver();
// HTTP-Signature keyIdを元にDBから取得
let authUser = await dbResolver.getAuthUserFromKeyId(signature.keyId);
let authUser: {
user: CacheableRemoteUser;
key: UserPublickey | null;
} | null = await dbResolver.getAuthUserFromKeyId(signature.keyId);
// keyIdでわからなければ、activity.actorを元にDBから取得 || activity.actorを元にリモートから取得
if (authUser == null) {

View file

@ -3,26 +3,26 @@ import Resolver from './resolver.js';
import { resolvePerson } from './models/person.js';
import { unique, concat } from '@/prelude/array.js';
import promiseLimit from 'promise-limit';
import { User, IRemoteUser } from '@/models/entities/user.js';
import { User, CacheableRemoteUser, CacheableUser } from '@/models/entities/user.js';
type Visibility = 'public' | 'home' | 'followers' | 'specified';
type AudienceInfo = {
visibility: Visibility,
mentionedUsers: User[],
visibleUsers: User[],
mentionedUsers: CacheableUser[],
visibleUsers: CacheableUser[],
};
export async function parseAudience(actor: IRemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise<AudienceInfo> {
export async function parseAudience(actor: CacheableRemoteUser, to?: ApObject, cc?: ApObject, resolver?: Resolver): Promise<AudienceInfo> {
const toGroups = groupingAudience(getApIds(to), actor);
const ccGroups = groupingAudience(getApIds(cc), actor);
const others = unique(concat([toGroups.other, ccGroups.other]));
const limit = promiseLimit<User | null>(2);
const limit = promiseLimit<CacheableUser | null>(2);
const mentionedUsers = (await Promise.all(
others.map(id => limit(() => resolvePerson(id, resolver).catch(() => null)))
)).filter((x): x is User => x != null);
)).filter((x): x is CacheableUser => x != null);
if (toGroups.public.length > 0) {
return {
@ -55,7 +55,7 @@ export async function parseAudience(actor: IRemoteUser, to?: ApObject, cc?: ApOb
};
}
function groupingAudience(ids: string[], actor: IRemoteUser) {
function groupingAudience(ids: string[], actor: CacheableRemoteUser) {
const groups = {
public: [] as string[],
followers: [] as string[],
@ -85,7 +85,7 @@ function isPublic(id: string) {
].includes(id);
}
function isFollowers(id: string, actor: IRemoteUser) {
function isFollowers(id: string, actor: CacheableRemoteUser) {
return (
id === (actor.followersUri || `${actor.uri}/followers`)
);

View file

@ -1,12 +1,17 @@
import escapeRegexp from 'escape-regexp';
import config from '@/config/index.js';
import { Note } from '@/models/entities/note.js';
import { User, IRemoteUser } from '@/models/entities/user.js';
import { User, IRemoteUser, CacheableRemoteUser } from '@/models/entities/user.js';
import { UserPublickey } from '@/models/entities/user-publickey.js';
import { MessagingMessage } from '@/models/entities/messaging-message.js';
import { Notes, Users, UserPublickeys, MessagingMessages } from '@/models/index.js';
import { IObject, getApId } from './type.js';
import { resolvePerson } from './models/person.js';
import escapeRegexp from 'escape-regexp';
import { Cache } from '@/misc/cache.js';
import { userByIdCache } from '@/services/user-cache.js';
const publicKeyCache = new Cache<UserPublickey | null>(Infinity);
const publicKeyByUserIdCache = new Cache<UserPublickey | null>(Infinity);
export default class DbResolver {
constructor() {
@ -75,17 +80,24 @@ export default class DbResolver {
/**
* AP KeyId => Misskey User and Key
*/
public async getAuthUserFromKeyId(keyId: string): Promise<AuthUser | null> {
const key = await UserPublickeys.findOne({
keyId,
}, {
relations: ['user'],
});
public async getAuthUserFromKeyId(keyId: string): Promise<{
user: CacheableRemoteUser;
key: UserPublickey;
} | null> {
const key = await publicKeyCache.fetch(keyId, async () => {
const key = await UserPublickeys.findOne({
keyId,
});
if (key == null) return null;
return key;
}, key => key != null);
if (key == null) return null;
return {
user: key.user as IRemoteUser,
user: await userByIdCache.fetch(key.userId, () => Users.findOneOrFail(key.userId)) as CacheableRemoteUser,
key,
};
}
@ -93,12 +105,15 @@ export default class DbResolver {
/**
* AP Actor id => Misskey User and Key
*/
public async getAuthUserFromApId(uri: string): Promise<AuthUser | null> {
const user = await resolvePerson(uri) as IRemoteUser;
public async getAuthUserFromApId(uri: string): Promise<{
user: CacheableRemoteUser;
key: UserPublickey | null;
} | null> {
const user = await resolvePerson(uri) as CacheableRemoteUser;
if (user == null) return null;
const key = await UserPublickeys.findOne(user.id);
const key = await publicKeyByUserIdCache.fetch(user.id, () => UserPublickeys.findOne(user.id).then(x => x || null), v => v != null); // TODO: typeorm 3.0 にしたら.then(x => x || null)は消せる
return {
user,
@ -125,11 +140,6 @@ export default class DbResolver {
}
}
export type AuthUser = {
user: IRemoteUser;
key?: UserPublickey;
};
type UriParseResult = {
/** id in DB (local object only) */
id?: string;

View file

@ -112,7 +112,7 @@ export default class DeliverManager {
* @param activity Activity
* @param from Followee
*/
export async function deliverToFollowers(actor: ILocalUser, activity: any) {
export async function deliverToFollowers(actor: { id: ILocalUser['id']; host: null; }, activity: any) {
const manager = new DeliverManager(actor, activity);
manager.addFollowersRecipe();
await manager.execute();
@ -123,7 +123,7 @@ export async function deliverToFollowers(actor: ILocalUser, activity: any) {
* @param activity Activity
* @param to Target user
*/
export async function deliverToUser(actor: ILocalUser, activity: any, to: IRemoteUser) {
export async function deliverToUser(actor: { id: ILocalUser['id']; host: null; }, activity: any, to: IRemoteUser) {
const manager = new DeliverManager(actor, activity);
manager.addDirectRecipe(to);
await manager.execute();

View file

@ -1,10 +1,10 @@
import { IRemoteUser } from '@/models/entities/user.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import accept from '@/services/following/requests/accept.js';
import { IFollow } from '../../type.js';
import DbResolver from '../../db-resolver.js';
import { relayAccepted } from '@/services/relay.js';
export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
export default async (actor: CacheableRemoteUser, activity: IFollow): Promise<string> => {
// ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
const dbResolver = new DbResolver();

View file

@ -1,12 +1,12 @@
import Resolver from '../../resolver.js';
import { IRemoteUser } from '@/models/entities/user.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import acceptFollow from './follow.js';
import { IAccept, isFollow, getApType } from '../../type.js';
import { apLogger } from '../../logger.js';
const logger = apLogger;
export default async (actor: IRemoteUser, activity: IAccept): Promise<string> => {
export default async (actor: CacheableRemoteUser, activity: IAccept): Promise<string> => {
const uri = activity.id || activity;
logger.info(`Accept: ${uri}`);

View file

@ -1,9 +1,9 @@
import { IRemoteUser } from '@/models/entities/user.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { IAdd } from '../../type.js';
import { resolveNote } from '../../models/note.js';
import { addPinned } from '@/services/i/pin.js';
export default async (actor: IRemoteUser, activity: IAdd): Promise<void> => {
export default async (actor: CacheableRemoteUser, activity: IAdd): Promise<void> => {
if ('actor' in activity && actor.uri !== activity.actor) {
throw new Error('invalid actor');
}

View file

@ -1,12 +1,12 @@
import Resolver from '../../resolver.js';
import { IRemoteUser } from '@/models/entities/user.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import announceNote from './note.js';
import { IAnnounce, getApId } from '../../type.js';
import { apLogger } from '../../logger.js';
const logger = apLogger;
export default async (actor: IRemoteUser, activity: IAnnounce): Promise<void> => {
export default async (actor: CacheableRemoteUser, activity: IAnnounce): Promise<void> => {
const uri = getApId(activity);
logger.info(`Announce: ${uri}`);

View file

@ -1,6 +1,6 @@
import Resolver from '../../resolver.js';
import post from '@/services/note/create.js';
import { IRemoteUser } from '@/models/entities/user.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { IAnnounce, getApId } from '../../type.js';
import { fetchNote, resolveNote } from '../../models/note.js';
import { apLogger } from '../../logger.js';
@ -15,10 +15,9 @@ const logger = apLogger;
/**
*
*/
export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, targetUri: string): Promise<void> {
export default async function(resolver: Resolver, actor: CacheableRemoteUser, activity: IAnnounce, targetUri: string): Promise<void> {
const uri = getApId(activity);
// アナウンサーが凍結されていたらスキップ
if (actor.isSuspended) {
return;
}

View file

@ -1,9 +1,10 @@
import { IBlock } from '../../type.js';
import block from '@/services/blocking/create.js';
import { IRemoteUser } from '@/models/entities/user.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import DbResolver from '../../db-resolver.js';
import { Users } from '@/models/index.js';
export default async (actor: IRemoteUser, activity: IBlock): Promise<string> => {
export default async (actor: CacheableRemoteUser, activity: IBlock): Promise<string> => {
// ※ activity.objectにブロック対象があり、それは存在するローカルユーザーのはず
const dbResolver = new DbResolver();
@ -17,6 +18,6 @@ export default async (actor: IRemoteUser, activity: IBlock): Promise<string> =>
return `skip: ブロックしようとしているユーザーはローカルユーザーではありません`;
}
await block(actor, blockee);
await block(await Users.findOneOrFail(actor.id), blockee);
return `ok`;
};

View file

@ -1,5 +1,5 @@
import Resolver from '../../resolver.js';
import { IRemoteUser } from '@/models/entities/user.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import createNote from './note.js';
import { ICreate, getApId, isPost, getApType } from '../../type.js';
import { apLogger } from '../../logger.js';
@ -7,7 +7,7 @@ import { toArray, concat, unique } from '@/prelude/array.js';
const logger = apLogger;
export default async (actor: IRemoteUser, activity: ICreate): Promise<void> => {
export default async (actor: CacheableRemoteUser, activity: ICreate): Promise<void> => {
const uri = getApId(activity);
logger.info(`Create: ${uri}`);

View file

@ -1,5 +1,5 @@
import Resolver from '../../resolver.js';
import { IRemoteUser } from '@/models/entities/user.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { createNote, fetchNote } from '../../models/note.js';
import { getApId, IObject, ICreate } from '../../type.js';
import { getApLock } from '@/misc/app-lock.js';
@ -9,7 +9,7 @@ import { StatusError } from '@/misc/fetch.js';
/**
* 稿
*/
export default async function(resolver: Resolver, actor: IRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise<string> {
export default async function(resolver: Resolver, actor: CacheableRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise<string> {
const uri = getApId(note);
if (typeof note === 'object') {

View file

@ -1,18 +1,19 @@
import { apLogger } from '../../logger.js';
import { createDeleteAccountJob } from '@/queue/index.js';
import { IRemoteUser } from '@/models/entities/user.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { Users } from '@/models/index.js';
const logger = apLogger;
export async function deleteActor(actor: IRemoteUser, uri: string): Promise<string> {
export async function deleteActor(actor: CacheableRemoteUser, uri: string): Promise<string> {
logger.info(`Deleting the Actor: ${uri}`);
if (actor.uri !== uri) {
return `skip: delete actor ${actor.uri} !== ${uri}`;
}
if (actor.isDeleted) {
const user = await Users.findOneOrFail(actor.id);
if (user.isDeleted) {
logger.info(`skip: already deleted`);
}

View file

@ -1,5 +1,5 @@
import deleteNote from './note.js';
import { IRemoteUser } from '@/models/entities/user.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { IDelete, getApId, isTombstone, IObject, validPost, validActor } from '../../type.js';
import { toSingle } from '@/prelude/array.js';
import { deleteActor } from './actor.js';
@ -7,7 +7,7 @@ import { deleteActor } from './actor.js';
/**
*
*/
export default async (actor: IRemoteUser, activity: IDelete): Promise<string> => {
export default async (actor: CacheableRemoteUser, activity: IDelete): Promise<string> => {
if ('actor' in activity && actor.uri !== activity.actor) {
throw new Error('invalid actor');
}

View file

@ -1,4 +1,4 @@
import { IRemoteUser } from '@/models/entities/user.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import deleteNode from '@/services/note/delete.js';
import { apLogger } from '../../logger.js';
import DbResolver from '../../db-resolver.js';
@ -7,7 +7,7 @@ import { deleteMessage } from '@/services/messages/delete.js';
const logger = apLogger;
export default async function(actor: IRemoteUser, uri: string): Promise<string> {
export default async function(actor: CacheableRemoteUser, uri: string): Promise<string> {
logger.info(`Deleting the Note: ${uri}`);
const unlock = await getApLock(uri);

View file

@ -1,11 +1,11 @@
import { IRemoteUser } from '@/models/entities/user.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import config from '@/config/index.js';
import { IFlag, getApIds } from '../../type.js';
import { AbuseUserReports, Users } from '@/models/index.js';
import { In } from 'typeorm';
import { genId } from '@/misc/gen-id.js';
export default async (actor: IRemoteUser, activity: IFlag): Promise<string> => {
export default async (actor: CacheableRemoteUser, activity: IFlag): Promise<string> => {
// objectは `(User|Note) | (User|Note)[]` だけど、全パターンDBスキーマと対応させられないので
// 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する
const uris = getApIds(activity.object);

View file

@ -1,9 +1,9 @@
import { IRemoteUser } from '@/models/entities/user.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import follow from '@/services/following/create.js';
import { IFollow } from '../type.js';
import DbResolver from '../db-resolver.js';
export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
export default async (actor: CacheableRemoteUser, activity: IFollow): Promise<string> => {
const dbResolver = new DbResolver();
const followee = await dbResolver.getUserFromApId(activity.object);

View file

@ -1,5 +1,5 @@
import { IObject, isCreate, isDelete, isUpdate, isRead, isFollow, isAccept, isReject, isAdd, isRemove, isAnnounce, isLike, isUndo, isBlock, isCollectionOrOrderedCollection, isCollection, isFlag } from '../type.js';
import { IRemoteUser } from '@/models/entities/user.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import create from './create/index.js';
import performDeleteActivity from './delete/index.js';
import performUpdateActivity from './update/index.js';
@ -17,8 +17,9 @@ import flag from './flag/index.js';
import { apLogger } from '../logger.js';
import Resolver from '../resolver.js';
import { toArray } from '@/prelude/array.js';
import { Users } from '@/models/index.js';
export async function performActivity(actor: IRemoteUser, activity: IObject) {
export async function performActivity(actor: CacheableRemoteUser, activity: IObject) {
if (isCollectionOrOrderedCollection(activity)) {
const resolver = new Resolver();
for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) {
@ -36,7 +37,7 @@ export async function performActivity(actor: IRemoteUser, activity: IObject) {
}
}
async function performOneActivity(actor: IRemoteUser, activity: IObject): Promise<void> {
async function performOneActivity(actor: CacheableRemoteUser, activity: IObject): Promise<void> {
if (actor.isSuspended) return;
if (isCreate(activity)) {

View file

@ -1,9 +1,9 @@
import { IRemoteUser } from '@/models/entities/user.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { ILike, getApId } from '../type.js';
import create from '@/services/note/reaction/create.js';
import { fetchNote, extractEmojis } from '../models/note.js';
export default async (actor: IRemoteUser, activity: ILike) => {
export default async (actor: CacheableRemoteUser, activity: ILike) => {
const targetUri = getApId(activity.object);
const note = await fetchNote(targetUri);

View file

@ -1,10 +1,10 @@
import { IRemoteUser } from '@/models/entities/user.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { IRead, getApId } from '../type.js';
import { isSelfHost, extractDbHost } from '@/misc/convert-host.js';
import { MessagingMessages } from '@/models/index.js';
import { readUserMessagingMessage } from '../../../server/api/common/read-messaging-message.js';
export const performReadActivity = async (actor: IRemoteUser, activity: IRead): Promise<string> => {
export const performReadActivity = async (actor: CacheableRemoteUser, activity: IRead): Promise<string> => {
const id = await getApId(activity.object);
if (!isSelfHost(extractDbHost(id))) {

View file

@ -1,11 +1,11 @@
import { IRemoteUser } from '@/models/entities/user.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { remoteReject } from '@/services/following/reject.js';
import { IFollow } from '../../type.js';
import DbResolver from '../../db-resolver.js';
import { relayRejected } from '@/services/relay.js';
import { Users } from '@/models/index.js';
export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
export default async (actor: CacheableRemoteUser, activity: IFollow): Promise<string> => {
// ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
const dbResolver = new DbResolver();

View file

@ -1,12 +1,12 @@
import Resolver from '../../resolver.js';
import { IRemoteUser } from '@/models/entities/user.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import rejectFollow from './follow.js';
import { IReject, isFollow, getApType } from '../../type.js';
import { apLogger } from '../../logger.js';
const logger = apLogger;
export default async (actor: IRemoteUser, activity: IReject): Promise<string> => {
export default async (actor: CacheableRemoteUser, activity: IReject): Promise<string> => {
const uri = activity.id || activity;
logger.info(`Reject: ${uri}`);

View file

@ -1,9 +1,9 @@
import { IRemoteUser } from '@/models/entities/user.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { IRemove } from '../../type.js';
import { resolveNote } from '../../models/note.js';
import { removePinned } from '@/services/i/pin.js';
export default async (actor: IRemoteUser, activity: IRemove): Promise<void> => {
export default async (actor: CacheableRemoteUser, activity: IRemove): Promise<void> => {
if ('actor' in activity && actor.uri !== activity.actor) {
throw new Error('invalid actor');
}

View file

@ -1,11 +1,11 @@
import unfollow from '@/services/following/delete.js';
import cancelRequest from '@/services/following/requests/cancel.js';
import {IAccept} from '../../type.js';
import { IRemoteUser } from '@/models/entities/user.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { Followings } from '@/models/index.js';
import DbResolver from '../../db-resolver.js';
export default async (actor: IRemoteUser, activity: IAccept): Promise<string> => {
export default async (actor: CacheableRemoteUser, activity: IAccept): Promise<string> => {
const dbResolver = new DbResolver();
const follower = await dbResolver.getUserFromApId(activity.object);

View file

@ -1,9 +1,9 @@
import { Notes } from '@/models/index.js';
import { IRemoteUser } from '@/models/entities/user.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { IAnnounce, getApId } from '../../type.js';
import deleteNote from '@/services/note/delete.js';
export const undoAnnounce = async (actor: IRemoteUser, activity: IAnnounce): Promise<string> => {
export const undoAnnounce = async (actor: CacheableRemoteUser, activity: IAnnounce): Promise<string> => {
const uri = getApId(activity);
const note = await Notes.findOne({

View file

@ -1,9 +1,10 @@
import { IBlock } from '../../type.js';
import unblock from '@/services/blocking/delete.js';
import { IRemoteUser } from '@/models/entities/user.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import DbResolver from '../../db-resolver.js';
import { Users } from '@/models/index.js';
export default async (actor: IRemoteUser, activity: IBlock): Promise<string> => {
export default async (actor: CacheableRemoteUser, activity: IBlock): Promise<string> => {
const dbResolver = new DbResolver();
const blockee = await dbResolver.getUserFromApId(activity.object);
@ -15,6 +16,6 @@ export default async (actor: IRemoteUser, activity: IBlock): Promise<string> =>
return `skip: ブロック解除しようとしているユーザーはローカルユーザーではありません`;
}
await unblock(actor, blockee);
await unblock(await Users.findOneOrFail(actor.id), blockee);
return `ok`;
};

View file

@ -1,11 +1,11 @@
import unfollow from '@/services/following/delete.js';
import cancelRequest from '@/services/following/requests/cancel.js';
import { IFollow } from '../../type.js';
import { IRemoteUser } from '@/models/entities/user.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { FollowRequests, Followings } from '@/models/index.js';
import DbResolver from '../../db-resolver.js';
export default async (actor: IRemoteUser, activity: IFollow): Promise<string> => {
export default async (actor: CacheableRemoteUser, activity: IFollow): Promise<string> => {
const dbResolver = new DbResolver();
const followee = await dbResolver.getUserFromApId(activity.object);

View file

@ -1,5 +1,5 @@
import { IRemoteUser } from '@/models/entities/user.js';
import {IUndo, isFollow, isBlock, isLike, isAnnounce, getApType, isAccept} from '../../type.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { IUndo, isFollow, isBlock, isLike, isAnnounce, getApType, isAccept } from '../../type.js';
import unfollow from './follow.js';
import unblock from './block.js';
import undoLike from './like.js';
@ -10,7 +10,7 @@ import { apLogger } from '../../logger.js';
const logger = apLogger;
export default async (actor: IRemoteUser, activity: IUndo): Promise<string> => {
export default async (actor: CacheableRemoteUser, activity: IUndo): Promise<string> => {
if ('actor' in activity && actor.uri !== activity.actor) {
throw new Error('invalid actor');
}

View file

@ -1,4 +1,4 @@
import { IRemoteUser } from '@/models/entities/user.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { ILike, getApId } from '../../type.js';
import deleteReaction from '@/services/note/reaction/delete.js';
import { fetchNote } from '../../models/note.js';
@ -6,7 +6,7 @@ import { fetchNote } from '../../models/note.js';
/**
* Process Undo.Like activity
*/
export default async (actor: IRemoteUser, activity: ILike) => {
export default async (actor: CacheableRemoteUser, activity: ILike) => {
const targetUri = getApId(activity.object);
const note = await fetchNote(targetUri);

View file

@ -1,4 +1,4 @@
import { IRemoteUser } from '@/models/entities/user.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { getApType, IUpdate, isActor } from '../../type.js';
import { apLogger } from '../../logger.js';
import { updateQuestion } from '../../models/question.js';
@ -8,7 +8,7 @@ import { updatePerson } from '../../models/person.js';
/**
* Updateアクティビティを捌きます
*/
export default async (actor: IRemoteUser, activity: IUpdate): Promise<string> => {
export default async (actor: CacheableRemoteUser, activity: IUpdate): Promise<string> => {
if ('actor' in activity && actor.uri !== activity.actor) {
return `skip: invalid actor`;
}

View file

@ -1,10 +1,10 @@
import { uploadFromUrl } from '@/services/drive/upload-from-url.js';
import { IRemoteUser } from '@/models/entities/user.js';
import { CacheableRemoteUser, IRemoteUser } from '@/models/entities/user.js';
import Resolver from '../resolver.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { apLogger } from '../logger.js';
import { DriveFile } from '@/models/entities/drive-file.js';
import { DriveFiles } from '@/models/index.js';
import { DriveFiles, Users } from '@/models/index.js';
import { truncate } from '@/misc/truncate.js';
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js';
@ -13,7 +13,7 @@ const logger = apLogger;
/**
* Imageを作成します
*/
export async function createImage(actor: IRemoteUser, value: any): Promise<DriveFile> {
export async function createImage(actor: CacheableRemoteUser, value: any): Promise<DriveFile> {
// 投稿者が凍結されていたらスキップ
if (actor.isSuspended) {
throw new Error('actor has been suspended');
@ -60,7 +60,7 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<Drive
* Misskeyに対象のImageが登録されていればそれを返し
* Misskeyに登録しそれを返します
*/
export async function resolveImage(actor: IRemoteUser, value: any): Promise<DriveFile> {
export async function resolveImage(actor: CacheableRemoteUser, value: any): Promise<DriveFile> {
// TODO
// リモートサーバーからフェッチしてきて登録

View file

@ -3,17 +3,17 @@ import { IObject, isMention, IApMention } from '../type.js';
import { resolvePerson } from './person.js';
import promiseLimit from 'promise-limit';
import Resolver from '../resolver.js';
import { User } from '@/models/entities/user.js';
import { CacheableUser, User } from '@/models/entities/user.js';
export async function extractApMentions(tags: IObject | IObject[] | null | undefined) {
const hrefs = unique(extractApMentionObjects(tags).map(x => x.href as string));
const resolver = new Resolver();
const limit = promiseLimit<User | null>(2);
const limit = promiseLimit<CacheableUser | null>(2);
const mentionedUsers = (await Promise.all(
hrefs.map(x => limit(() => resolvePerson(x, resolver).catch(() => null)))
)).filter((x): x is User => x != null);
)).filter((x): x is CacheableUser => x != null);
return mentionedUsers;
}

View file

@ -5,7 +5,7 @@ import Resolver from '../resolver.js';
import post from '@/services/note/create.js';
import { resolvePerson, updatePerson } from './person.js';
import { resolveImage } from './image.js';
import { IRemoteUser } from '@/models/entities/user.js';
import { CacheableRemoteUser, IRemoteUser } from '@/models/entities/user.js';
import { htmlToMfm } from '../misc/html-to-mfm.js';
import { extractApHashtags } from './tag.js';
import { unique, toArray, toSingle } from '@/prelude/array.js';
@ -15,7 +15,7 @@ import { apLogger } from '../logger.js';
import { DriveFile } from '@/models/entities/drive-file.js';
import { deliverQuestionUpdate } from '@/services/note/polls/update.js';
import { extractDbHost, toPuny } from '@/misc/convert-host.js';
import { Emojis, Polls, MessagingMessages } from '@/models/index.js';
import { Emojis, Polls, MessagingMessages, Users } from '@/models/index.js';
import { Note } from '@/models/entities/note.js';
import { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji, getApType } from '../type.js';
import { Emoji } from '@/models/entities/emoji.js';
@ -90,7 +90,7 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s
logger.info(`Creating the Note: ${note.id}`);
// 投稿者をフェッチ
const actor = await resolvePerson(getOneApId(note.attributedTo), resolver) as IRemoteUser;
const actor = await resolvePerson(getOneApId(note.attributedTo), resolver) as CacheableRemoteUser;
// 投稿者が凍結されていたらスキップ
if (actor.isSuspended) {
@ -230,11 +230,6 @@ export async function createNote(value: string | IObject, resolver?: Resolver, s
const poll = await extractPollFromQuestion(note, resolver).catch(() => undefined);
// ユーザーの情報が古かったらついでに更新しておく
if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
if (actor.uri) updatePerson(actor.uri);
}
if (isTalk) {
for (const recipient of visibleUsers) {
await createMessage(actor, recipient, undefined, text || undefined, (files && files.length > 0) ? files[0] : null, object.id);

View file

@ -15,7 +15,7 @@ import { apLogger } from '../logger.js';
import { Note } from '@/models/entities/note.js';
import { updateUsertags } from '@/services/update-hashtag.js';
import { Users, Instances, DriveFiles, Followings, UserProfiles, UserPublickeys } from '@/models/index.js';
import { User, IRemoteUser } from '@/models/entities/user.js';
import { User, IRemoteUser, CacheableUser } from '@/models/entities/user.js';
import { Emoji } from '@/models/entities/emoji.js';
import { UserNotePining } from '@/models/entities/user-note-pining.js';
import { genId } from '@/misc/gen-id.js';
@ -30,6 +30,8 @@ import { fetchInstanceMetadata } from '@/services/fetch-instance-metadata.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { truncate } from '@/misc/truncate.js';
import { StatusError } from '@/misc/fetch.js';
import { uriPersonCache } from '@/services/user-cache.js';
import { publishInternalEvent } from '@/services/stream.js';
const logger = apLogger;
@ -91,19 +93,25 @@ function validateActor(x: IObject, uri: string): IActor {
*
* Misskeyに対象のPersonが登録されていればそれを返します
*/
export async function fetchPerson(uri: string, resolver?: Resolver): Promise<User | null> {
export async function fetchPerson(uri: string, resolver?: Resolver): Promise<CacheableUser | null> {
if (typeof uri !== 'string') throw new Error('uri is not string');
const cached = uriPersonCache.get(uri);
if (cached) return cached;
// URIがこのサーバーを指しているならデータベースからフェッチ
if (uri.startsWith(config.url + '/')) {
const id = uri.split('/').pop();
return await Users.findOne(id).then(x => x || null);
const u = await Users.findOne(id).then(x => x || null); // TODO: typeorm 3.0 にしたら .then(x => x || null) を消す
if (u) uriPersonCache.set(uri, u);
return u;
}
//#region このサーバーに既に登録されていたらそれを返す
const exist = await Users.findOne({ uri });
if (exist) {
uriPersonCache.set(uri, exist);
return exist;
}
//#endregion
@ -352,6 +360,8 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
location: person['vcard:Address'] || null,
});
publishInternalEvent('remoteUserUpdated', { id: exist.id });
// ハッシュタグ更新
updateUsertags(exist, tags);
@ -371,7 +381,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
* Misskeyに対象のPersonが登録されていればそれを返し
* Misskeyに登録しそれを返します
*/
export async function resolvePerson(uri: string, resolver?: Resolver): Promise<User> {
export async function resolvePerson(uri: string, resolver?: Resolver): Promise<CacheableUser> {
if (typeof uri !== 'string') throw new Error('uri is not string');
//#region このサーバーに既に登録されていたらそれを返す

View file

@ -1,7 +1,17 @@
import { IObject } from './type.js';
import { IRemoteUser } from '@/models/entities/user.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { performActivity } from './kernel/index.js';
import { updatePerson } from './models/person.js';
export default async (actor: IRemoteUser, activity: IObject): Promise<void> => {
export default async (actor: CacheableRemoteUser, activity: IObject): Promise<void> => {
await performActivity(actor, activity);
// ついでにリモートユーザーの情報が古かったら更新しておく
if (actor.uri) {
if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
setImmediate(() => {
updatePerson(actor.uri!);
});
}
}
};

View file

@ -1,4 +0,0 @@
import { Cache } from "@/misc/cache.js";
import { User } from "@/models/entities/user.js";
export const userCache = new Cache<User | null>(1000 * 60 * 30);

View file

@ -10,7 +10,6 @@ import renderFollowUser from '@/remote/activitypub/renderer/follow-user.js';
import { setResponseType } from '../activitypub.js';
import { Users, Followings, UserProfiles } from '@/models/index.js';
import { LessThan } from 'typeorm';
import { userCache } from './cache.js';
export default async (ctx: Router.RouterContext) => {
const userId = ctx.params.user;
@ -28,11 +27,10 @@ export default async (ctx: Router.RouterContext) => {
return;
}
// TODO: typeorm 3.0にしたら .then(x => x || null) は消せる
const user = await userCache.fetch(userId, () => Users.findOne({
const user = await Users.findOne({
id: userId,
host: null,
}).then(x => x || null));
});
if (user == null) {
ctx.status = 404;

View file

@ -11,7 +11,6 @@ import { setResponseType } from '../activitypub.js';
import { Users, Followings, UserProfiles } from '@/models/index.js';
import { LessThan, FindConditions } from 'typeorm';
import { Following } from '@/models/entities/following.js';
import { userCache } from './cache.js';
export default async (ctx: Router.RouterContext) => {
const userId = ctx.params.user;
@ -29,11 +28,10 @@ export default async (ctx: Router.RouterContext) => {
return;
}
// TODO: typeorm 3.0にしたら .then(x => x || null) は消せる
const user = await userCache.fetch(userId, () => Users.findOne({
const user = await Users.findOne({
id: userId,
host: null,
}).then(x => x || null));
});
if (user == null) {
ctx.status = 404;

View file

@ -15,7 +15,6 @@ import { Users, Notes } from '@/models/index.js';
import { makePaginationQuery } from '../api/common/make-pagination-query.js';
import { Brackets } from 'typeorm';
import { Note } from '@/models/entities/note.js';
import { userCache } from './cache.js';
export default async (ctx: Router.RouterContext) => {
const userId = ctx.params.user;
@ -36,11 +35,10 @@ export default async (ctx: Router.RouterContext) => {
return;
}
// TODO: typeorm 3.0にしたら .then(x => x || null) は消せる
const user = await userCache.fetch(userId, () => Users.findOne({
const user = await Users.findOne({
id: userId,
host: null,
}).then(x => x || null));
});
if (user == null) {
ctx.status = 404;

View file

@ -1,7 +1,12 @@
import isNativeToken from './common/is-native-token.js';
import { User } from '@/models/entities/user.js';
import { CacheableLocalUser, ILocalUser } from '@/models/entities/user.js';
import { Users, AccessTokens, Apps } from '@/models/index.js';
import { AccessToken } from '@/models/entities/access-token.js';
import { Cache } from '@/misc/cache.js';
import { App } from '@/models/entities/app.js';
import { localUserByIdCache, localUserByNativeTokenCache } from '@/services/user-cache.js';
const appCache = new Cache<App>(Infinity);
export class AuthenticationError extends Error {
constructor(message: string) {
@ -10,15 +15,15 @@ export class AuthenticationError extends Error {
}
}
export default async (token: string | null): Promise<[User | null | undefined, AccessToken | null | undefined]> => {
export default async (token: string | null): Promise<[CacheableLocalUser | null | undefined, AccessToken | null | undefined]> => {
if (token == null) {
return [null, null];
}
if (isNativeToken(token)) {
// Fetch user
const user = await Users
.findOne({ token });
// TODO: typeorm 3.0にしたら .then(x => x || null) は消せる
const user = await localUserByNativeTokenCache.fetch(token,
() => Users.findOne({ token }).then(x => x || null) as Promise<ILocalUser | null>);
if (user == null) {
throw new AuthenticationError('user not found');
@ -42,14 +47,14 @@ export default async (token: string | null): Promise<[User | null | undefined, A
lastUsedAt: new Date(),
});
const user = await Users
.findOne({
const user = await localUserByIdCache.fetch(accessToken.userId,
() => Users.findOne({
id: accessToken.userId, // findOne(accessToken.userId) のように書かないのは後方互換性のため
});
}) as Promise<ILocalUser>);
if (accessToken.appId) {
const app = await Apps
.findOneOrFail(accessToken.appId);
const app = await appCache.fetch(accessToken.appId,
() => Apps.findOneOrFail(accessToken.appId!));
return [user, {
id: accessToken.id,

View file

@ -1,7 +1,7 @@
import Koa from 'koa';
import { performance } from 'perf_hooks';
import { limiter } from './limiter.js';
import { User } from '@/models/entities/user.js';
import { CacheableLocalUser, User } from '@/models/entities/user.js';
import endpoints, { IEndpoint } from './endpoints.js';
import { ApiError } from './error.js';
import { apiLogger } from './logger.js';
@ -13,7 +13,7 @@ const accessDenied = {
id: '56f35758-7dd5-468b-8439-5d6fb8ec9b8e',
};
export default async (endpoint: string, user: User | null | undefined, token: AccessToken | null | undefined, data: any, ctx?: Koa.Context) => {
export default async (endpoint: string, user: CacheableLocalUser | null | undefined, token: AccessToken | null | undefined, data: any, ctx?: Koa.Context) => {
const isSecure = user != null && token == null;
const ep = endpoints.find(e => e.name === endpoint);

View file

@ -1,30 +1,16 @@
import * as fs from 'node:fs';
import Ajv from 'ajv';
import { ILocalUser } from '@/models/entities/user.js';
import { CacheableLocalUser, ILocalUser } from '@/models/entities/user.js';
import { IEndpointMeta } from './endpoints.js';
import { ApiError } from './error.js';
import { Schema, SchemaType } from '@/misc/schema.js';
import { AccessToken } from '@/models/entities/access-token.js';
type SimpleUserInfo = {
id: ILocalUser['id'];
createdAt: ILocalUser['createdAt'];
host: ILocalUser['host'];
username: ILocalUser['username'];
uri: ILocalUser['uri'];
inbox: ILocalUser['inbox'];
sharedInbox: ILocalUser['sharedInbox'];
isAdmin: ILocalUser['isAdmin'];
isModerator: ILocalUser['isModerator'];
isSilenced: ILocalUser['isSilenced'];
showTimelineReplies: ILocalUser['showTimelineReplies'];
};
export type Response = Record<string, any> | void;
// TODO: paramsの型をT['params']のスキーマ定義から推論する
type executor<T extends IEndpointMeta, Ps extends Schema> =
(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? SimpleUserInfo : SimpleUserInfo | null, token: AccessToken | null, file?: any, cleanup?: () => any) =>
(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any, cleanup?: () => any) =>
Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
const ajv = new Ajv({
@ -34,11 +20,11 @@ const ajv = new Ajv({
ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/);
export default function <T extends IEndpointMeta, Ps extends Schema>(meta: T, paramDef: Ps, cb: executor<T, Ps>)
: (params: any, user: T['requireCredential'] extends true ? SimpleUserInfo : SimpleUserInfo | null, token: AccessToken | null, file?: any) => Promise<any> {
: (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any) => Promise<any> {
const validate = ajv.compile(paramDef);
return (params: any, user: T['requireCredential'] extends true ? SimpleUserInfo : SimpleUserInfo | null, token: AccessToken | null, file?: any) => {
return (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any) => {
function cleanup() {
fs.unlink(file.path, () => {});
}

View file

@ -1,5 +1,6 @@
import define from '../../../define.js';
import { Users } from '@/models/index.js';
import { publishInternalEvent } from '@/services/stream.js';
export const meta = {
tags: ['admin'],
@ -31,4 +32,6 @@ export default define(meta, paramDef, async (ps) => {
await Users.update(user.id, {
isModerator: true,
});
publishInternalEvent('userChangeModeratorState', { id: user.id, isModerator: true });
});

View file

@ -27,4 +27,6 @@ export default define(meta, paramDef, async (ps) => {
await Users.update(user.id, {
isModerator: false,
});
publishInternalEvent('userChangeModeratorState', { id: user.id, isModerator: false });
});

View file

@ -29,7 +29,8 @@ export default define(meta, paramDef, async (ps, me) => {
throw new Error('user not found');
}
if ((me.isModerator && !me.isAdmin) && user.isAdmin) {
const _me = await Users.findOneOrFail(me.id);
if ((_me.isModerator && !_me.isAdmin) && user.isAdmin) {
throw new Error('cannot show info of admin');
}

View file

@ -1,6 +1,7 @@
import define from '../../define.js';
import { Users } from '@/models/index.js';
import { insertModerationLog } from '@/services/insert-moderation-log.js';
import { publishInternalEvent } from '@/services/stream.js';
export const meta = {
tags: ['admin'],
@ -33,6 +34,8 @@ export default define(meta, paramDef, async (ps, me) => {
isSilenced: true,
});
publishInternalEvent('userChangeSilencedState', { id: user.id, isSilenced: true });
insertModerationLog(me, 'silence', {
targetId: user.id,
});

View file

@ -1,6 +1,7 @@
import define from '../../define.js';
import { Users } from '@/models/index.js';
import { insertModerationLog } from '@/services/insert-moderation-log.js';
import { publishInternalEvent } from '@/services/stream.js';
export const meta = {
tags: ['admin'],
@ -29,6 +30,8 @@ export default define(meta, paramDef, async (ps, me) => {
isSilenced: false,
});
publishInternalEvent('userChangeSilencedState', { id: user.id, isSilenced: false });
insertModerationLog(me, 'unsilence', {
targetId: user.id,
});

View file

@ -2,7 +2,7 @@ import { deleteFile } from '@/services/drive/delete-file.js';
import { publishDriveStream } from '@/services/stream.js';
import define from '../../../define.js';
import { ApiError } from '../../../error.js';
import { DriveFiles } from '@/models/index.js';
import { DriveFiles, Users } from '@/models/index.js';
export const meta = {
tags: ['drive'],
@ -42,7 +42,7 @@ export default define(meta, paramDef, async (ps, user) => {
throw new ApiError(meta.errors.noSuchFile);
}
if (!user.isAdmin && !user.isModerator && (file.userId !== user.id)) {
if ((!user.isAdmin && !user.isModerator) && (file.userId !== user.id)) {
throw new ApiError(meta.errors.accessDenied);
}

View file

@ -1,7 +1,7 @@
import define from '../../../define.js';
import { ApiError } from '../../../error.js';
import { DriveFile } from '@/models/entities/drive-file.js';
import { DriveFiles } from '@/models/index.js';
import { DriveFiles, Users } from '@/models/index.js';
export const meta = {
tags: ['drive'],
@ -70,7 +70,7 @@ export default define(meta, paramDef, async (ps, user) => {
throw new ApiError(meta.errors.noSuchFile);
}
if (!user.isAdmin && !user.isModerator && (file.userId !== user.id)) {
if ((!user.isAdmin && !user.isModerator) && (file.userId !== user.id)) {
throw new ApiError(meta.errors.accessDenied);
}

View file

@ -1,7 +1,7 @@
import { publishDriveStream } from '@/services/stream.js';
import define from '../../../define.js';
import { ApiError } from '../../../error.js';
import { DriveFiles, DriveFolders } from '@/models/index.js';
import { DriveFiles, DriveFolders, Users } from '@/models/index.js';
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js';
export const meta = {
@ -64,7 +64,7 @@ export default define(meta, paramDef, async (ps, user) => {
throw new ApiError(meta.errors.noSuchFile);
}
if (!user.isAdmin && !user.isModerator && (file.userId !== user.id)) {
if ((!user.isAdmin && !user.isModerator) && (file.userId !== user.id)) {
throw new ApiError(meta.errors.accessDenied);
}

View file

@ -1,5 +1,5 @@
import bcrypt from 'bcryptjs';
import { publishMainStream, publishUserEvent } from '@/services/stream.js';
import { publishInternalEvent, publishMainStream, publishUserEvent } from '@/services/stream.js';
import generateUserToken from '../../common/generate-native-user-token.js';
import define from '../../define.js';
import { Users, UserProfiles } from '@/models/index.js';
@ -20,6 +20,9 @@ export const paramDef = {
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, user) => {
const freshUser = await Users.findOneOrFail(user.id);
const oldToken = freshUser.token;
const profile = await UserProfiles.findOneOrFail(user.id);
// Compare password
@ -29,14 +32,14 @@ export default define(meta, paramDef, async (ps, user) => {
throw new Error('incorrect password');
}
// Generate secret
const secret = generateUserToken();
const newToken = generateUserToken();
await Users.update(user.id, {
token: secret,
token: newToken,
});
// Publish event
publishInternalEvent('userTokenRegenerated', { id: user.id, oldToken, newToken });
publishMainStream(user.id, 'myTokenRegenerated');
// Terminate streaming

View file

@ -48,7 +48,7 @@ export default define(meta, paramDef, async (ps, user) => {
throw e;
});
if (!user.isAdmin && !user.isModerator && (note.userId !== user.id)) {
if ((!user.isAdmin && !user.isModerator) && (note.userId !== user.id)) {
throw new ApiError(meta.errors.accessDenied);
}

View file

@ -2,7 +2,7 @@ import define from '../../define.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { ApiError } from '../../error.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { Notes } from '@/models/index.js';
import { Notes, Users } from '@/models/index.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query.js';
import { activeUsersChart } from '@/services/chart/index.js';

View file

@ -2,7 +2,7 @@ import define from '../../define.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { ApiError } from '../../error.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { Followings, Notes } from '@/models/index.js';
import { Followings, Notes, Users } from '@/models/index.js';
import { Brackets } from 'typeorm';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
@ -56,7 +56,7 @@ export const paramDef = {
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, user) => {
const m = await fetchMeta();
if (m.disableLocalTimeline && !user.isAdmin && !user.isModerator) {
if (m.disableLocalTimeline && (!user.isAdmin && !user.isModerator)) {
throw new ApiError(meta.errors.stlDisabled);
}

View file

@ -1,7 +1,7 @@
import define from '../../define.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { ApiError } from '../../error.js';
import { Notes } from '@/models/index.js';
import { Notes, Users } from '@/models/index.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';

View file

@ -1,5 +1,4 @@
import define from '../../define.js';
import { getNote } from '../../common/getters.js';
import { ApiError } from '../../error.js';
import { NoteReactions } from '@/models/index.js';
import { DeepPartial } from 'typeorm';

View file

@ -2,12 +2,12 @@ import Limiter from 'ratelimiter';
import { redisClient } from '../../db/redis.js';
import { IEndpoint } from './endpoints.js';
import * as Acct from '@/misc/acct.js';
import { User } from '@/models/entities/user.js';
import { CacheableLocalUser, User } from '@/models/entities/user.js';
import Logger from '@/services/logger.js';
const logger = new Logger('limiter');
export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable<IEndpoint['meta']['limit']> } }, user: User) => new Promise<void>((ok, reject) => {
export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable<IEndpoint['meta']['limit']> } }, user: CacheableLocalUser) => new Promise<void>((ok, reject) => {
const limitation = endpoint.meta.limit;
const key = Object.prototype.hasOwnProperty.call(limitation, 'key')

View file

@ -18,6 +18,11 @@ import { Packed } from '@/misc/schema.js';
//#region Stream type-body definitions
export interface InternalStreamTypes {
userChangeSuspendedState: { id: User['id']; isSuspended: User['isSuspended']; };
userChangeSilencedState: { id: User['id']; isSilenced: User['isSilenced']; };
userChangeModeratorState: { id: User['id']; isModerator: User['isModerator']; };
userTokenRegenerated: { id: User['id']; oldToken: User['token']; newToken: User['token']; };
remoteUserUpdated: { id: User['id']; };
antennaCreated: Antenna;
antennaDeleted: Antenna;
antennaUpdated: Antenna;

View file

@ -7,8 +7,17 @@ import { User, ILocalUser, IRemoteUser } from '@/models/entities/user.js';
import { Users, FollowRequests, Followings } from '@/models/index.js';
import { decrementFollowing } from './delete.js';
type Local = ILocalUser | { id: User['id']; host: User['host']; uri: User['host'] };
type Remote = IRemoteUser;
type Local = ILocalUser | {
id: ILocalUser['id'];
host: ILocalUser['host'];
uri: ILocalUser['uri']
};
type Remote = IRemoteUser | {
id: IRemoteUser['id'];
host: IRemoteUser['host'];
uri: IRemoteUser['uri'];
inbox: IRemoteUser['inbox'];
};
type Both = Local | Remote;
/**

View file

@ -1,4 +1,4 @@
import { User } from '@/models/entities/user.js';
import { CacheableUser, User } from '@/models/entities/user.js';
import { UserGroup } from '@/models/entities/user-group.js';
import { DriveFile } from '@/models/entities/drive-file.js';
import { MessagingMessages, UserGroupJoinings, Mutings, Users } from '@/models/index.js';
@ -13,7 +13,7 @@ import renderCreate from '@/remote/activitypub/renderer/create.js';
import { renderActivity } from '@/remote/activitypub/renderer/index.js';
import { deliver } from '@/queue/index.js';
export async function createMessage(user: { id: User['id']; host: User['host']; }, recipientUser: User | undefined, recipientGroup: UserGroup | undefined, text: string | null | undefined, file: DriveFile | null, uri?: string) {
export async function createMessage(user: { id: User['id']; host: User['host']; }, recipientUser: CacheableUser | undefined, recipientGroup: UserGroup | undefined, text: string | null | undefined, file: DriveFile | null, uri?: string) {
const message = {
id: genId(),
createdAt: new Date(),

View file

@ -38,8 +38,6 @@ import { endedPollNotificationQueue } from '@/queue/queues.js';
import { Cache } from '@/misc/cache.js';
import { UserProfile } from '@/models/entities/user-profile.js';
const usersCache = new Cache<MinimumUser>(Infinity);
const mutedWordsCache = new Cache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@ -212,7 +210,7 @@ export default async (user: { id: User['id']; username: User['username']; host:
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 usersCache.fetch(data.reply.userId, () => Users.findOneOrFail(data.reply!.userId)));
mentionedUsers.push(await Users.findOneOrFail(data.reply!.userId));
}
if (data.visibility === 'specified') {
@ -225,7 +223,7 @@ export default async (user: { id: User['id']; username: User['username']; host:
}
if (data.reply && !data.visibleUsers.some(x => x.id === data.reply!.userId)) {
data.visibleUsers.push(await usersCache.fetch(data.reply.userId, () => Users.findOneOrFail(data.reply!.userId)));
data.visibleUsers.push(await Users.findOneOrFail(data.reply!.userId));
}
}

View file

@ -20,7 +20,7 @@ import { Brackets, In } from 'typeorm';
* @param user 稿
* @param note 稿
*/
export default async function(user: User, note: Note, quiet = false) {
export default async function(user: { id: User['id']; uri: User['uri']; host: User['host']; }, note: Note, quiet = false) {
const deletedAt = new Date();
// この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき
@ -131,7 +131,7 @@ async function getMentionedRemoteUsers(note: Note) {
}) as IRemoteUser[];
}
async function deliverToConcerned(user: ILocalUser, note: Note, content: any) {
async function deliverToConcerned(user: { id: ILocalUser['id']; host: null; }, note: Note, content: any) {
deliverToFollowers(user, content);
deliverToRelays(user, content);
const remoteUsers = await getMentionedRemoteUsers(note);

View file

@ -1,12 +1,12 @@
import { publishNoteStream } from '@/services/stream.js';
import { User } from '@/models/entities/user.js';
import { CacheableUser, User } from '@/models/entities/user.js';
import { Note } from '@/models/entities/note.js';
import { PollVotes, NoteWatchings, Polls, Blockings } from '@/models/index.js';
import { Not } from 'typeorm';
import { genId } from '@/misc/gen-id.js';
import { createNotification } from '../../create-notification.js';
export default async function(user: User, note: Note, choice: number) {
export default async function(user: CacheableUser, note: Note, choice: number) {
const poll = await Polls.findOne(note.id);
if (poll == null) throw new Error('poll not found');

View file

@ -6,9 +6,13 @@ import { deliver } from '@/queue/index.js';
import { ILocalUser, User } from '@/models/entities/user.js';
import { Users, Relays } from '@/models/index.js';
import { genId } from '@/misc/gen-id.js';
import { Cache } from '@/misc/cache.js';
import { Relay } from '@/models/entities/relay.js';
const ACTOR_USERNAME = 'relay.actor' as const;
const relaysCache = new Cache<Relay[]>(1000 * 60 * 10);
export async function getRelayActor(): Promise<ILocalUser> {
const user = await Users.findOne({
host: null,
@ -78,9 +82,9 @@ export async function relayRejected(id: string) {
export async function deliverToRelays(user: { id: User['id']; host: null; }, activity: any) {
if (activity == null) return;
const relays = await Relays.find({
const relays = await relaysCache.fetch(null, () => Relays.find({
status: 'accepted',
});
}));
if (relays.length === 0) return;
const copy = JSON.parse(JSON.stringify(activity));

View file

@ -5,8 +5,11 @@ import config from '@/config/index.js';
import { User } from '@/models/entities/user.js';
import { Users, Followings } from '@/models/index.js';
import { Not, IsNull } from 'typeorm';
import { publishInternalEvent } from './stream';
export async function doPostSuspend(user: { id: User['id']; host: User['host'] }) {
publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true });
if (Users.isLocalUser(user)) {
// 知り得る全SharedInboxにDelete配信
const content = renderActivity(renderDelete(`${config.url}/users/${user.id}`, user));

View file

@ -6,8 +6,11 @@ import config from '@/config/index.js';
import { User } from '@/models/entities/user.js';
import { Users, Followings } from '@/models/index.js';
import { Not, IsNull } from 'typeorm';
import { publishInternalEvent } from './stream';
export async function doPostUnsuspend(user: User) {
publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: false });
if (Users.isLocalUser(user)) {
// 知り得る全SharedInboxにUndo Delete配信
const content = renderActivity(renderUndo(renderDelete(`${config.url}/users/${user.id}`, user), user));

View file

@ -0,0 +1,44 @@
import { CacheableLocalUser, CacheableUser, ILocalUser, User } from '@/models/entities/user.js';
import { Users } from '@/models/index.js';
import { Cache } from '@/misc/cache.js';
import { subsdcriber } from '@/db/redis.js';
export const userByIdCache = new Cache<CacheableUser>(Infinity);
export const localUserByNativeTokenCache = new Cache<CacheableLocalUser | null>(Infinity);
export const localUserByIdCache = new Cache<CacheableLocalUser>(Infinity);
export const uriPersonCache = new Cache<CacheableUser | null>(Infinity);
subsdcriber.on('message', async (_, data) => {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
const { type, body } = obj.message;
switch (type) {
case 'userChangeSuspendedState':
case 'userChangeSilencedState':
case 'userChangeModeratorState':
case 'remoteUserUpdated': {
const user = await Users.findOneOrFail(body.id);
userByIdCache.set(user.id, user);
for (const [k, v] of uriPersonCache.cache.entries()) {
if (v.value?.id === user.id) {
uriPersonCache.set(k, user);
}
}
if (Users.isLocalUser(user)) {
localUserByNativeTokenCache.set(user.token, user);
localUserByIdCache.set(user.id, user);
}
break;
}
case 'userTokenRegenerated': {
const user = await Users.findOneOrFail(body.id) as ILocalUser;
localUserByNativeTokenCache.delete(body.oldToken);
localUserByNativeTokenCache.set(body.newToken, user);
break;
}
default:
break;
}
}
});

View file

@ -54,7 +54,7 @@
</FormSlot>
<FormSwitch v-model="profile.isCat" class="_formBlock">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></FormSwitch>
<FormSwitch v-model="profile.showTimelineReplies" class="_formBlock">{{ i18n.ts.flagShowTimelineReplies }}<template #caption>{{ i18n.ts.flagShowTimelineRepliesDescription }}</template></FormSwitch>
<FormSwitch v-model="profile.showTimelineReplies" class="_formBlock">{{ i18n.ts.flagShowTimelineReplies }}<template #caption>{{ i18n.ts.flagShowTimelineRepliesDescription }} {{ i18n.ts.reflectMayTakeTime }}</template></FormSwitch>
<FormSwitch v-model="profile.isBot" class="_formBlock">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></FormSwitch>
<FormSwitch v-model="profile.alwaysMarkNsfw" class="_formBlock">{{ i18n.ts.alwaysMarkSensitive }}</FormSwitch>

View file

@ -25,6 +25,7 @@
<FormSwitch v-if="user.host == null && $i.isAdmin && (moderator || !user.isAdmin)" v-model="moderator" class="_formBlock" @update:modelValue="toggleModerator">{{ $ts.moderator }}</FormSwitch>
<FormSwitch v-model="silenced" class="_formBlock" @update:modelValue="toggleSilence">{{ $ts.silence }}</FormSwitch>
<FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ $ts.suspend }}</FormSwitch>
{{ $ts.reflectMayTakeTime }}
<FormButton v-if="user.host == null && iAmModerator" class="_formBlock" @click="resetPassword"><i class="fas fa-key"></i> {{ $ts.resetPassword }}</FormButton>
</FormSection>