Merge remote-tracking branch 'mi-dev/master' into report

This commit is contained in:
mattyatea 2023-12-28 04:16:03 +09:00
commit cb1586658e
No known key found for this signature in database
GPG key ID: 068E54E2C33BEF9A
567 changed files with 10281 additions and 3996 deletions

View file

@ -11,7 +11,7 @@
"decoratorMetadata": true
},
"experimental": {
"keepImportAttributes": true
"keepImportAssertions": true
},
"baseUrl": "src",
"paths": {

View file

@ -0,0 +1,37 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class ffVisibility1702718871541 {
constructor() {
this.name = 'ffVisibility1702718871541';
}
async up(queryRunner) {
await queryRunner.query(`CREATE TYPE "public"."user_profile_followingvisibility_enum" AS ENUM('public', 'followers', 'private')`);
await queryRunner.query(`CREATE CAST ("public"."user_profile_ffvisibility_enum" AS "public"."user_profile_followingvisibility_enum") WITH INOUT AS ASSIGNMENT`);
await queryRunner.query(`CREATE TYPE "public"."user_profile_followersVisibility_enum" AS ENUM('public', 'followers', 'private')`);
await queryRunner.query(`CREATE CAST ("public"."user_profile_ffvisibility_enum" AS "public"."user_profile_followersVisibility_enum") WITH INOUT AS ASSIGNMENT`);
await queryRunner.query(`ALTER TABLE "user_profile" ADD "followingVisibility" "public"."user_profile_followingvisibility_enum" NOT NULL DEFAULT 'public'`);
await queryRunner.query(`ALTER TABLE "user_profile" ADD "followersVisibility" "public"."user_profile_followersVisibility_enum" NOT NULL DEFAULT 'public'`);
await queryRunner.query(`UPDATE "user_profile" SET "followingVisibility" = "ffVisibility"`);
await queryRunner.query(`UPDATE "user_profile" SET "followersVisibility" = "ffVisibility"`);
await queryRunner.query(`DROP CAST ("public"."user_profile_ffvisibility_enum" AS "public"."user_profile_followersVisibility_enum")`);
await queryRunner.query(`DROP CAST ("public"."user_profile_ffvisibility_enum" AS "public"."user_profile_followingvisibility_enum")`);
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "ffVisibility"`);
await queryRunner.query(`DROP TYPE "public"."user_profile_ffvisibility_enum"`);
}
async down(queryRunner) {
await queryRunner.query(`CREATE TYPE "public"."user_profile_ffvisibility_enum" AS ENUM('public', 'followers', 'private')`);
await queryRunner.query(`ALTER TABLE "user_profile" ADD "ffVisibility" "public"."user_profile_ffvisibility_enum" NOT NULL DEFAULT 'public'`);
await queryRunner.query(`CREATE CAST ("public"."user_profile_followingvisibility_enum" AS "public"."user_profile_ffvisibility_enum") WITH INOUT AS ASSIGNMENT`);
await queryRunner.query(`UPDATE "user_profile" SET "ffVisibility" = "followingVisibility"`);
await queryRunner.query(`DROP CAST ("public"."user_profile_followingvisibility_enum" AS "public"."user_profile_ffvisibility_enum")`);
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "followersVisibility"`);
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "followingVisibility"`);
await queryRunner.query(`DROP TYPE "public"."user_profile_followersVisibility_enum"`);
await queryRunner.query(`DROP TYPE "public"."user_profile_followingvisibility_enum"`);
}
}

View file

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class bannedEmailDomains1703209889304 {
constructor() {
this.name = 'bannedEmailDomains1703209889304';
}
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "bannedEmailDomains" character varying(1024) array NOT NULL DEFAULT '{}'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "bannedEmailDomains"`);
}
}

View file

@ -4,7 +4,7 @@
"private": true,
"type": "module",
"engines": {
"node": ">=18.16.0"
"node": ">=20.10.0"
},
"scripts": {
"start": "node ./built/boot/entry.js",
@ -16,7 +16,8 @@
"watch:swc": "swc src -d built -D -w",
"build:tsc": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json",
"watch": "node watch.mjs",
"dev": "node ./built/boot/entry.js",
"restart": "pnpm build && pnpm start",
"dev": "nodemon -w src -e ts,js,mjs,cjs,json --exec \"cross-env NODE_ENV=development pnpm run restart\"",
"typecheck": "tsc --noEmit",
"eslint": "eslint --quiet \"src/**/*.ts\"",
"lint": "pnpm typecheck && pnpm eslint",
@ -64,10 +65,10 @@
"@bull-board/api": "5.10.2",
"@bull-board/fastify": "5.10.2",
"@bull-board/ui": "5.10.2",
"@discordapp/twemoji": "14.1.2",
"@discordapp/twemoji": "15.0.2",
"@fastify/accepts": "4.3.0",
"@fastify/cookie": "9.2.0",
"@fastify/cors": "8.4.2",
"@fastify/cors": "8.5.0",
"@fastify/express": "2.3.0",
"@fastify/http-proxy": "9.3.0",
"@fastify/multipart": "8.0.0",
@ -82,6 +83,7 @@
"@smithy/node-http-handler": "2.1.10",
"@swc/cli": "0.1.63",
"@swc/core": "1.3.100",
"@twemoji/parser": "15.0.0",
"accepts": "1.3.8",
"ajv": "8.12.0",
"archiver": "6.0.1",
@ -89,7 +91,7 @@
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.2",
"bullmq": "4.15.2",
"bullmq": "4.15.4",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.1",
"chalk": "5.3.0",
@ -106,7 +108,7 @@
"file-type": "18.7.0",
"fluent-ffmpeg": "2.1.2",
"form-data": "4.0.0",
"got": "13.0.0",
"got": "14.0.0",
"happy-dom": "10.0.3",
"hpagent": "1.2.0",
"http-link-header": "1.1.1",
@ -120,8 +122,8 @@
"jsonld": "8.3.2",
"jsrsasign": "10.9.0",
"meilisearch": "0.36.0",
"mfm-js": "0.23.3",
"microformats-parser": "1.5.2",
"mfm-js": "0.24.0",
"microformats-parser": "2.0.2",
"mime-types": "2.1.35",
"misskey-js": "workspace:*",
"ms": "3.0.0-canary.1",
@ -165,13 +167,12 @@
"tmp": "0.2.1",
"tsc-alias": "1.8.8",
"tsconfig-paths": "4.2.0",
"twemoji-parser": "14.0.0",
"typeorm": "0.3.17",
"typescript": "5.3.3",
"ulid": "2.3.0",
"vary": "1.1.2",
"web-push": "3.6.6",
"ws": "8.14.2",
"ws": "8.15.1",
"xev": "3.0.2"
},
"devDependencies": {
@ -194,7 +195,7 @@
"@types/jsrsasign": "10.5.12",
"@types/mime-types": "2.1.4",
"@types/ms": "0.7.34",
"@types/node": "20.10.4",
"@types/node": "20.10.5",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.14",
"@types/oauth": "0.9.4",
@ -217,15 +218,16 @@
"@types/vary": "1.1.3",
"@types/web-push": "3.6.3",
"@types/ws": "8.5.10",
"@typescript-eslint/eslint-plugin": "6.13.2",
"@typescript-eslint/parser": "6.13.2",
"@typescript-eslint/eslint-plugin": "6.14.0",
"@typescript-eslint/parser": "6.14.0",
"aws-sdk-client-mock": "3.0.0",
"cross-env": "7.0.3",
"eslint": "8.55.0",
"eslint-plugin-import": "2.29.0",
"eslint": "8.56.0",
"eslint-plugin-import": "2.29.1",
"execa": "8.0.1",
"jest": "29.7.0",
"jest-mock": "29.7.0",
"nodemon": "3.0.2",
"simple-oauth2": "5.0.0"
}
}

View file

@ -7,8 +7,8 @@ import { URLSearchParams } from 'node:url';
import * as nodemailer from 'nodemailer';
import { Inject, Injectable } from '@nestjs/common';
import { validate as validateEmail } from 'deep-email-validator';
import { SubOutputFormat } from 'deep-email-validator/dist/output/output.js';
import { MetaService } from '@/core/MetaService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import type Logger from '@/logger.js';
@ -30,6 +30,7 @@ export class EmailService {
private metaService: MetaService,
private loggerService: LoggerService,
private utilityService: UtilityService,
private httpRequestService: HttpRequestService,
) {
this.logger = this.loggerService.getLogger('email');
@ -155,7 +156,7 @@ export class EmailService {
@bindThis
public async validateEmailForAccount(emailAddress: string): Promise<{
available: boolean;
reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp';
reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned';
}> {
const meta = await this.metaService.fetch();
@ -164,32 +165,38 @@ export class EmailService {
email: emailAddress,
});
const verifymailApi = meta.enableVerifymailApi && meta.verifymailAuthKey != null;
let validated;
let validated: {
valid: boolean,
reason?: string | null,
};
if (meta.enableActiveEmailValidation && meta.verifymailAuthKey) {
if (verifymailApi) {
if (meta.enableActiveEmailValidation) {
if (meta.enableVerifymailApi && meta.verifymailAuthKey != null) {
validated = await this.verifyMail(emailAddress, meta.verifymailAuthKey);
} else {
validated = meta.enableActiveEmailValidation ? await validateEmail({
validated = await validateEmail({
email: emailAddress,
validateRegex: true,
validateMx: true,
validateTypo: false, // TLDを見ているみたいだけどclubとか弾かれるので
validateDisposable: true, // 捨てアドかどうかチェック
validateSMTP: false, // 日本だと25ポートが殆どのプロバイダーで塞がれていてタイムアウトになるので
}) : { valid: true, reason: null };
});
}
} else {
validated = { valid: true, reason: null };
}
const available = exist === 0 && validated.valid;
const emailDomain: string = emailAddress.split('@')[1];
const isBanned = this.utilityService.isBlockedHost(meta.bannedEmailDomains, emailDomain);
const available = exist === 0 && validated.valid && !isBanned;
return {
available,
reason: available ? null :
exist !== 0 ? 'used' :
isBanned ? 'banned' :
validated.reason === 'regex' ? 'format' :
validated.reason === 'disposable' ? 'disposable' :
validated.reason === 'mx' ? 'mx' :

View file

@ -77,6 +77,17 @@ export class FeaturedService {
return Array.from(ranking.keys());
}
@bindThis
private async removeFromRanking(name: string, windowRange: number, element: string): Promise<void> {
const currentWindow = this.getCurrentWindow(windowRange);
const previousWindow = currentWindow - 1;
const redisPipeline = this.redisClient.pipeline();
redisPipeline.zrem(`${name}:${currentWindow}`, element);
redisPipeline.zrem(`${name}:${previousWindow}`, element);
await redisPipeline.exec();
}
@bindThis
public updateGlobalNotesRanking(noteId: MiNote['id'], score = 1): Promise<void> {
return this.updateRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, noteId, score);
@ -126,4 +137,9 @@ export class FeaturedService {
public getHashtagsRanking(threshold: number): Promise<string[]> {
return this.getRankingOf('featuredHashtagsRanking', HASHTAG_RANKING_WINDOW, threshold);
}
@bindThis
public removeHashtagsFromRanking(hashtag: string): Promise<void> {
return this.removeFromRanking('featuredHashtagsRanking', HASHTAG_RANKING_WINDOW, hashtag);
}
}

View file

@ -15,6 +15,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { FeaturedService } from '@/core/FeaturedService.js';
import { MetaService } from '@/core/MetaService.js';
import { UtilityService } from '@/core/UtilityService.js';
@Injectable()
export class HashtagService {
@ -29,6 +30,7 @@ export class HashtagService {
private featuredService: FeaturedService,
private idService: IdService,
private metaService: MetaService,
private utilityService: UtilityService,
) {
}
@ -161,6 +163,7 @@ export class HashtagService {
const instance = await this.metaService.fetch();
const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t));
if (hiddenTags.includes(hashtag)) return;
if (this.utilityService.isSensitiveWordIncluded(hashtag, instance.sensitiveWords)) return;
// YYYYMMDDHHmm (10分間隔)
const now = new Date();

View file

@ -11,6 +11,7 @@ import { MiMeta } from '@/models/Meta.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { bindThis } from '@/decorators.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { FeaturedService } from '@/core/FeaturedService.js';
import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable()
@ -25,6 +26,7 @@ export class MetaService implements OnApplicationShutdown {
@Inject(DI.db)
private db: DataSource,
private featuredService: FeaturedService,
private globalEventService: GlobalEventService,
) {
//this.onMessage = this.onMessage.bind(this);
@ -95,6 +97,8 @@ export class MetaService implements OnApplicationShutdown {
@bindThis
public async update(data: Partial<MiMeta>): Promise<MiMeta> {
let before: MiMeta | undefined;
const updated = await this.db.transaction(async transactionalEntityManager => {
const metas = await transactionalEntityManager.find(MiMeta, {
order: {
@ -102,10 +106,10 @@ export class MetaService implements OnApplicationShutdown {
},
});
const meta = metas[0];
before = metas[0];
if (meta) {
await transactionalEntityManager.update(MiMeta, meta.id, data);
if (before) {
await transactionalEntityManager.update(MiMeta, before.id, data);
const metas = await transactionalEntityManager.find(MiMeta, {
order: {
@ -119,6 +123,21 @@ export class MetaService implements OnApplicationShutdown {
}
});
if (data.hiddenTags) {
process.nextTick(() => {
const hiddenTags = new Set<string>(data.hiddenTags);
if (before) {
for (const previousHiddenTag of before.hiddenTags) {
hiddenTags.delete(previousHiddenTag);
}
}
for (const hiddenTag of hiddenTags) {
this.featuredService.removeHashtagsFromRanking(hiddenTag);
}
});
}
this.globalEventService.publishInternalEvent('metaUpdated', updated);
return updated;

View file

@ -253,7 +253,7 @@ export class NoteCreateService implements OnApplicationShutdown {
if (data.visibility === 'public' && data.channel == null) {
const sensitiveWords = meta.sensitiveWords;
if (this.isSensitive(data, sensitiveWords)) {
if (this.utilityService.isSensitiveWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) {
data.visibility = 'home';
} else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) {
data.visibility = 'home';
@ -293,7 +293,7 @@ export class NoteCreateService implements OnApplicationShutdown {
}
// Check blocking
if (data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length === 0)) {
if (data.renote && !this.isQuote(data)) {
if (data.renote.userHost === null) {
if (data.renote.userId !== user.id) {
const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id);
@ -622,7 +622,7 @@ export class NoteCreateService implements OnApplicationShutdown {
// If it is renote
if (data.renote) {
const type = data.text ? 'quote' : 'renote';
const type = this.isQuote(data) ? 'quote' : 'renote';
// Notify
if (data.renote.userHost === null) {
@ -705,28 +705,9 @@ export class NoteCreateService implements OnApplicationShutdown {
}
@bindThis
private isSensitive(note: Option, sensitiveWord: string[]): boolean {
if (sensitiveWord.length > 0) {
const text = note.cw ?? note.text ?? '';
if (text === '') return false;
const matched = sensitiveWord.some(filter => {
// represents RegExp
const regexp = filter.match(/^\/(.+)\/(.*)$/);
// This should never happen due to input sanitisation.
if (!regexp) {
const words = filter.split(' ');
return words.every(keyword => text.includes(keyword));
}
try {
return new RE2(regexp[1], regexp[2]).test(text);
} catch (err) {
// This should never happen due to input sanitisation.
return false;
}
});
if (matched) return true;
}
return false;
private isQuote(note: Option): note is Option & { renote: MiNote } {
// sync with misc/is-quote.ts
return !!note.renote && (!!note.text || !!note.cw || (!!note.files && !!note.files.length) || !!note.poll);
}
@bindThis
@ -794,7 +775,7 @@ export class NoteCreateService implements OnApplicationShutdown {
private async renderNoteOrRenoteActivity(data: Option, note: MiNote) {
if (data.localOnly) return null;
const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length === 0)
const content = data.renote && !this.isQuote(data)
? this.apRendererService.renderAnnounce(data.renote.uri ? data.renote.uri : `${this.config.url}/notes/${data.renote.id}`, note)
: this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false), note);
@ -906,6 +887,7 @@ export class NoteCreateService implements OnApplicationShutdown {
// ダイレクトのとき、そのリストが対象外のユーザーの場合
if (
note.visibility === 'specified' &&
note.userId !== userListMembership.userListUserId &&
!note.visibleUserIds.some(v => v === userListMembership.userListUserId)
) continue;

View file

@ -138,7 +138,7 @@ export class ReactionService {
reaction = reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
// センシティブ
if ((note.reactionAcceptance === 'nonSensitiveOnly') && emoji.isSensitive) {
if ((note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && emoji.isSensitive) {
reaction = FALLBACK;
}
} else {

View file

@ -6,7 +6,14 @@
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import { In } from 'typeorm';
import type { MiRole, MiRoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/_.js';
import { ModuleRef } from '@nestjs/core';
import type {
MiRole,
MiRoleAssignment,
RoleAssignmentsRepository,
RolesRepository,
UsersRepository,
} from '@/models/_.js';
import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js';
import type { MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
@ -16,12 +23,13 @@ import { CacheService } from '@/core/CacheService.js';
import type { RoleCondFormulaValue } from '@/models/Role.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { IdService } from '@/core/IdService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import type { Packed } from '@/misc/json-schema.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import type { OnApplicationShutdown } from '@nestjs/common';
import { NotificationService } from '@/core/NotificationService.js';
import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
export type RolePolicies = {
gtlAvailable: boolean;
@ -47,6 +55,7 @@ export type RolePolicies = {
userListLimit: number;
userEachUserListsLimit: number;
rateLimitFactor: number;
avatarDecorationLimit: number;
};
export const DEFAULT_POLICIES: RolePolicies = {
@ -73,17 +82,21 @@ export const DEFAULT_POLICIES: RolePolicies = {
userListLimit: 10,
userEachUserListsLimit: 50,
rateLimitFactor: 1,
avatarDecorationLimit: 1,
};
@Injectable()
export class RoleService implements OnApplicationShutdown {
export class RoleService implements OnApplicationShutdown, OnModuleInit {
private rolesCache: MemorySingleCache<MiRole[]>;
private roleAssignmentByUserIdCache: MemoryKVCache<MiRoleAssignment[]>;
private notificationService: NotificationService;
public static AlreadyAssignedError = class extends Error {};
public static NotAssignedError = class extends Error {};
constructor(
private moduleRef: ModuleRef,
@Inject(DI.redis)
private redisClient: Redis.Redis,
@ -118,6 +131,10 @@ export class RoleService implements OnApplicationShutdown {
this.redisForSub.on('message', this.onMessage);
}
async onModuleInit() {
this.notificationService = this.moduleRef.get(NotificationService.name);
}
@bindThis
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);
@ -326,6 +343,7 @@ export class RoleService implements OnApplicationShutdown {
userListLimit: calc('userListLimit', vs => Math.max(...vs)),
userEachUserListsLimit: calc('userEachUserListsLimit', vs => Math.max(...vs)),
rateLimitFactor: calc('rateLimitFactor', vs => Math.max(...vs)),
avatarDecorationLimit: calc('avatarDecorationLimit', vs => Math.max(...vs)),
};
}
@ -424,6 +442,12 @@ export class RoleService implements OnApplicationShutdown {
this.globalEventService.publishInternalEvent('userRoleAssigned', created);
if (role.isPublic) {
this.notificationService.createNotification(userId, 'roleAssigned', {
roleId: roleId,
});
}
if (moderator) {
const user = await this.usersRepository.findOneByOrFail({ id: userId });
this.moderationLogService.log(moderator, 'assignRole', {

View file

@ -3,30 +3,34 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import { Inject, Injectable, OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
import * as Redis from 'ioredis';
import { ModuleRef } from '@nestjs/core';
import type { UserListMembershipsRepository } from '@/models/_.js';
import type { MiUser } from '@/models/User.js';
import type { MiUserList } from '@/models/UserList.js';
import type { MiUserListMembership } from '@/models/UserListMembership.js';
import { IdService } from '@/core/IdService.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ProxyAccountService } from '@/core/ProxyAccountService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { QueueService } from '@/core/QueueService.js';
import { RedisKVCache } from '@/misc/cache.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { RoleService } from '@/core/RoleService.js';
@Injectable()
export class UserListService implements OnApplicationShutdown {
export class UserListService implements OnApplicationShutdown, OnModuleInit {
public static TooManyUsersError = class extends Error {};
public membersCache: RedisKVCache<Set<string>>;
private roleService: RoleService;
constructor(
private moduleRef: ModuleRef,
@Inject(DI.redis)
private redisClient: Redis.Redis,
@ -38,7 +42,6 @@ export class UserListService implements OnApplicationShutdown {
private userEntityService: UserEntityService,
private idService: IdService,
private roleService: RoleService,
private globalEventService: GlobalEventService,
private proxyAccountService: ProxyAccountService,
private queueService: QueueService,
@ -54,6 +57,10 @@ export class UserListService implements OnApplicationShutdown {
this.redisForSub.on('message', this.onMessage);
}
async onModuleInit() {
this.roleService = this.moduleRef.get(RoleService.name);
}
@bindThis
private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data);

View file

@ -6,6 +6,7 @@
import { URL } from 'node:url';
import { toASCII } from 'punycode';
import { Inject, Injectable } from '@nestjs/common';
import RE2 from 're2';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { bindThis } from '@/decorators.js';
@ -41,6 +42,33 @@ export class UtilityService {
return silencedHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`));
}
@bindThis
public isSensitiveWordIncluded(text: string, sensitiveWords: string[]): boolean {
if (sensitiveWords.length === 0) return false;
if (text === '') return false;
const regexpregexp = /^\/(.+)\/(.*)$/;
const matched = sensitiveWords.some(filter => {
// represents RegExp
const regexp = filter.match(regexpregexp);
// This should never happen due to input sanitisation.
if (!regexp) {
const words = filter.split(' ');
return words.every(keyword => text.includes(keyword));
}
try {
// TODO: RE2インスタンスをキャッシュ
return new RE2(regexp[1], regexp[2]).test(text);
} catch (err) {
// This should never happen due to input sanitisation.
return false;
}
});
return matched;
}
@bindThis
public extractDbHost(uri: string): string {
const url = new URL(uri);

View file

@ -15,8 +15,8 @@ import type { Packed } from '@/misc/json-schema.js';
import { bindThis } from '@/decorators.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { FilterUnionByProperty, notificationTypes } from '@/types.js';
import { RoleEntityService } from './RoleEntityService.js';
import type { OnModuleInit } from '@nestjs/common';
import type { CustomEmojiService } from '../CustomEmojiService.js';
import type { UserEntityService } from './UserEntityService.js';
import type { NoteEntityService } from './NoteEntityService.js';
@ -27,7 +27,7 @@ const NOTE_REQUIRED_GROUPED_NOTIFICATION_TYPES = new Set(['note', 'mention', 're
export class NotificationEntityService implements OnModuleInit {
private userEntityService: UserEntityService;
private noteEntityService: NoteEntityService;
private customEmojiService: CustomEmojiService;
private roleEntityService: RoleEntityService;
constructor(
private moduleRef: ModuleRef,
@ -43,14 +43,13 @@ export class NotificationEntityService implements OnModuleInit {
//private userEntityService: UserEntityService,
//private noteEntityService: NoteEntityService,
//private customEmojiService: CustomEmojiService,
) {
}
onModuleInit() {
this.userEntityService = this.moduleRef.get('UserEntityService');
this.noteEntityService = this.moduleRef.get('NoteEntityService');
this.customEmojiService = this.moduleRef.get('CustomEmojiService');
this.roleEntityService = this.moduleRef.get('RoleEntityService');
}
@bindThis
@ -81,6 +80,7 @@ export class NotificationEntityService implements OnModuleInit {
detail: false,
})
) : undefined;
const role = notification.type === 'roleAssigned' ? await this.roleEntityService.pack(notification.roleId) : undefined;
return await awaitAll({
id: notification.id,
@ -92,6 +92,9 @@ export class NotificationEntityService implements OnModuleInit {
...(notification.type === 'reaction' ? {
reaction: notification.reaction,
} : {}),
...(notification.type === 'roleAssigned' ? {
role: role,
} : {}),
...(notification.type === 'achievementEarned' ? {
achievement: notification.achievement,
} : {}),
@ -216,6 +219,8 @@ export class NotificationEntityService implements OnModuleInit {
});
}
const role = notification.type === 'roleAssigned' ? await this.roleEntityService.pack(notification.roleId) : undefined;
return await awaitAll({
id: notification.id,
createdAt: new Date(notification.createdAt).toISOString(),
@ -226,6 +231,9 @@ export class NotificationEntityService implements OnModuleInit {
...(notification.type === 'reaction' ? {
reaction: notification.reaction,
} : {}),
...(notification.type === 'roleAssigned' ? {
role: role,
} : {}),
...(notification.type === 'achievementEarned' ? {
achievement: notification.achievement,
} : {}),

View file

@ -332,13 +332,13 @@ export class UserEntityService implements OnModuleInit {
const profile = opts.detail ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) : null;
const followingCount = profile == null ? null :
(profile.ffVisibility === 'public') || isMe ? user.followingCount :
(profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followingCount :
(profile.followingVisibility === 'public') || isMe ? user.followingCount :
(profile.followingVisibility === 'followers') && (relation && relation.isFollowing) ? user.followingCount :
null;
const followersCount = profile == null ? null :
(profile.ffVisibility === 'public') || isMe ? user.followersCount :
(profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount :
(profile.followersVisibility === 'public') || isMe ? user.followersCount :
(profile.followersVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount :
null;
const isModerator = isMe && opts.detail ? this.roleService.isModerator(user) : null;
@ -362,6 +362,8 @@ export class UserEntityService implements OnModuleInit {
id: ud.id,
angle: ud.angle || undefined,
flipH: ud.flipH || undefined,
offsetX: ud.offsetX || undefined,
offsetY: ud.offsetY || undefined,
url: decorations.find(d => d.id === ud.id)!.url,
}))) : [],
isBot: user.isBot,
@ -415,7 +417,8 @@ export class UserEntityService implements OnModuleInit {
pinnedPageId: profile!.pinnedPageId,
pinnedPage: profile!.pinnedPageId ? this.pageEntityService.pack(profile!.pinnedPageId, me) : null,
publicReactions: profile!.publicReactions,
ffVisibility: profile!.ffVisibility,
followersVisibility: profile!.followersVisibility,
followingVisibility: profile!.followingVisibility,
twoFactorEnabled: profile!.twoFactorEnabled,
usePasswordLessLogin: profile!.usePasswordLessLogin,
securityKeys: profile!.twoFactorEnabled

View file

@ -1,40 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export const kinds = [
'read:account',
'write:account',
'read:blocks',
'write:blocks',
'read:drive',
'write:drive',
'read:favorites',
'write:favorites',
'read:following',
'write:following',
'read:messaging',
'write:messaging',
'read:mutes',
'write:mutes',
'write:notes',
'read:notifications',
'write:notifications',
'read:reactions',
'write:reactions',
'write:votes',
'read:pages',
'write:pages',
'write:page-likes',
'read:page-likes',
'read:user-groups',
'write:user-groups',
'read:channels',
'write:channels',
'read:gallery',
'write:gallery',
'read:gallery-likes',
'write:gallery-likes',
];
// IF YOU ADD KINDS(PERMISSIONS), YOU MUST ADD TRANSLATIONS (under _permissions).

File diff suppressed because one or more lines are too long

View file

@ -7,5 +7,6 @@ import type { MiNote } from '@/models/Note.js';
// eslint-disable-next-line import/no-default-export
export default function(note: MiNote): boolean {
// sync with NoteCreateService.isQuote
return note.renoteId != null && (note.text != null || note.hasPoll || (note.fileIds != null && note.fileIds.length > 0));
}

View file

@ -38,6 +38,7 @@ import { packedFlashSchema } from '@/models/json-schema/flash.js';
import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js';
import { packedSigninSchema } from '@/models/json-schema/signin.js';
import { packedRoleLiteSchema, packedRoleSchema } from '@/models/json-schema/role.js';
import { packedAdSchema } from '@/models/json-schema/ad.js';
export const refs = {
UserLite: packedUserLiteSchema,
@ -49,6 +50,7 @@ export const refs = {
User: packedUserSchema,
UserList: packedUserListSchema,
Ad: packedAdSchema,
Announcement: packedAnnouncementSchema,
App: packedAppSchema,
Note: packedNoteSchema,

View file

@ -495,6 +495,13 @@ export class MiMeta {
})
public manifestJsonOverride: string;
@Column('varchar', {
length: 1024,
array: true,
default: '{}',
})
public bannedEmailDomains: string[];
@Column('varchar', {
length: 1024, array: true, default: '{ "admin", "administrator", "root", "system", "maintainer", "host", "mod", "moderator", "owner", "superuser", "staff", "auth", "i", "me", "everyone", "all", "mention", "mentions", "example", "user", "users", "account", "accounts", "official", "help", "helps", "support", "supports", "info", "information", "informations", "announce", "announces", "announcement", "announcements", "notice", "notification", "notifications", "dev", "developer", "developers", "tech", "misskey" }',
})

View file

@ -3,11 +3,10 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { notificationTypes } from '@/types.js';
import { MiUser } from './User.js';
import { MiNote } from './Note.js';
import { MiFollowRequest } from './FollowRequest.js';
import { MiAccessToken } from './AccessToken.js';
import { MiRole } from './Role.js';
export type MiNotification = {
type: 'note';
@ -68,6 +67,11 @@ export type MiNotification = {
id: string;
createdAt: string;
notifierId: MiUser['id'];
} | {
type: 'roleAssigned';
id: string;
createdAt: string;
roleId: MiRole['id'];
} | {
type: 'achievementEarned';
id: string;

View file

@ -143,8 +143,10 @@ export class MiUser {
})
public avatarDecorations: {
id: string;
angle: number;
flipH: boolean;
angle?: number;
flipH?: boolean;
offsetX?: number;
offsetY?: number;
}[];
@Index()

View file

@ -4,7 +4,7 @@
*/
import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
import { obsoleteNotificationTypes, ffVisibility, notificationTypes } from '@/types.js';
import { obsoleteNotificationTypes, followingVisibilities, followersVisibilities, notificationTypes } from '@/types.js';
import { id } from './util/id.js';
import { MiUser } from './User.js';
import { MiPage } from './Page.js';
@ -94,10 +94,16 @@ export class MiUserProfile {
public publicReactions: boolean;
@Column('enum', {
enum: ffVisibility,
enum: followingVisibilities,
default: 'public',
})
public ffVisibility: typeof ffVisibility[number];
public followingVisibility: typeof followingVisibilities[number];
@Column('enum', {
enum: followersVisibilities,
default: 'public',
})
public followersVisibility: typeof followersVisibilities[number];
@Column('varchar', {
length: 128, nullable: true,

View file

@ -0,0 +1,64 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export const packedAdSchema = {
type: 'object',
properties: {
id: {
type: 'string',
optional: false,
nullable: false,
format: 'id',
example: 'xxxxxxxxxx',
},
expiresAt: {
type: 'string',
optional: false,
nullable: false,
format: 'date-time',
},
startsAt: {
type: 'string',
optional: false,
nullable: false,
format: 'date-time',
},
place: {
type: 'string',
optional: false,
nullable: false,
},
priority: {
type: 'string',
optional: false,
nullable: false,
},
ratio: {
type: 'number',
optional: false,
nullable: false,
},
url: {
type: 'string',
optional: false,
nullable: false,
},
imageUrl: {
type: 'string',
optional: false,
nullable: false,
},
memo: {
type: 'string',
optional: false,
nullable: false,
},
dayOfWeek: {
type: 'integer',
optional: false,
nullable: false,
},
},
} as const;

View file

@ -145,6 +145,7 @@ export const packedRoleSchema = {
userEachUserListsLimit: rolePolicyValue,
canManageAvatarDecorations: rolePolicyValue,
canUseTranslator: rolePolicyValue,
avatarDecorationLimit: rolePolicyValue,
},
},
usersCount: {

View file

@ -74,6 +74,14 @@ export const packedUserLiteSchema = {
format: 'url',
nullable: false, optional: false,
},
offsetX: {
type: 'number',
nullable: false, optional: true,
},
offsetY: {
type: 'number',
nullable: false, optional: true,
},
},
},
},
@ -303,7 +311,12 @@ export const packedUserDetailedNotMeOnlySchema = {
type: 'boolean',
nullable: false, optional: false,
},
ffVisibility: {
followingVisibility: {
type: 'string',
nullable: false, optional: false,
enum: ['public', 'followers', 'private'],
},
followersVisibility: {
type: 'string',
nullable: false, optional: false,
enum: ['public', 'followers', 'private'],
@ -541,9 +554,7 @@ export const packedMeDetailedOnlySchema = {
mention: notificationRecieveConfig,
reaction: notificationRecieveConfig,
pollEnded: notificationRecieveConfig,
achievementEarned: notificationRecieveConfig,
receiveFollowRequest: notificationRecieveConfig,
followRequestAccepted: notificationRecieveConfig,
},
},
emailNotificationTypes: {
@ -672,6 +683,10 @@ export const packedMeDetailedOnlySchema = {
type: 'number',
nullable: false, optional: false,
},
avatarDecorationLimit: {
type: 'number',
nullable: false, optional: false,
},
},
},
//#region secrets

View file

@ -153,8 +153,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.systemQueueWorker
.on('active', (job) => systemLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => systemLogger.error(`error ${err}`, { e: renderError(err) }))
.on('failed', (job, err) => systemLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => systemLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => systemLogger.warn(`stalled id=${jobId}`));
//#endregion
@ -191,8 +191,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.dbQueueWorker
.on('active', (job) => dbLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => dbLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => dbLogger.error(`error ${err}`, { e: renderError(err) }))
.on('failed', (job, err) => dbLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => dbLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => dbLogger.warn(`stalled id=${jobId}`));
//#endregion
@ -215,8 +215,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.deliverQueueWorker
.on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
.on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
.on('failed', (job, err) => deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
.on('error', (err: Error) => deliverLogger.error(`error ${err}`, { e: renderError(err) }))
.on('failed', (job, err) => deliverLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
.on('error', (err: Error) => deliverLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => deliverLogger.warn(`stalled id=${jobId}`));
//#endregion
@ -226,7 +226,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
autorun: false,
concurrency: this.config.inboxJobConcurrency ?? 16,
limiter: {
max: this.config.inboxJobPerSec ?? 16,
max: this.config.inboxJobPerSec ?? 32,
duration: 1000,
},
settings: {
@ -239,8 +239,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.inboxQueueWorker
.on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`))
.on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
.on('failed', (job, err) => inboxLogger.warn(`failed(${err}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => inboxLogger.error(`error ${err}`, { e: renderError(err) }))
.on('failed', (job, err) => inboxLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => inboxLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => inboxLogger.warn(`stalled id=${jobId}`));
//#endregion
@ -263,8 +263,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.webhookDeliverQueueWorker
.on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
.on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
.on('failed', (job, err) => webhookLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
.on('error', (err: Error) => webhookLogger.error(`error ${err}`, { e: renderError(err) }))
.on('failed', (job, err) => webhookLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
.on('error', (err: Error) => webhookLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => webhookLogger.warn(`stalled id=${jobId}`));
//#endregion
@ -292,8 +292,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.relationshipQueueWorker
.on('active', (job) => relationshipLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => relationshipLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => relationshipLogger.error(`error ${err}`, { e: renderError(err) }))
.on('failed', (job, err) => relationshipLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => relationshipLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => relationshipLogger.warn(`stalled id=${jobId}`));
//#endregion
@ -315,8 +315,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.objectStorageQueueWorker
.on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => objectStorageLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => objectStorageLogger.error(`error ${err}`, { e: renderError(err) }))
.on('failed', (job, err) => objectStorageLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => objectStorageLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => objectStorageLogger.warn(`stalled id=${jobId}`));
//#endregion

View file

@ -138,7 +138,7 @@ export class ActivityPubServerService {
return;
}
const algo = match[1];
const algo = match[1].toUpperCase();
const digestValue = match[2];
if (algo !== 'SHA-256') {
@ -195,11 +195,11 @@ export class ActivityPubServerService {
//#region Check ff visibility
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
if (profile.ffVisibility === 'private') {
if (profile.followersVisibility === 'private') {
reply.code(403);
reply.header('Cache-Control', 'public, max-age=30');
return;
} else if (profile.ffVisibility === 'followers') {
} else if (profile.followersVisibility === 'followers') {
reply.code(403);
reply.header('Cache-Control', 'public, max-age=30');
return;
@ -287,11 +287,11 @@ export class ActivityPubServerService {
//#region Check ff visibility
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
if (profile.ffVisibility === 'private') {
if (profile.followingVisibility === 'private') {
reply.code(403);
reply.header('Cache-Control', 'public, max-age=30');
return;
} else if (profile.ffVisibility === 'followers') {
} else if (profile.followingVisibility === 'followers') {
reply.code(403);
reply.header('Cache-Control', 'public, max-age=30');
return;
@ -493,8 +493,7 @@ export class ActivityPubServerService {
@bindThis
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
// addConstraintStrategy の型定義がおかしいため
(fastify.addConstraintStrategy as any)({
fastify.addConstraintStrategy({
name: 'apOrHtml',
storage() {
const store = {} as any;

View file

@ -61,6 +61,9 @@ export class FileServerService {
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
fastify.addHook('onRequest', (request, reply, done) => {
reply.header('Content-Security-Policy', 'default-src \'none\'; img-src \'self\'; media-src \'self\'; style-src \'unsafe-inline\'');
if (process.env.NODE_ENV === 'development') {
reply.header('Access-Control-Allow-Origin', '*');
}
done();
});

View file

@ -107,7 +107,8 @@ export class ServerService implements OnApplicationShutdown {
fastify.register(this.activityPubServerService.createServer);
fastify.register(this.nodeinfoServerService.createServer);
fastify.register(this.wellKnownServerService.createServer);
fastify.register(this.oauth2ProviderService.createServer);
fastify.register(this.oauth2ProviderService.createServer, { prefix: '/oauth' });
fastify.register(this.oauth2ProviderService.createTokenServer, { prefix: '/oauth/token' });
fastify.get<{ Params: { path: string }; Querystring: { static?: any; badge?: any; }; }>('/emoji/:path(.*)', async (request, reply) => {
const path = request.params.path;

View file

@ -16,6 +16,7 @@ import * as Acct from '@/misc/acct.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { NodeinfoServerService } from './NodeinfoServerService.js';
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
import type { FindOptionsWhere } from 'typeorm';
import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
@ -30,6 +31,7 @@ export class WellKnownServerService {
private nodeinfoServerService: NodeinfoServerService,
private userEntityService: UserEntityService,
private oauth2ProviderService: OAuth2ProviderService,
) {
//this.createServer = this.createServer.bind(this);
}
@ -87,6 +89,10 @@ export class WellKnownServerService {
return { links: this.nodeinfoServerService.getLinks() };
});
fastify.get('/.well-known/oauth-authorization-server', async () => {
return this.oauth2ProviderService.generateRFC8414();
});
/* TODO
fastify.get('/.well-known/change-password', async (request, reply) => {
});

View file

@ -330,7 +330,8 @@ export class ApiCallService implements OnApplicationShutdown {
}
}
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))
|| (!ep.meta.kind && (ep.meta.requireCredential || ep.meta.requireModerator || ep.meta.requireAdmin)))) {
throw new ApiError({
message: 'Your app does not have the necessary permissions to use this endpoint.',
code: 'PERMISSION_DENIED',

View file

@ -71,6 +71,10 @@ export class StreamingApiServerService {
try {
[user, app] = await this.authenticateService.authenticate(token);
if (app !== null && !app.permission.some(p => p === 'read:account')) {
throw new AuthenticationError('Your app does not have necessary permissions to use websocket API.');
}
} catch (e) {
if (e instanceof AuthenticationError) {
socket.write([

View file

@ -4,6 +4,7 @@
*/
import type { Schema } from '@/misc/json-schema.js';
import { permissions } from 'misskey-js';
import { RolePolicies } from '@/core/RoleService.js';
import * as ep___admin_meta from './endpoints/admin/meta.js';
@ -724,7 +725,7 @@ const eps = [
['retention', ep___retention],
];
export interface IEndpointMeta {
interface IEndpointMetaBase {
readonly stability?: 'deprecated' | 'experimental' | 'stable';
readonly tags?: ReadonlyArray<string>;
@ -823,6 +824,23 @@ export interface IEndpointMeta {
readonly cacheSec?: number;
}
export type IEndpointMeta = (Omit<IEndpointMetaBase, 'requireCrential' | 'requireModerator' | 'requireAdmin'> & {
requireCredential?: false,
requireAdmin?: false,
requireModerator?: false,
}) | (Omit<IEndpointMetaBase, 'secure'> & {
secure: true,
}) | (Omit<IEndpointMetaBase, 'requireCredential' | 'kind'> & {
requireCredential: true,
kind: (typeof permissions)[number],
}) | (Omit<IEndpointMetaBase, 'requireModerator' | 'kind'> & {
requireModerator: true,
kind: (typeof permissions)[number],
}) | (Omit<IEndpointMetaBase, 'requireAdmin' | 'kind'> & {
requireAdmin: true,
kind: (typeof permissions)[number],
})
export interface IEndpoint {
name: string;
meta: IEndpointMeta;

View file

@ -15,6 +15,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'read:admin:abuse-user-reports',
res: {
type: 'array',

View file

@ -46,12 +46,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private userEntityService: UserEntityService,
private signupService: SignupService,
) {
super(meta, paramDef, async (ps, _me) => {
super(meta, paramDef, async (ps, _me, token) => {
const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null;
const noUsers = (await this.usersRepository.countBy({
host: IsNull(),
})) === 0;
if (!noUsers && !me?.isRoot) throw new Error('access denied');
if ((!noUsers && !me?.isRoot) || token !== null) throw new Error('access denied');
const { account, secret } = await this.signupService.signup({
username: ps.username,

View file

@ -16,6 +16,7 @@ export const meta = {
requireCredential: true,
requireAdmin: true,
kind: 'write:admin:account',
} as const;
export const paramDef = {

View file

@ -15,6 +15,7 @@ export const meta = {
requireCredential: true,
requireAdmin: true,
kind: 'read:admin:account',
errors: {
userNotFound: {
@ -23,6 +24,11 @@ export const meta = {
id: 'cb865949-8af5-4062-a88c-ef55e8786d1d',
},
},
res: {
type: 'object',
optional: false, nullable: false,
ref: 'User',
},
} as const;
export const paramDef = {

View file

@ -15,6 +15,13 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'write:admin:ad',
res: {
type: 'object',
optional: false,
nullable: false,
ref: 'Ad',
},
} as const;
export const paramDef = {
@ -61,7 +68,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
ad: ad,
});
return ad;
return {
id: ad.id,
expiresAt: ad.expiresAt.toISOString(),
startsAt: ad.startsAt.toISOString(),
dayOfWeek: ad.dayOfWeek,
url: ad.url,
imageUrl: ad.imageUrl,
priority: ad.priority,
ratio: ad.ratio,
place: ad.place,
memo: ad.memo,
};
});
}
}

View file

@ -15,6 +15,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'write:admin:ad',
errors: {
noSuchAd: {

View file

@ -14,6 +14,18 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'read:admin:ad',
res: {
type: 'array',
optional: false,
nullable: false,
items: {
type: 'object',
optional: false,
nullable: false,
ref: 'Ad',
},
},
} as const;
export const paramDef = {
@ -44,7 +56,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
const ads = await query.limit(ps.limit).getMany();
return ads;
return ads.map(ad => ({
id: ad.id,
expiresAt: ad.expiresAt.toISOString(),
startsAt: ad.startsAt.toISOString(),
dayOfWeek: ad.dayOfWeek,
url: ad.url,
imageUrl: ad.imageUrl,
memo: ad.memo,
place: ad.place,
priority: ad.priority,
ratio: ad.ratio,
}));
});
}
}

View file

@ -15,6 +15,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'write:admin:ad',
errors: {
noSuchAd: {

View file

@ -12,6 +12,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'write:admin:announcements',
res: {
type: 'object',

View file

@ -15,6 +15,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'write:admin:announcements',
errors: {
noSuchAnnouncement: {

View file

@ -16,6 +16,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'read:admin:announcements',
res: {
type: 'array',

View file

@ -15,6 +15,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'write:admin:announcements',
errors: {
noSuchAnnouncement: {

View file

@ -12,6 +12,7 @@ export const meta = {
requireCredential: true,
requireRolePolicy: 'canManageAvatarDecorations',
kind: 'write:admin:avatar-decorations',
} as const;
export const paramDef = {

View file

@ -14,6 +14,7 @@ export const meta = {
requireCredential: true,
requireRolePolicy: 'canManageAvatarDecorations',
kind: 'write:admin:avatar-decorations',
errors: {
},
} as const;

View file

@ -17,6 +17,7 @@ export const meta = {
requireCredential: true,
requireRolePolicy: 'canManageAvatarDecorations',
kind: 'read:admin:avatar-decorations',
res: {
type: 'array',

View file

@ -14,6 +14,7 @@ export const meta = {
requireCredential: true,
requireRolePolicy: 'canManageAvatarDecorations',
kind: 'write:admin:avatar-decorations',
errors: {
},

View file

@ -14,6 +14,7 @@ export const meta = {
requireCredential: true,
requireAdmin: true,
kind: 'write:admin:delete-account',
res: {
},

View file

@ -14,6 +14,7 @@ export const meta = {
requireCredential: true,
requireAdmin: true,
kind: 'write:admin:delete-all-files-of-a-user',
} as const;
export const paramDef = {

View file

@ -12,6 +12,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'write:admin:drive',
} as const;
export const paramDef = {

View file

@ -15,6 +15,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'write:admin:drive',
} as const;
export const paramDef = {

View file

@ -15,6 +15,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'read:admin:drive',
res: {
type: 'array',

View file

@ -16,6 +16,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'read:admin:drive',
errors: {
noSuchFile: {

View file

@ -12,6 +12,7 @@ export const meta = {
requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis',
kind: 'write:admin:emoji',
} as const;
export const paramDef = {

View file

@ -16,6 +16,7 @@ export const meta = {
requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis',
kind: 'write:admin:emoji',
errors: {
noSuchFile: {
@ -29,6 +30,8 @@ export const meta = {
id: 'f7a3462c-4e6e-4069-8421-b9bd4f4c3975',
},
},
ref: 'EmojiDetailed',
} as const;
export const paramDef = {

View file

@ -18,6 +18,7 @@ export const meta = {
requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis',
kind: 'write:admin:emoji',
errors: {
noSuchEmoji: {

View file

@ -12,6 +12,7 @@ export const meta = {
requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis',
kind: 'write:admin:emoji',
} as const;
export const paramDef = {

View file

@ -12,6 +12,7 @@ export const meta = {
requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis',
kind: 'write:admin:emoji',
errors: {
noSuchEmoji: {

View file

@ -17,6 +17,7 @@ export const meta = {
requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis',
kind: 'read:admin:emoji',
res: {
type: 'array',

View file

@ -17,6 +17,7 @@ export const meta = {
requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis',
kind: 'read:admin:emoji',
res: {
type: 'array',

View file

@ -12,6 +12,7 @@ export const meta = {
requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis',
kind: 'write:admin:emoji',
} as const;
export const paramDef = {

View file

@ -12,6 +12,7 @@ export const meta = {
requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis',
kind: 'write:admin:emoji',
} as const;
export const paramDef = {

View file

@ -12,6 +12,7 @@ export const meta = {
requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis',
kind: 'write:admin:emoji',
} as const;
export const paramDef = {

View file

@ -12,6 +12,7 @@ export const meta = {
requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis',
kind: 'write:admin:emoji',
} as const;
export const paramDef = {

View file

@ -15,6 +15,7 @@ export const meta = {
requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis',
kind: 'write:admin:emoji',
errors: {
noSuchEmoji: {

View file

@ -14,6 +14,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'write:admin:federation',
} as const;
export const paramDef = {

View file

@ -15,6 +15,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'write:admin:federation',
} as const;
export const paramDef = {

View file

@ -14,6 +14,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'write:admin:federation',
} as const;
export const paramDef = {

View file

@ -16,6 +16,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'write:admin:federation',
} as const;
export const paramDef = {

View file

@ -11,8 +11,19 @@ import { DI } from '@/di-symbols.js';
export const meta = {
requireCredential: true,
requireAdmin: true,
kind: 'read:admin:index-stats',
tags: ['admin'],
res: {
type: 'array',
items: {
type: 'object',
properties: {
tablename: { type: 'string' },
indexname: { type: 'string' },
},
},
},
} as const;
export const paramDef = {

View file

@ -11,6 +11,7 @@ import { DI } from '@/di-symbols.js';
export const meta = {
requireCredential: true,
requireAdmin: true,
kind: 'read:admin:table-stats',
tags: ['admin'],

View file

@ -14,6 +14,26 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'read:admin:user-ips',
res: {
type: 'array',
optional: false,
nullable: false,
items: {
type: 'object',
optional: false,
nullable: false,
properties: {
ip: { type: 'string' },
createdAt: {
type: 'string',
optional: false,
nullable: false,
format: 'date-time',
},
},
},
},
} as const;
export const paramDef = {

View file

@ -18,6 +18,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'write:admin:invite-codes',
errors: {
invalidDateTime: {

View file

@ -14,6 +14,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'read:admin:invite-codes',
res: {
type: 'array',

View file

@ -15,6 +15,7 @@ export const meta = {
requireCredential: true,
requireAdmin: true,
kind: 'read:admin:meta',
res: {
type: 'object',
@ -143,6 +144,14 @@ export const meta = {
type: 'string',
},
},
bannedEmailDomains: {
type: 'array',
optional: true, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
preservedUsernames: {
type: 'array',
optional: false, nullable: false,
@ -371,6 +380,10 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
shortName: {
type: 'string',
optional: false, nullable: true,
},
objectStorageS3ForcePathStyle: {
type: 'boolean',
optional: false, nullable: false,
@ -511,6 +524,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances,
enableServerMachineStats: instance.enableServerMachineStats,
enableIdenticonGeneration: instance.enableIdenticonGeneration,
bannedEmailDomains: instance.bannedEmailDomains,
policies: { ...DEFAULT_POLICIES, ...instance.policies },
manifestJsonOverride: instance.manifestJsonOverride,
enableFanoutTimeline: instance.enableFanoutTimeline,

View file

@ -15,6 +15,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'write:admin:promo',
errors: {
noSuchNote: {

View file

@ -13,6 +13,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'write:admin:queue',
} as const;
export const paramDef = {

View file

@ -13,6 +13,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'read:admin:queue',
res: {
type: 'array',

View file

@ -13,6 +13,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'read:admin:queue',
res: {
type: 'array',

View file

@ -13,6 +13,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'write:admin:queue',
} as const;
export const paramDef = {

View file

@ -12,6 +12,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'read:admin:emoji',
res: {
type: 'object',

View file

@ -14,6 +14,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'write:admin:relays',
errors: {
invalidUrl: {

View file

@ -12,6 +12,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'read:admin:relays',
res: {
type: 'array',

View file

@ -12,6 +12,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'write:admin:relays',
} as const;
export const paramDef = {

View file

@ -16,6 +16,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'write:admin:reset-password',
res: {
type: 'object',

View file

@ -17,6 +17,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'write:admin:resolve-abuse-user-report',
} as const;
export const paramDef = {

View file

@ -15,6 +15,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'write:admin:roles',
errors: {
noSuchRole: {

View file

@ -13,6 +13,7 @@ export const meta = {
requireCredential: true,
requireAdmin: true,
kind: 'write:admin:roles',
res: {
type: 'object',

View file

@ -15,6 +15,7 @@ export const meta = {
requireCredential: true,
requireAdmin: true,
kind: 'write:admin:roles',
errors: {
noSuchRole: {

View file

@ -14,6 +14,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'read:admin:roles',
res: {
type: 'array',

View file

@ -15,6 +15,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'read:admin:roles',
errors: {
noSuchRole: {

View file

@ -15,6 +15,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'write:admin:roles',
errors: {
noSuchRole: {

View file

@ -13,6 +13,7 @@ export const meta = {
requireCredential: true,
requireAdmin: true,
kind: 'write:admin:roles',
} as const;
export const paramDef = {

View file

@ -16,6 +16,7 @@ export const meta = {
requireCredential: true,
requireAdmin: true,
kind: 'write:admin:roles',
errors: {
noSuchRole: {

View file

@ -18,6 +18,7 @@ export const meta = {
requireCredential: false,
requireAdmin: true,
kind: 'read:admin:roles',
errors: {
noSuchRole: {
@ -26,6 +27,20 @@ export const meta = {
id: '224eff5e-2488-4b18-b3e7-f50d94421648',
},
},
res: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'string', format: 'misskey:id' },
createdAt: { type: 'string', format: 'date-time' },
user: { ref: 'UserDetailed' },
expiresAt: { type: 'string', format: 'date-time', nullable: true },
},
required: ['id', 'createdAt', 'user'],
},
}
} as const;
export const paramDef = {
@ -78,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
id: assign.id,
createdAt: this.idService.parse(assign.id).date.toISOString(),
user: await this.userEntityService.pack(assign.user!, me, { detail: true }),
expiresAt: assign.expiresAt,
expiresAt: assign.expiresAt?.toISOString() ?? null,
})));
});
}

View file

@ -12,6 +12,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'write:admin:send-email',
} as const;
export const paramDef = {

View file

@ -14,6 +14,7 @@ import { DI } from '@/di-symbols.js';
export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'read:admin:server-info',
tags: ['admin', 'meta'],

Some files were not shown because too many files have changed in this diff Show more