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時間" oneHour: "1時間"
oneDay: "1日" oneDay: "1日"
oneWeek: "1週間" oneWeek: "1週間"
reflectMayTakeTime: "反映されるまで時間がかかる場合があります。"
failedToFetchAccountInformation: "アカウント情報の取得に失敗しました"
_emailUnavailable: _emailUnavailable:
used: "既に使用されています" 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,11 +28,22 @@ export class Cache<T> {
this.cache.delete(key); 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); const cachedValue = this.get(key);
if (cachedValue !== undefined) { if (cachedValue !== undefined) {
// Cache HIT if (validator) {
return cachedValue; if (validator(cachedValue)) {
// Cache HIT
return cachedValue;
}
} else {
// Cache HIT
return cachedValue;
}
} }
// Cache MISS // Cache MISS

View File

@ -106,12 +106,14 @@ export class User {
}) })
public tags: string[]; public tags: string[];
@Index()
@Column('boolean', { @Column('boolean', {
default: false, default: false,
comment: 'Whether the User is suspended.', comment: 'Whether the User is suspended.',
}) })
public isSuspended: boolean; public isSuspended: boolean;
@Index()
@Column('boolean', { @Column('boolean', {
default: false, default: false,
comment: 'Whether the User is silenced.', comment: 'Whether the User is silenced.',
@ -136,12 +138,14 @@ export class User {
}) })
public isCat: boolean; public isCat: boolean;
@Index()
@Column('boolean', { @Column('boolean', {
default: false, default: false,
comment: 'Whether the User is the admin.', comment: 'Whether the User is the admin.',
}) })
public isAdmin: boolean; public isAdmin: boolean;
@Index()
@Column('boolean', { @Column('boolean', {
default: false, default: false,
comment: 'Whether the User is a moderator.', comment: 'Whether the User is a moderator.',
@ -234,3 +238,27 @@ export interface ILocalUser extends User {
export interface IRemoteUser extends User { export interface IRemoteUser extends User {
host: string; 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); public validateBirthday = ajv.compile(this.birthdaySchema);
//#endregion //#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']) { public async getRelation(me: User['id'], target: User['id']) {
const [following1, following2, followReq1, followReq2, toBlocking, fromBlocked, mute] = await Promise.all([ const [following1, following2, followReq1, followReq2, toBlocking, fromBlocked, mute] = await Promise.all([
Followings.findOne({ 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 { resolvePerson } from '@/remote/activitypub/models/person.js';
import { LdSignature } from '@/remote/activitypub/misc/ld-signature.js'; import { LdSignature } from '@/remote/activitypub/misc/ld-signature.js';
import { StatusError } from '@/misc/fetch.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'); 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}`; return `Old keyId is no longer supported. ${keyIdLower}`;
} }
// TDOO: キャッシュ
const dbResolver = new DbResolver(); const dbResolver = new DbResolver();
// HTTP-Signature keyIdを元にDBから取得 // 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を元にリモートから取得 // keyIdでわからなければ、activity.actorを元にDBから取得 || activity.actorを元にリモートから取得
if (authUser == null) { if (authUser == null) {

View File

@ -3,7 +3,7 @@ import Resolver from './resolver.js';
import { resolvePerson } from './models/person.js'; import { resolvePerson } from './models/person.js';
import { unique, concat } from '@/prelude/array.js'; import { unique, concat } from '@/prelude/array.js';
import promiseLimit from 'promise-limit'; 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'; type Visibility = 'public' | 'home' | 'followers' | 'specified';
@ -13,7 +13,7 @@ type AudienceInfo = {
visibleUsers: User[], 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 toGroups = groupingAudience(getApIds(to), actor);
const ccGroups = groupingAudience(getApIds(cc), 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 = { const groups = {
public: [] as string[], public: [] as string[],
followers: [] as string[], followers: [] as string[],
@ -85,7 +85,7 @@ function isPublic(id: string) {
].includes(id); ].includes(id);
} }
function isFollowers(id: string, actor: IRemoteUser) { function isFollowers(id: string, actor: CacheableRemoteUser) {
return ( return (
id === (actor.followersUri || `${actor.uri}/followers`) id === (actor.followersUri || `${actor.uri}/followers`)
); );

View File

@ -1,12 +1,15 @@
import escapeRegexp from 'escape-regexp';
import config from '@/config/index.js'; import config from '@/config/index.js';
import { Note } from '@/models/entities/note.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 { UserPublickey } from '@/models/entities/user-publickey.js';
import { MessagingMessage } from '@/models/entities/messaging-message.js'; import { MessagingMessage } from '@/models/entities/messaging-message.js';
import { Notes, Users, UserPublickeys, MessagingMessages } from '@/models/index.js'; import { Notes, Users, UserPublickeys, MessagingMessages } from '@/models/index.js';
import { IObject, getApId } from './type.js'; import { IObject, getApId } from './type.js';
import { resolvePerson } from './models/person.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 { export default class DbResolver {
constructor() { constructor() {
@ -75,12 +78,21 @@ export default class DbResolver {
/** /**
* AP KeyId => Misskey User and Key * AP KeyId => Misskey User and Key
*/ */
public async getAuthUserFromKeyId(keyId: string): Promise<AuthUser | null> { public async getAuthUserFromKeyId(keyId: string): Promise<{
const key = await UserPublickeys.findOne({ user: CacheableRemoteUser;
keyId, key: UserPublickey;
}, { } | null> {
relations: ['user'], const key = await publicKeyCache.fetch(keyId, async () => {
}); const key = await UserPublickeys.findOne({
keyId,
}, {
relations: ['user'],
});
if (key == null) return null;
return key as UserPublickey & { user: User };
}, key => key != null);
if (key == null) return null; if (key == null) return null;
@ -93,7 +105,10 @@ export default class DbResolver {
/** /**
* AP Actor id => Misskey User and Key * 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; const user = await resolvePerson(uri) as IRemoteUser;
if (user == null) return null; if (user == null) return null;
@ -125,11 +140,6 @@ export default class DbResolver {
} }
} }
export type AuthUser = {
user: IRemoteUser;
key?: UserPublickey;
};
type UriParseResult = { type UriParseResult = {
/** id in DB (local object only) */ /** id in DB (local object only) */
id?: string; id?: string;

View File

@ -112,7 +112,7 @@ export default class DeliverManager {
* @param activity Activity * @param activity Activity
* @param from Followee * @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); const manager = new DeliverManager(actor, activity);
manager.addFollowersRecipe(); manager.addFollowersRecipe();
await manager.execute(); await manager.execute();
@ -123,7 +123,7 @@ export async function deliverToFollowers(actor: ILocalUser, activity: any) {
* @param activity Activity * @param activity Activity
* @param to Target user * @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); const manager = new DeliverManager(actor, activity);
manager.addDirectRecipe(to); manager.addDirectRecipe(to);
await manager.execute(); 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 accept from '@/services/following/requests/accept.js';
import { IFollow } from '../../type.js'; import { IFollow } from '../../type.js';
import DbResolver from '../../db-resolver.js'; import DbResolver from '../../db-resolver.js';
import { relayAccepted } from '@/services/relay.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は存在するローカルユーザーである必要がある // ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
const dbResolver = new DbResolver(); const dbResolver = new DbResolver();

View File

@ -1,12 +1,12 @@
import Resolver from '../../resolver.js'; 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 acceptFollow from './follow.js';
import { IAccept, isFollow, getApType } from '../../type.js'; import { IAccept, isFollow, getApType } from '../../type.js';
import { apLogger } from '../../logger.js'; import { apLogger } from '../../logger.js';
const logger = apLogger; 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; const uri = activity.id || activity;
logger.info(`Accept: ${uri}`); 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 { IAdd } from '../../type.js';
import { resolveNote } from '../../models/note.js'; import { resolveNote } from '../../models/note.js';
import { addPinned } from '@/services/i/pin.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) { if ('actor' in activity && actor.uri !== activity.actor) {
throw new Error('invalid actor'); throw new Error('invalid actor');
} }

View File

@ -1,12 +1,12 @@
import Resolver from '../../resolver.js'; 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 announceNote from './note.js';
import { IAnnounce, getApId } from '../../type.js'; import { IAnnounce, getApId } from '../../type.js';
import { apLogger } from '../../logger.js'; import { apLogger } from '../../logger.js';
const logger = apLogger; 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); const uri = getApId(activity);
logger.info(`Announce: ${uri}`); logger.info(`Announce: ${uri}`);

View File

@ -1,6 +1,6 @@
import Resolver from '../../resolver.js'; import Resolver from '../../resolver.js';
import post from '@/services/note/create.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 { IAnnounce, getApId } from '../../type.js';
import { fetchNote, resolveNote } from '../../models/note.js'; import { fetchNote, resolveNote } from '../../models/note.js';
import { apLogger } from '../../logger.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); const uri = getApId(activity);
// アナウンサーが凍結されていたらスキップ
if (actor.isSuspended) {
return;
}
// アナウンス先をブロックしてたら中断 // アナウンス先をブロックしてたら中断
const meta = await fetchMeta(); const meta = await fetchMeta();
if (meta.blockedHosts.includes(extractDbHost(uri))) return; if (meta.blockedHosts.includes(extractDbHost(uri))) return;

View File

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

View File

@ -1,5 +1,5 @@
import Resolver from '../../resolver.js'; 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 createNote from './note.js';
import { ICreate, getApId, isPost, getApType } from '../../type.js'; import { ICreate, getApId, isPost, getApType } from '../../type.js';
import { apLogger } from '../../logger.js'; import { apLogger } from '../../logger.js';
@ -7,7 +7,7 @@ import { toArray, concat, unique } from '@/prelude/array.js';
const logger = apLogger; 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); const uri = getApId(activity);
logger.info(`Create: ${uri}`); logger.info(`Create: ${uri}`);

View File

@ -1,5 +1,5 @@
import Resolver from '../../resolver.js'; 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 { createNote, fetchNote } from '../../models/note.js';
import { getApId, IObject, ICreate } from '../../type.js'; import { getApId, IObject, ICreate } from '../../type.js';
import { getApLock } from '@/misc/app-lock.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); const uri = getApId(note);
if (typeof note === 'object') { if (typeof note === 'object') {

View File

@ -1,18 +1,19 @@
import { apLogger } from '../../logger.js'; import { apLogger } from '../../logger.js';
import { createDeleteAccountJob } from '@/queue/index.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'; import { Users } from '@/models/index.js';
const logger = apLogger; 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}`); logger.info(`Deleting the Actor: ${uri}`);
if (actor.uri !== uri) { if (actor.uri !== uri) {
return `skip: delete actor ${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`); logger.info(`skip: already deleted`);
} }

View File

@ -1,5 +1,5 @@
import deleteNote from './note.js'; 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 { IDelete, getApId, isTombstone, IObject, validPost, validActor } from '../../type.js';
import { toSingle } from '@/prelude/array.js'; import { toSingle } from '@/prelude/array.js';
import { deleteActor } from './actor.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) { if ('actor' in activity && actor.uri !== activity.actor) {
throw new Error('invalid 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 deleteNode from '@/services/note/delete.js';
import { apLogger } from '../../logger.js'; import { apLogger } from '../../logger.js';
import DbResolver from '../../db-resolver.js'; import DbResolver from '../../db-resolver.js';
@ -7,7 +7,7 @@ import { deleteMessage } from '@/services/messages/delete.js';
const logger = apLogger; 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}`); logger.info(`Deleting the Note: ${uri}`);
const unlock = await getApLock(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 config from '@/config/index.js';
import { IFlag, getApIds } from '../../type.js'; import { IFlag, getApIds } from '../../type.js';
import { AbuseUserReports, Users } from '@/models/index.js'; import { AbuseUserReports, Users } from '@/models/index.js';
import { In } from 'typeorm'; import { In } from 'typeorm';
import { genId } from '@/misc/gen-id.js'; 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スキーマと対応させられないので // objectは `(User|Note) | (User|Note)[]` だけど、全パターンDBスキーマと対応させられないので
// 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する // 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する
const uris = getApIds(activity.object); 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 follow from '@/services/following/create.js';
import { IFollow } from '../type.js'; import { IFollow } from '../type.js';
import DbResolver from '../db-resolver.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 dbResolver = new DbResolver();
const followee = await dbResolver.getUserFromApId(activity.object); 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 { 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 create from './create/index.js';
import performDeleteActivity from './delete/index.js'; import performDeleteActivity from './delete/index.js';
import performUpdateActivity from './update/index.js'; import performUpdateActivity from './update/index.js';
@ -17,8 +17,9 @@ import flag from './flag/index.js';
import { apLogger } from '../logger.js'; import { apLogger } from '../logger.js';
import Resolver from '../resolver.js'; import Resolver from '../resolver.js';
import { toArray } from '@/prelude/array.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)) { if (isCollectionOrOrderedCollection(activity)) {
const resolver = new Resolver(); const resolver = new Resolver();
for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) { 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> { async function performOneActivity(actor: CacheableRemoteUser, activity: IObject): Promise<void> {
if (actor.isSuspended) return; if (Users.checkSuspended(actor.id)) return;
if (isCreate(activity)) { if (isCreate(activity)) {
await create(actor, 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 { ILike, getApId } from '../type.js';
import create from '@/services/note/reaction/create.js'; import create from '@/services/note/reaction/create.js';
import { fetchNote, extractEmojis } from '../models/note.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 targetUri = getApId(activity.object);
const note = await fetchNote(targetUri); 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 { IRead, getApId } from '../type.js';
import { isSelfHost, extractDbHost } from '@/misc/convert-host.js'; import { isSelfHost, extractDbHost } from '@/misc/convert-host.js';
import { MessagingMessages } from '@/models/index.js'; import { MessagingMessages } from '@/models/index.js';
import { readUserMessagingMessage } from '../../../server/api/common/read-messaging-message.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); const id = await getApId(activity.object);
if (!isSelfHost(extractDbHost(id))) { 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 { remoteReject } from '@/services/following/reject.js';
import { IFollow } from '../../type.js'; import { IFollow } from '../../type.js';
import DbResolver from '../../db-resolver.js'; import DbResolver from '../../db-resolver.js';
import { relayRejected } from '@/services/relay.js'; import { relayRejected } from '@/services/relay.js';
import { Users } from '@/models/index.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は存在するローカルユーザーである必要がある // ※ activityはこっちから投げたフォローリクエストなので、activity.actorは存在するローカルユーザーである必要がある
const dbResolver = new DbResolver(); const dbResolver = new DbResolver();

View File

@ -1,12 +1,12 @@
import Resolver from '../../resolver.js'; 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 rejectFollow from './follow.js';
import { IReject, isFollow, getApType } from '../../type.js'; import { IReject, isFollow, getApType } from '../../type.js';
import { apLogger } from '../../logger.js'; import { apLogger } from '../../logger.js';
const logger = apLogger; 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; const uri = activity.id || activity;
logger.info(`Reject: ${uri}`); 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 { IRemove } from '../../type.js';
import { resolveNote } from '../../models/note.js'; import { resolveNote } from '../../models/note.js';
import { removePinned } from '@/services/i/pin.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) { if ('actor' in activity && actor.uri !== activity.actor) {
throw new Error('invalid actor'); throw new Error('invalid actor');
} }

View File

@ -1,11 +1,11 @@
import unfollow from '@/services/following/delete.js'; import unfollow from '@/services/following/delete.js';
import cancelRequest from '@/services/following/requests/cancel.js'; import cancelRequest from '@/services/following/requests/cancel.js';
import {IAccept} from '../../type.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 { Followings } from '@/models/index.js';
import DbResolver from '../../db-resolver.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 dbResolver = new DbResolver();
const follower = await dbResolver.getUserFromApId(activity.object); const follower = await dbResolver.getUserFromApId(activity.object);

View File

@ -1,9 +1,9 @@
import { Notes } from '@/models/index.js'; 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 { IAnnounce, getApId } from '../../type.js';
import deleteNote from '@/services/note/delete.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 uri = getApId(activity);
const note = await Notes.findOne({ const note = await Notes.findOne({

View File

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

View File

@ -1,11 +1,11 @@
import unfollow from '@/services/following/delete.js'; import unfollow from '@/services/following/delete.js';
import cancelRequest from '@/services/following/requests/cancel.js'; import cancelRequest from '@/services/following/requests/cancel.js';
import { IFollow } from '../../type.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 { FollowRequests, Followings } from '@/models/index.js';
import DbResolver from '../../db-resolver.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 dbResolver = new DbResolver();
const followee = await dbResolver.getUserFromApId(activity.object); const followee = await dbResolver.getUserFromApId(activity.object);

View File

@ -1,5 +1,5 @@
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 { IUndo, isFollow, isBlock, isLike, isAnnounce, getApType, isAccept } from '../../type.js';
import unfollow from './follow.js'; import unfollow from './follow.js';
import unblock from './block.js'; import unblock from './block.js';
import undoLike from './like.js'; import undoLike from './like.js';
@ -10,7 +10,7 @@ import { apLogger } from '../../logger.js';
const logger = apLogger; 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) { if ('actor' in activity && actor.uri !== activity.actor) {
throw new Error('invalid 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 { ILike, getApId } from '../../type.js';
import deleteReaction from '@/services/note/reaction/delete.js'; import deleteReaction from '@/services/note/reaction/delete.js';
import { fetchNote } from '../../models/note.js'; import { fetchNote } from '../../models/note.js';
@ -6,7 +6,7 @@ import { fetchNote } from '../../models/note.js';
/** /**
* Process Undo.Like activity * Process Undo.Like activity
*/ */
export default async (actor: IRemoteUser, activity: ILike) => { export default async (actor: CacheableRemoteUser, activity: ILike) => {
const targetUri = getApId(activity.object); const targetUri = getApId(activity.object);
const note = await fetchNote(targetUri); 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 { getApType, IUpdate, isActor } from '../../type.js';
import { apLogger } from '../../logger.js'; import { apLogger } from '../../logger.js';
import { updateQuestion } from '../../models/question.js'; import { updateQuestion } from '../../models/question.js';
@ -8,7 +8,7 @@ import { updatePerson } from '../../models/person.js';
/** /**
* Updateアクティビティを捌きます * 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) { if ('actor' in activity && actor.uri !== activity.actor) {
return `skip: invalid 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 { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { truncate } from '@/misc/truncate.js'; import { truncate } from '@/misc/truncate.js';
import { StatusError } from '@/misc/fetch.js'; import { StatusError } from '@/misc/fetch.js';
import { Cache } from '@/misc/cache.js';
const uriPersonCache = new Cache<User | null>(Infinity);
const logger = apLogger; const logger = apLogger;
@ -90,6 +93,8 @@ function validateActor(x: IObject, uri: string): IActor {
* Personをフェッチします * Personをフェッチします
* *
* Misskeyに対象のPersonが登録されていればそれを返します * Misskeyに対象のPersonが登録されていればそれを返します
*
* TODO: cache
*/ */
export async function fetchPerson(uri: string, resolver?: Resolver): Promise<User | null> { export async function fetchPerson(uri: string, resolver?: Resolver): Promise<User | null> {
if (typeof uri !== 'string') throw new Error('uri is not string'); if (typeof uri !== 'string') throw new Error('uri is not string');

View File

@ -1,7 +1,7 @@
import { IObject } from './type.js'; 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 { 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); await performActivity(actor, activity);
}; };

View File

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

View File

@ -1,11 +1,12 @@
import Koa from 'koa'; import Koa from 'koa';
import { performance } from 'perf_hooks'; import { performance } from 'perf_hooks';
import { limiter } from './limiter.js'; 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 endpoints, { IEndpoint } from './endpoints.js';
import { ApiError } from './error.js'; import { ApiError } from './error.js';
import { apiLogger } from './logger.js'; import { apiLogger } from './logger.js';
import { AccessToken } from '@/models/entities/access-token.js'; import { AccessToken } from '@/models/entities/access-token.js';
import { Users } from '@/models/index.js';
const accessDenied = { const accessDenied = {
message: 'Access denied.', message: 'Access denied.',
@ -13,7 +14,7 @@ const accessDenied = {
id: '56f35758-7dd5-468b-8439-5d6fb8ec9b8e', 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 isSecure = user != null && token == null;
const ep = endpoints.find(e => e.name === endpoint); 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({ throw new ApiError({
message: 'Your account has been suspended.', message: 'Your account has been suspended.',
code: 'YOUR_ACCOUNT_SUSPENDED', code: 'YOUR_ACCOUNT_SUSPENDED',
@ -49,12 +50,16 @@ export default async (endpoint: string, user: User | null | undefined, token: Ac
}); });
} }
if (ep.meta.requireAdmin && !user!.isAdmin) { if (ep.meta.requireAdmin || ep.meta.requireModerator) {
throw new ApiError(accessDenied, { reason: 'You are not the admin.' }); const fullUser = await Users.findOneOrFail(user!.id);
}
if (ep.meta.requireModerator && !user!.isAdmin && !user!.isModerator) { if (ep.meta.requireAdmin && !fullUser.isAdmin) {
throw new ApiError(accessDenied, { reason: 'You are not a moderator.' }); throw new ApiError(accessDenied, { reason: 'You are not the admin.' });
}
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)) { if (token && ep.meta.kind && !token.permission.some(p => p === ep.meta.kind)) {
@ -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 // Rate limit
await limiter(ep as IEndpoint & { meta: { limit: NonNullable<IEndpoint['meta']['limit']> } }, user!).catch(e => { await limiter(ep as IEndpoint & { meta: { limit: NonNullable<IEndpoint['meta']['limit']> } }, user!).catch(e => {
throw new ApiError({ throw new ApiError({

View File

@ -1,30 +1,16 @@
import * as fs from 'node:fs'; import * as fs from 'node:fs';
import Ajv from 'ajv'; 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 { IEndpointMeta } from './endpoints.js';
import { ApiError } from './error.js'; import { ApiError } from './error.js';
import { Schema, SchemaType } from '@/misc/schema.js'; import { Schema, SchemaType } from '@/misc/schema.js';
import { AccessToken } from '@/models/entities/access-token.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; export type Response = Record<string, any> | void;
// TODO: paramsの型をT['params']のスキーマ定義から推論する // TODO: paramsの型をT['params']のスキーマ定義から推論する
type executor<T extends IEndpointMeta, Ps extends Schema> = 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']>>>; Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
const ajv = new Ajv({ const ajv = new Ajv({
@ -34,11 +20,11 @@ const ajv = new Ajv({
ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/); 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>) 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); 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() { function cleanup() {
fs.unlink(file.path, () => {}); 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'); 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'); 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 { publishDriveStream } from '@/services/stream.js';
import define from '../../../define.js'; import define from '../../../define.js';
import { ApiError } from '../../../error.js'; import { ApiError } from '../../../error.js';
import { DriveFiles } from '@/models/index.js'; import { DriveFiles, Users } from '@/models/index.js';
export const meta = { export const meta = {
tags: ['drive'], tags: ['drive'],
@ -42,7 +42,7 @@ export default define(meta, paramDef, async (ps, user) => {
throw new ApiError(meta.errors.noSuchFile); 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); throw new ApiError(meta.errors.accessDenied);
} }

View File

@ -1,7 +1,7 @@
import define from '../../../define.js'; import define from '../../../define.js';
import { ApiError } from '../../../error.js'; import { ApiError } from '../../../error.js';
import { DriveFile } from '@/models/entities/drive-file.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 = { export const meta = {
tags: ['drive'], tags: ['drive'],
@ -70,7 +70,7 @@ export default define(meta, paramDef, async (ps, user) => {
throw new ApiError(meta.errors.noSuchFile); 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); throw new ApiError(meta.errors.accessDenied);
} }

View File

@ -1,7 +1,7 @@
import { publishDriveStream } from '@/services/stream.js'; import { publishDriveStream } from '@/services/stream.js';
import define from '../../../define.js'; import define from '../../../define.js';
import { ApiError } from '../../../error.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'; import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/misc/hard-limits.js';
export const meta = { export const meta = {
@ -64,7 +64,7 @@ export default define(meta, paramDef, async (ps, user) => {
throw new ApiError(meta.errors.noSuchFile); 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); 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; } as const;
@ -552,45 +407,6 @@ export default define(meta, paramDef, async (ps, me) => {
serviceWorker: instance.enableServiceWorker, serviceWorker: instance.enableServiceWorker,
miauth: true, 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; return response;

View File

@ -48,7 +48,7 @@ export default define(meta, paramDef, async (ps, user) => {
throw e; 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); 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 { fetchMeta } from '@/misc/fetch-meta.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
import { makePaginationQuery } from '../../common/make-pagination-query.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 { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query.js'; import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query.js';
import { activeUsersChart } from '@/services/chart/index.js'; import { activeUsersChart } from '@/services/chart/index.js';
@ -49,7 +49,7 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, user) => { export default define(meta, paramDef, async (ps, user) => {
const m = await fetchMeta(); const m = await fetchMeta();
if (m.disableGlobalTimeline) { if (m.disableGlobalTimeline) {
if (user == null || (!user.isAdmin && !user.isModerator)) { if (user == null || !Users.checkModerator(user.id)) {
throw new ApiError(meta.errors.gtlDisabled); 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 { fetchMeta } from '@/misc/fetch-meta.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
import { makePaginationQuery } from '../../common/make-pagination-query.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 { Brackets } from 'typeorm';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js'; import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-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 // eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, user) => { export default define(meta, paramDef, async (ps, user) => {
const m = await fetchMeta(); const m = await fetchMeta();
if (m.disableLocalTimeline && !user.isAdmin && !user.isModerator) { if (m.disableLocalTimeline && !Users.checkModerator(user.id)) {
throw new ApiError(meta.errors.stlDisabled); throw new ApiError(meta.errors.stlDisabled);
} }

View File

@ -1,7 +1,7 @@
import define from '../../define.js'; import define from '../../define.js';
import { fetchMeta } from '@/misc/fetch-meta.js'; import { fetchMeta } from '@/misc/fetch-meta.js';
import { ApiError } from '../../error.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 { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js'; import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { generateVisibilityQuery } from '../../common/generate-visibility-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) => { export default define(meta, paramDef, async (ps, user) => {
const m = await fetchMeta(); const m = await fetchMeta();
if (m.disableLocalTimeline) { if (m.disableLocalTimeline) {
if (user == null || (!user.isAdmin && !user.isModerator)) { if (user == null || !Users.checkModerator(user.id)) {
throw new ApiError(meta.errors.ltlDisabled); throw new ApiError(meta.errors.ltlDisabled);
} }
} }

View File

@ -1,5 +1,4 @@
import define from '../../define.js'; import define from '../../define.js';
import { getNote } from '../../common/getters.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
import { NoteReactions } from '@/models/index.js'; import { NoteReactions } from '@/models/index.js';
import { DeepPartial } from 'typeorm'; import { DeepPartial } from 'typeorm';
@ -44,13 +43,8 @@ export const paramDef = {
// eslint-disable-next-line import/no-default-export // eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, user) => { 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 = { const query = {
noteId: note.id, noteId: ps.noteId,
} as DeepPartial<NoteReaction>; } as DeepPartial<NoteReaction>;
if (ps.type) { if (ps.type) {

View File

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

View File

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

View File

@ -2,12 +2,12 @@ import Limiter from 'ratelimiter';
import { redisClient } from '../../db/redis.js'; import { redisClient } from '../../db/redis.js';
import { IEndpoint } from './endpoints.js'; import { IEndpoint } from './endpoints.js';
import * as Acct from '@/misc/acct.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'; import Logger from '@/services/logger.js';
const logger = new Logger('limiter'); 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 limitation = endpoint.meta.limit;
const key = Object.prototype.hasOwnProperty.call(limitation, 'key') 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 { Users, FollowRequests, Followings } from '@/models/index.js';
import { decrementFollowing } from './delete.js'; import { decrementFollowing } from './delete.js';
type Local = ILocalUser | { id: User['id']; host: User['host']; uri: User['host'] }; type Local = ILocalUser | {
type Remote = IRemoteUser; 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; type Both = Local | Remote;
/** /**

View File

@ -112,7 +112,7 @@ type Option = {
app?: App | null; 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) { 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 (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'; data.visibility = 'home';
} }

View File

@ -20,7 +20,7 @@ import { Brackets, In } from 'typeorm';
* @param user 稿 * @param user 稿
* @param note 稿 * @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(); const deletedAt = new Date();
// この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき // この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき
@ -127,7 +127,7 @@ async function getMentionedRemoteUsers(note: Note) {
}) as IRemoteUser[]; }) 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); deliverToFollowers(user, content);
deliverToRelays(user, content); deliverToRelays(user, content);
const remoteUsers = await getMentionedRemoteUsers(note); const remoteUsers = await getMentionedRemoteUsers(note);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -54,7 +54,7 @@
</FormSlot> </FormSlot>
<FormSwitch v-model="profile.isCat" class="_formBlock">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></FormSwitch> <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.isBot" class="_formBlock">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></FormSwitch>
<FormSwitch v-model="profile.alwaysMarkNsfw" class="_formBlock">{{ i18n.ts.alwaysMarkSensitive }}</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-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="silenced" class="_formBlock" @update:modelValue="toggleSilence">{{ $ts.silence }}</FormSwitch>
<FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ $ts.suspend }}</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> <FormButton v-if="user.host == null && iAmModerator" class="_formBlock" @click="resetPassword"><i class="fas fa-key"></i> {{ $ts.resetPassword }}</FormButton>
</FormSection> </FormSection>