perf(server): cache user info as much as possible

This commit is contained in:
syuilo 2022-03-20 21:26:54 +09:00
parent aebd77ad38
commit 1589e9c9b8
72 changed files with 716 additions and 358 deletions

View File

@ -840,6 +840,8 @@ tenMinutes: "10分"
oneHour: "1時間"
oneDay: "1日"
oneWeek: "1週間"
reflectMayTakeTime: "反映されるまで時間がかかる場合があります。"
failedToFetchAccountInformation: "アカウント情報の取得に失敗しました"
_emailUnavailable:
used: "既に使用されています"

View File

@ -0,0 +1,17 @@
export class userIndexes1647777116829 {
name = 'userIndexes1647777116829'
async up(queryRunner) {
await queryRunner.query(`CREATE INDEX "IDX_8977c6037a7bc2cb0c84b6d4db" ON "user" ("isSuspended") `);
await queryRunner.query(`CREATE INDEX "IDX_391e03c755cc3dffa85d85536f" ON "user" ("isSilenced") `);
await queryRunner.query(`CREATE INDEX "IDX_b2033a3235871353c93700a0b6" ON "user" ("isAdmin") `);
await queryRunner.query(`CREATE INDEX "IDX_dfb7b092897e7b354f73e7ae25" ON "user" ("isModerator") `);
}
async down(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_dfb7b092897e7b354f73e7ae25"`);
await queryRunner.query(`DROP INDEX "public"."IDX_b2033a3235871353c93700a0b6"`);
await queryRunner.query(`DROP INDEX "public"."IDX_391e03c755cc3dffa85d85536f"`);
await queryRunner.query(`DROP INDEX "public"."IDX_8977c6037a7bc2cb0c84b6d4db"`);
}
}

View File

@ -28,12 +28,23 @@ export class Cache<T> {
this.cache.delete(key);
}
public async fetch(key: string | null, fetcher: () => Promise<T>): Promise<T> {
/**
* fetcherを呼び出して結果をキャッシュ&
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
*/
public async fetch(key: string | null, fetcher: () => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> {
const cachedValue = this.get(key);
if (cachedValue !== undefined) {
if (validator) {
if (validator(cachedValue)) {
// Cache HIT
return cachedValue;
}
} else {
// Cache HIT
return cachedValue;
}
}
// Cache MISS
const value = await fetcher();

View File

@ -106,12 +106,14 @@ export class User {
})
public tags: string[];
@Index()
@Column('boolean', {
default: false,
comment: 'Whether the User is suspended.',
})
public isSuspended: boolean;
@Index()
@Column('boolean', {
default: false,
comment: 'Whether the User is silenced.',
@ -136,12 +138,14 @@ export class User {
})
public isCat: boolean;
@Index()
@Column('boolean', {
default: false,
comment: 'Whether the User is the admin.',
})
public isAdmin: boolean;
@Index()
@Column('boolean', {
default: false,
comment: 'Whether the User is a moderator.',
@ -234,3 +238,27 @@ export interface ILocalUser extends User {
export interface IRemoteUser extends User {
host: string;
}
export type CacheableLocalUser = {
id: ILocalUser['id'];
createdAt: ILocalUser['createdAt'];
host: ILocalUser['host'];
username: ILocalUser['username'];
uri: ILocalUser['uri'];
inbox: ILocalUser['inbox'];
sharedInbox: ILocalUser['sharedInbox'];
showTimelineReplies: ILocalUser['showTimelineReplies'];
};
export type CacheableRemoteUser = {
id: IRemoteUser['id'];
createdAt: IRemoteUser['createdAt'];
host: IRemoteUser['host'];
username: IRemoteUser['username'];
uri: IRemoteUser['uri'];
inbox: IRemoteUser['inbox'];
sharedInbox: IRemoteUser['sharedInbox'];
featured: IRemoteUser['featured'];
followersUri: IRemoteUser['followersUri'];
};

View File

@ -37,6 +37,65 @@ export class UserRepository extends Repository<User> {
public validateBirthday = ajv.compile(this.birthdaySchema);
//#endregion
private suspendedUsersCache: Set<User['id']> = new Set();
private silencedUsersCache: Set<User['id']> = new Set();
private moderatorsCache: Set<User['id']> = new Set();
constructor() {
super();
const fetchCache = () => {
this.find({
where: {
isSuspended: true,
},
select: ['id'],
}).then(users => {
this.suspendedUsersCache = new Set(users.map(user => user.id));
});
this.find({
where: {
isSilenced: true,
},
select: ['id'],
}).then(users => {
this.silencedUsersCache = new Set(users.map(user => user.id));
});
this.find({
where: [{
isAdmin: true,
}, {
isModerator: true,
}],
select: ['id'],
}).then(users => {
this.moderatorsCache = new Set(users.map(user => user.id));
});
};
setImmediate(() => {
fetchCache();
});
setInterval(() => {
fetchCache();
}, 1000 * 60 * 5);
}
public checkSuspended(userId: User['id']): boolean {
return this.suspendedUsersCache.has(userId);
}
public checkSilenced(userId: User['id']): boolean {
return this.silencedUsersCache.has(userId);
}
public checkModerator(userId: User['id']): boolean {
return this.moderatorsCache.has(userId);
}
public async getRelation(me: User['id'], target: User['id']) {
const [following1, following2, followReq1, followReq2, toBlocking, fromBlocked, mute] = await Promise.all([
Followings.findOne({

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 = await dbResolver.getAuthUserFromKeyId(signature.keyId);
// keyIdでわからなければ、activity.actorを元にDBから取得 || activity.actorを元にリモートから取得
if (authUser == null) {

View File

@ -3,7 +3,7 @@ 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 } from '@/models/entities/user.js';
type Visibility = 'public' | 'home' | 'followers' | 'specified';
@ -13,7 +13,7 @@ type AudienceInfo = {
visibleUsers: User[],
};
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);
@ -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,15 @@
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';
const publicKeyCache = new Cache<(UserPublickey & { user: User }) | null>(Infinity);
export default class DbResolver {
constructor() {
@ -75,7 +78,11 @@ export default class DbResolver {
/**
* AP KeyId => Misskey User and Key
*/
public async getAuthUserFromKeyId(keyId: string): Promise<AuthUser | null> {
public async getAuthUserFromKeyId(keyId: string): Promise<{
user: CacheableRemoteUser;
key: UserPublickey;
} | null> {
const key = await publicKeyCache.fetch(keyId, async () => {
const key = await UserPublickeys.findOne({
keyId,
}, {
@ -84,6 +91,11 @@ export default class DbResolver {
if (key == null) return null;
return key as UserPublickey & { user: User };
}, key => key != null);
if (key == null) return null;
return {
user: key.user as IRemoteUser,
key,
@ -93,7 +105,10 @@ export default class DbResolver {
/**
* AP Actor id => Misskey User and Key
*/
public async getAuthUserFromApId(uri: string): Promise<AuthUser | null> {
public async getAuthUserFromApId(uri: string): Promise<{
user: CacheableRemoteUser;
key?: UserPublickey;
} | null> {
const user = await resolvePerson(uri) as IRemoteUser;
if (user == null) return null;
@ -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,14 +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;
}
// アナウンス先をブロックしてたら中断
const meta = await fetchMeta();
if (meta.blockedHosts.includes(extractDbHost(uri))) 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,8 +37,8 @@ export async function performActivity(actor: IRemoteUser, activity: IObject) {
}
}
async function performOneActivity(actor: IRemoteUser, activity: IObject): Promise<void> {
if (actor.isSuspended) return;
async function performOneActivity(actor: CacheableRemoteUser, activity: IObject): Promise<void> {
if (Users.checkSuspended(actor.id)) return;
if (isCreate(activity)) {
await create(actor, 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,4 +1,4 @@
import { IRemoteUser } from '@/models/entities/user.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';
@ -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

@ -30,6 +30,9 @@ 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 { Cache } from '@/misc/cache.js';
const uriPersonCache = new Cache<User | null>(Infinity);
const logger = apLogger;
@ -90,6 +93,8 @@ function validateActor(x: IObject, uri: string): IActor {
* Personをフェッチします
*
* Misskeyに対象のPersonが登録されていればそれを返します
*
* TODO: cache
*/
export async function fetchPerson(uri: string, resolver?: Resolver): Promise<User | null> {
if (typeof uri !== 'string') throw new Error('uri is not string');

View File

@ -1,7 +1,7 @@
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';
export default async (actor: IRemoteUser, activity: IObject): Promise<void> => {
export default async (actor: CacheableRemoteUser, activity: IObject): Promise<void> => {
await performActivity(actor, activity);
};

View File

@ -1,7 +1,13 @@
import isNativeToken from './common/is-native-token.js';
import { User } from '@/models/entities/user.js';
import { CacheableLocalUser, ILocalUser, User } 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';
const userByNativeTokenCache = new Cache<CacheableLocalUser | null>(1000 * 60 * 5);
const userByIdCache = new Cache<CacheableLocalUser>(1000 * 60 * 5);
const appCache = new Cache<App>(Infinity);
export class AuthenticationError extends Error {
constructor(message: string) {
@ -10,15 +16,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 userByNativeTokenCache.fetch(token,
() => Users.findOne({ token }).then(x => x || null) as Promise<ILocalUser | null>);
if (user == null) {
throw new AuthenticationError('user not found');
@ -42,14 +48,14 @@ export default async (token: string | null): Promise<[User | null | undefined, A
lastUsedAt: new Date(),
});
const user = await Users
.findOne({
const user = await userByIdCache.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,11 +1,12 @@
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';
import { AccessToken } from '@/models/entities/access-token.js';
import { Users } from '@/models/index.js';
const accessDenied = {
message: 'Access denied.',
@ -13,7 +14,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);
@ -40,7 +41,7 @@ export default async (endpoint: string, user: User | null | undefined, token: Ac
});
}
if (ep.meta.requireCredential && user!.isSuspended) {
if (ep.meta.requireCredential && Users.checkSuspended(user!.id)) {
throw new ApiError({
message: 'Your account has been suspended.',
code: 'YOUR_ACCOUNT_SUSPENDED',
@ -49,13 +50,17 @@ export default async (endpoint: string, user: User | null | undefined, token: Ac
});
}
if (ep.meta.requireAdmin && !user!.isAdmin) {
if (ep.meta.requireAdmin || ep.meta.requireModerator) {
const fullUser = await Users.findOneOrFail(user!.id);
if (ep.meta.requireAdmin && !fullUser.isAdmin) {
throw new ApiError(accessDenied, { reason: 'You are not the admin.' });
}
if (ep.meta.requireModerator && !user!.isAdmin && !user!.isModerator) {
if (ep.meta.requireModerator && !fullUser.isAdmin && !fullUser.isModerator) {
throw new ApiError(accessDenied, { reason: 'You are not a moderator.' });
}
}
if (token && ep.meta.kind && !token.permission.some(p => p === ep.meta.kind)) {
throw new ApiError({
@ -65,7 +70,7 @@ export default async (endpoint: string, user: User | null | undefined, token: Ac
});
}
if (ep.meta.requireCredential && ep.meta.limit && !user!.isAdmin && !user!.isModerator) {
if (ep.meta.requireCredential && ep.meta.limit) {
// Rate limit
await limiter(ep as IEndpoint & { meta: { limit: NonNullable<IEndpoint['meta']['limit']> } }, user!).catch(e => {
throw new ApiError({

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

@ -0,0 +1,401 @@
import config from '@/config/index.js';
import define from '../../define.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
export const meta = {
tags: ['meta'],
requireCredential: true,
requireAdmin: true,
res: {
type: 'object',
optional: false, nullable: false,
properties: {
driveCapacityPerLocalUserMb: {
type: 'number',
optional: false, nullable: false,
},
driveCapacityPerRemoteUserMb: {
type: 'number',
optional: false, nullable: false,
},
cacheRemoteFiles: {
type: 'boolean',
optional: false, nullable: false,
},
emailRequiredForSignup: {
type: 'boolean',
optional: false, nullable: false,
},
enableHcaptcha: {
type: 'boolean',
optional: false, nullable: false,
},
hcaptchaSiteKey: {
type: 'string',
optional: false, nullable: true,
},
enableRecaptcha: {
type: 'boolean',
optional: false, nullable: false,
},
recaptchaSiteKey: {
type: 'string',
optional: false, nullable: true,
},
swPublickey: {
type: 'string',
optional: false, nullable: true,
},
mascotImageUrl: {
type: 'string',
optional: false, nullable: false,
default: '/assets/ai.png',
},
bannerUrl: {
type: 'string',
optional: false, nullable: false,
},
errorImageUrl: {
type: 'string',
optional: false, nullable: false,
default: 'https://xn--931a.moe/aiart/yubitun.png',
},
iconUrl: {
type: 'string',
optional: false, nullable: true,
},
maxNoteTextLength: {
type: 'number',
optional: false, nullable: false,
},
emojis: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
id: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
aliases: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
category: {
type: 'string',
optional: false, nullable: true,
},
host: {
type: 'string',
optional: false, nullable: true,
},
url: {
type: 'string',
optional: false, nullable: false,
format: 'url',
},
},
},
},
ads: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
place: {
type: 'string',
optional: false, nullable: false,
},
url: {
type: 'string',
optional: false, nullable: false,
format: 'url',
},
imageUrl: {
type: 'string',
optional: false, nullable: false,
format: 'url',
},
},
},
},
enableEmail: {
type: 'boolean',
optional: false, nullable: false,
},
enableTwitterIntegration: {
type: 'boolean',
optional: false, nullable: false,
},
enableGithubIntegration: {
type: 'boolean',
optional: false, nullable: false,
},
enableDiscordIntegration: {
type: 'boolean',
optional: false, nullable: false,
},
enableServiceWorker: {
type: 'boolean',
optional: false, nullable: false,
},
translatorAvailable: {
type: 'boolean',
optional: false, nullable: false,
},
proxyAccountName: {
type: 'string',
optional: false, nullable: true,
},
userStarForReactionFallback: {
type: 'boolean',
optional: true, nullable: false,
},
pinnedUsers: {
type: 'array',
optional: true, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
hiddenTags: {
type: 'array',
optional: true, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
blockedHosts: {
type: 'array',
optional: true, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
hcaptchaSecretKey: {
type: 'string',
optional: true, nullable: true,
},
recaptchaSecretKey: {
type: 'string',
optional: true, nullable: true,
},
proxyAccountId: {
type: 'string',
optional: true, nullable: true,
format: 'id',
},
twitterConsumerKey: {
type: 'string',
optional: true, nullable: true,
},
twitterConsumerSecret: {
type: 'string',
optional: true, nullable: true,
},
githubClientId: {
type: 'string',
optional: true, nullable: true,
},
githubClientSecret: {
type: 'string',
optional: true, nullable: true,
},
discordClientId: {
type: 'string',
optional: true, nullable: true,
},
discordClientSecret: {
type: 'string',
optional: true, nullable: true,
},
summaryProxy: {
type: 'string',
optional: true, nullable: true,
},
email: {
type: 'string',
optional: true, nullable: true,
},
smtpSecure: {
type: 'boolean',
optional: true, nullable: false,
},
smtpHost: {
type: 'string',
optional: true, nullable: true,
},
smtpPort: {
type: 'string',
optional: true, nullable: true,
},
smtpUser: {
type: 'string',
optional: true, nullable: true,
},
smtpPass: {
type: 'string',
optional: true, nullable: true,
},
swPrivateKey: {
type: 'string',
optional: true, nullable: true,
},
useObjectStorage: {
type: 'boolean',
optional: true, nullable: false,
},
objectStorageBaseUrl: {
type: 'string',
optional: true, nullable: true,
},
objectStorageBucket: {
type: 'string',
optional: true, nullable: true,
},
objectStoragePrefix: {
type: 'string',
optional: true, nullable: true,
},
objectStorageEndpoint: {
type: 'string',
optional: true, nullable: true,
},
objectStorageRegion: {
type: 'string',
optional: true, nullable: true,
},
objectStoragePort: {
type: 'number',
optional: true, nullable: true,
},
objectStorageAccessKey: {
type: 'string',
optional: true, nullable: true,
},
objectStorageSecretKey: {
type: 'string',
optional: true, nullable: true,
},
objectStorageUseSSL: {
type: 'boolean',
optional: true, nullable: false,
},
objectStorageUseProxy: {
type: 'boolean',
optional: true, nullable: false,
},
objectStorageSetPublicRead: {
type: 'boolean',
optional: true, nullable: false,
},
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
},
required: [],
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, me) => {
const instance = await fetchMeta(true);
return {
maintainerName: instance.maintainerName,
maintainerEmail: instance.maintainerEmail,
version: config.version,
name: instance.name,
uri: config.url,
description: instance.description,
langs: instance.langs,
tosUrl: instance.ToSUrl,
repositoryUrl: instance.repositoryUrl,
feedbackUrl: instance.feedbackUrl,
disableRegistration: instance.disableRegistration,
disableLocalTimeline: instance.disableLocalTimeline,
disableGlobalTimeline: instance.disableGlobalTimeline,
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
emailRequiredForSignup: instance.emailRequiredForSignup,
enableHcaptcha: instance.enableHcaptcha,
hcaptchaSiteKey: instance.hcaptchaSiteKey,
enableRecaptcha: instance.enableRecaptcha,
recaptchaSiteKey: instance.recaptchaSiteKey,
swPublickey: instance.swPublicKey,
themeColor: instance.themeColor,
mascotImageUrl: instance.mascotImageUrl,
bannerUrl: instance.bannerUrl,
errorImageUrl: instance.errorImageUrl,
iconUrl: instance.iconUrl,
backgroundImageUrl: instance.backgroundImageUrl,
logoImageUrl: instance.logoImageUrl,
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH, // 後方互換性のため
defaultLightTheme: instance.defaultLightTheme,
defaultDarkTheme: instance.defaultDarkTheme,
enableEmail: instance.enableEmail,
enableTwitterIntegration: instance.enableTwitterIntegration,
enableGithubIntegration: instance.enableGithubIntegration,
enableDiscordIntegration: instance.enableDiscordIntegration,
enableServiceWorker: instance.enableServiceWorker,
translatorAvailable: instance.deeplAuthKey != null,
pinnedPages: instance.pinnedPages,
pinnedClipId: instance.pinnedClipId,
cacheRemoteFiles: instance.cacheRemoteFiles,
useStarForReactionFallback: instance.useStarForReactionFallback,
pinnedUsers: instance.pinnedUsers,
hiddenTags: instance.hiddenTags,
blockedHosts: instance.blockedHosts,
hcaptchaSecretKey: instance.hcaptchaSecretKey,
recaptchaSecretKey: instance.recaptchaSecretKey,
proxyAccountId: instance.proxyAccountId,
twitterConsumerKey: instance.twitterConsumerKey,
twitterConsumerSecret: instance.twitterConsumerSecret,
githubClientId: instance.githubClientId,
githubClientSecret: instance.githubClientSecret,
discordClientId: instance.discordClientId,
discordClientSecret: instance.discordClientSecret,
summalyProxy: instance.summalyProxy,
email: instance.email,
smtpSecure: instance.smtpSecure,
smtpHost: instance.smtpHost,
smtpPort: instance.smtpPort,
smtpUser: instance.smtpUser,
smtpPass: instance.smtpPass,
swPrivateKey: instance.swPrivateKey,
useObjectStorage: instance.useObjectStorage,
objectStorageBaseUrl: instance.objectStorageBaseUrl,
objectStorageBucket: instance.objectStorageBucket,
objectStoragePrefix: instance.objectStoragePrefix,
objectStorageEndpoint: instance.objectStorageEndpoint,
objectStorageRegion: instance.objectStorageRegion,
objectStoragePort: instance.objectStoragePort,
objectStorageAccessKey: instance.objectStorageAccessKey,
objectStorageSecretKey: instance.objectStorageSecretKey,
objectStorageUseSSL: instance.objectStorageUseSSL,
objectStorageUseProxy: instance.objectStorageUseProxy,
objectStorageSetPublicRead: instance.objectStorageSetPublicRead,
objectStorageS3ForcePathStyle: instance.objectStorageS3ForcePathStyle,
deeplAuthKey: instance.deeplAuthKey,
deeplIsPro: instance.deeplIsPro,
};
});

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

@ -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 (!Users.checkModerator(user.id) && (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 (!Users.checkModerator(user.id) && (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 (!Users.checkModerator(user.id) && (file.userId !== user.id)) {
throw new ApiError(meta.errors.accessDenied);
}

View File

@ -290,151 +290,6 @@ export const meta = {
},
},
},
userStarForReactionFallback: {
type: 'boolean',
optional: true, nullable: false,
},
pinnedUsers: {
type: 'array',
optional: true, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
hiddenTags: {
type: 'array',
optional: true, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
blockedHosts: {
type: 'array',
optional: true, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
hcaptchaSecretKey: {
type: 'string',
optional: true, nullable: true,
},
recaptchaSecretKey: {
type: 'string',
optional: true, nullable: true,
},
proxyAccountId: {
type: 'string',
optional: true, nullable: true,
format: 'id',
},
twitterConsumerKey: {
type: 'string',
optional: true, nullable: true,
},
twitterConsumerSecret: {
type: 'string',
optional: true, nullable: true,
},
githubClientId: {
type: 'string',
optional: true, nullable: true,
},
githubClientSecret: {
type: 'string',
optional: true, nullable: true,
},
discordClientId: {
type: 'string',
optional: true, nullable: true,
},
discordClientSecret: {
type: 'string',
optional: true, nullable: true,
},
summaryProxy: {
type: 'string',
optional: true, nullable: true,
},
email: {
type: 'string',
optional: true, nullable: true,
},
smtpSecure: {
type: 'boolean',
optional: true, nullable: false,
},
smtpHost: {
type: 'string',
optional: true, nullable: true,
},
smtpPort: {
type: 'string',
optional: true, nullable: true,
},
smtpUser: {
type: 'string',
optional: true, nullable: true,
},
smtpPass: {
type: 'string',
optional: true, nullable: true,
},
swPrivateKey: {
type: 'string',
optional: true, nullable: true,
},
useObjectStorage: {
type: 'boolean',
optional: true, nullable: false,
},
objectStorageBaseUrl: {
type: 'string',
optional: true, nullable: true,
},
objectStorageBucket: {
type: 'string',
optional: true, nullable: true,
},
objectStoragePrefix: {
type: 'string',
optional: true, nullable: true,
},
objectStorageEndpoint: {
type: 'string',
optional: true, nullable: true,
},
objectStorageRegion: {
type: 'string',
optional: true, nullable: true,
},
objectStoragePort: {
type: 'number',
optional: true, nullable: true,
},
objectStorageAccessKey: {
type: 'string',
optional: true, nullable: true,
},
objectStorageSecretKey: {
type: 'string',
optional: true, nullable: true,
},
objectStorageUseSSL: {
type: 'boolean',
optional: true, nullable: false,
},
objectStorageUseProxy: {
type: 'boolean',
optional: true, nullable: false,
},
objectStorageSetPublicRead: {
type: 'boolean',
optional: true, nullable: false,
},
},
},
} as const;
@ -552,45 +407,6 @@ export default define(meta, paramDef, async (ps, me) => {
serviceWorker: instance.enableServiceWorker,
miauth: true,
};
if (me && me.isAdmin) {
response.useStarForReactionFallback = instance.useStarForReactionFallback;
response.pinnedUsers = instance.pinnedUsers;
response.hiddenTags = instance.hiddenTags;
response.blockedHosts = instance.blockedHosts;
response.hcaptchaSecretKey = instance.hcaptchaSecretKey;
response.recaptchaSecretKey = instance.recaptchaSecretKey;
response.proxyAccountId = instance.proxyAccountId;
response.twitterConsumerKey = instance.twitterConsumerKey;
response.twitterConsumerSecret = instance.twitterConsumerSecret;
response.githubClientId = instance.githubClientId;
response.githubClientSecret = instance.githubClientSecret;
response.discordClientId = instance.discordClientId;
response.discordClientSecret = instance.discordClientSecret;
response.summalyProxy = instance.summalyProxy;
response.email = instance.email;
response.smtpSecure = instance.smtpSecure;
response.smtpHost = instance.smtpHost;
response.smtpPort = instance.smtpPort;
response.smtpUser = instance.smtpUser;
response.smtpPass = instance.smtpPass;
response.swPrivateKey = instance.swPrivateKey;
response.useObjectStorage = instance.useObjectStorage;
response.objectStorageBaseUrl = instance.objectStorageBaseUrl;
response.objectStorageBucket = instance.objectStorageBucket;
response.objectStoragePrefix = instance.objectStoragePrefix;
response.objectStorageEndpoint = instance.objectStorageEndpoint;
response.objectStorageRegion = instance.objectStorageRegion;
response.objectStoragePort = instance.objectStoragePort;
response.objectStorageAccessKey = instance.objectStorageAccessKey;
response.objectStorageSecretKey = instance.objectStorageSecretKey;
response.objectStorageUseSSL = instance.objectStorageUseSSL;
response.objectStorageUseProxy = instance.objectStorageUseProxy;
response.objectStorageSetPublicRead = instance.objectStorageSetPublicRead;
response.objectStorageS3ForcePathStyle = instance.objectStorageS3ForcePathStyle;
response.deeplAuthKey = instance.deeplAuthKey;
response.deeplIsPro = instance.deeplIsPro;
}
}
return response;

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 (!Users.checkModerator(user.id) && (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';
@ -49,7 +49,7 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, user) => {
const m = await fetchMeta();
if (m.disableGlobalTimeline) {
if (user == null || (!user.isAdmin && !user.isModerator)) {
if (user == null || !Users.checkModerator(user.id)) {
throw new ApiError(meta.errors.gtlDisabled);
}
}

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 && !Users.checkModerator(user.id)) {
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';
@ -55,7 +55,7 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, user) => {
const m = await fetchMeta();
if (m.disableLocalTimeline) {
if (user == null || (!user.isAdmin && !user.isModerator)) {
if (user == null || !Users.checkModerator(user.id)) {
throw new ApiError(meta.errors.ltlDisabled);
}
}

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';
@ -44,13 +43,8 @@ export const paramDef = {
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, user) => {
const note = await getNote(ps.noteId).catch(e => {
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
throw e;
});
const query = {
noteId: note.id,
noteId: ps.noteId,
} as DeepPartial<NoteReaction>;
if (ps.type) {

View File

@ -70,7 +70,7 @@ export default define(meta, paramDef, async (ps, me) => {
}).then(x => AbuseUserReports.findOneOrFail(x.identifiers[0]));
// Publish event to moderators
setTimeout(async () => {
setImmediate(async () => {
const moderators = await Users.find({
where: [{
isAdmin: true,
@ -94,5 +94,5 @@ export default define(meta, paramDef, async (ps, me) => {
sanitizeHtml(ps.comment),
sanitizeHtml(ps.comment));
}
}, 1);
});
});

View File

@ -61,7 +61,7 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, me) => {
let user;
const isAdminOrModerator = me && (me.isAdmin || me.isModerator);
const isAdminOrModerator = me && Users.checkModerator(me.id);
if (ps.userIds) {
if (ps.userIds.length === 0) {

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

@ -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

@ -112,7 +112,7 @@ type Option = {
app?: App | null;
};
export default async (user: { id: User['id']; username: User['username']; host: User['host']; isSilenced: User['isSilenced']; createdAt: User['createdAt']; }, data: Option, silent = false) => new Promise<Note>(async (res, rej) => {
export default async (user: { id: User['id']; username: User['username']; host: User['host']; createdAt: User['createdAt']; }, data: Option, silent = false) => new Promise<Note>(async (res, rej) => {
// チャンネル外にリプライしたら対象のスコープに合わせる
// (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで)
if (data.reply && data.channel && data.reply.channelId !== data.channel.id) {
@ -137,7 +137,7 @@ export default async (user: { id: User['id']; username: User['username']; host:
if (data.channel != null) data.localOnly = true;
// サイレンス
if (user.isSilenced && data.visibility === 'public' && data.channel == null) {
if (Users.checkSilenced(user.id) && data.visibility === 'public' && data.channel == null) {
data.visibility = 'home';
}

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();
// この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき
@ -127,7 +127,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

@ -2,7 +2,7 @@ import { del, get, set } from '@/scripts/idb-proxy';
import { reactive } from 'vue';
import * as misskey from 'misskey-js';
import { apiUrl } from '@/config';
import { waiting, api, popup, popupMenu, success } from '@/os';
import { waiting, api, popup, popupMenu, success, alert } from '@/os';
import { unisonReload, reloadChannel } from '@/scripts/unison-reload';
import { showSuspendedDialog } from './scripts/show-suspended-dialog';
import { i18n } from './i18n';
@ -89,7 +89,11 @@ function fetchAccount(token): Promise<Account> {
signout();
});
} else {
signout();
alert({
type: 'error',
title: i18n.ts.failedToFetchAccountInformation,
text: JSON.stringify(res.error),
});
}
} else {
res.token = token;

View File

@ -84,7 +84,7 @@ export default defineComponent({
methods: {
async init() {
const meta = await os.api('meta', { detail: true });
const meta = await os.api('admin/meta');
this.enableHcaptcha = meta.enableHcaptcha;
this.hcaptchaSiteKey = meta.hcaptchaSiteKey;
this.hcaptchaSecretKey = meta.hcaptchaSecretKey;

View File

@ -95,7 +95,7 @@ export default defineComponent({
methods: {
async init() {
const meta = await os.api('meta', { detail: true });
const meta = await os.api('admin/meta');
this.enableEmail = meta.enableEmail;
this.email = meta.email;
this.smtpSecure = meta.smtpSecure;

View File

@ -42,7 +42,7 @@ export default defineComponent({
methods: {
async init() {
const meta = await os.api('meta', { detail: true });
const meta = await os.api('admin/meta');
this.blockedHosts = meta.blockedHosts.join('\n');
},

View File

@ -60,7 +60,7 @@ export default defineComponent({
methods: {
async init() {
const meta = await os.api('meta', { detail: true });
const meta = await os.api('admin/meta');
this.uri = meta.uri;
this.enableDiscordIntegration = meta.enableDiscordIntegration;
this.discordClientId = meta.discordClientId;

View File

@ -60,7 +60,7 @@ export default defineComponent({
methods: {
async init() {
const meta = await os.api('meta', { detail: true });
const meta = await os.api('admin/meta');
this.uri = meta.uri;
this.enableGithubIntegration = meta.enableGithubIntegration;
this.githubClientId = meta.githubClientId;

View File

@ -60,7 +60,7 @@ export default defineComponent({
methods: {
async init() {
const meta = await os.api('meta', { detail: true });
const meta = await os.api('admin/meta');
this.uri = meta.uri;
this.enableTwitterIntegration = meta.enableTwitterIntegration;
this.twitterConsumerKey = meta.twitterConsumerKey;

View File

@ -62,7 +62,7 @@ export default defineComponent({
methods: {
async init() {
const meta = await os.api('meta', { detail: true });
const meta = await os.api('admin/meta');
this.enableTwitterIntegration = meta.enableTwitterIntegration;
this.enableGithubIntegration = meta.enableGithubIntegration;
this.enableDiscordIntegration = meta.enableDiscordIntegration;

View File

@ -120,7 +120,7 @@ export default defineComponent({
methods: {
async init() {
const meta = await os.api('meta', { detail: true });
const meta = await os.api('admin/meta');
this.useObjectStorage = meta.useObjectStorage;
this.objectStorageBaseUrl = meta.objectStorageBaseUrl;
this.objectStorageBucket = meta.objectStorageBucket;

View File

@ -44,7 +44,7 @@ export default defineComponent({
methods: {
async init() {
const meta = await os.api('meta', { detail: true });
const meta = await os.api('admin/meta');
},
save() {
os.apiWithDialog('admin/update-meta', {

View File

@ -46,7 +46,7 @@ export default defineComponent({
methods: {
async init() {
const meta = await os.api('meta', { detail: true });
const meta = await os.api('admin/meta');
this.proxyAccountId = meta.proxyAccountId;
if (this.proxyAccountId) {
this.proxyAccount = await os.api('users/show', { userId: this.proxyAccountId });

View File

@ -72,7 +72,7 @@ export default defineComponent({
methods: {
async init() {
const meta = await os.api('meta', { detail: true });
const meta = await os.api('admin/meta');
this.summalyProxy = meta.summalyProxy;
this.enableHcaptcha = meta.enableHcaptcha;
this.enableRecaptcha = meta.enableRecaptcha;

View File

@ -210,7 +210,7 @@ export default defineComponent({
methods: {
async init() {
const meta = await os.api('meta', { detail: true });
const meta = await os.api('admin/meta');
this.name = meta.name;
this.description = meta.description;
this.tosUrl = meta.tosUrl;

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>