Merge remote-tracking branch 'misskey-dev/develop' into io
This commit is contained in:
8
packages/backend/generate_api_json.js
Normal file
8
packages/backend/generate_api_json.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { loadConfig } from './built/config.js'
|
||||
import { genOpenapiSpec } from './built/server/api/openapi/gen-spec.js'
|
||||
import { writeFileSync } from "node:fs";
|
||||
|
||||
const config = loadConfig();
|
||||
const spec = genOpenapiSpec(config);
|
||||
|
||||
writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8');
|
11
packages/backend/migration/1700383825690-hard-mute.js
Normal file
11
packages/backend/migration/1700383825690-hard-mute.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export class HardMute1700383825690 {
|
||||
name = 'HardMute1700383825690'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ADD "hardMutedWords" jsonb NOT NULL DEFAULT '[]'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "hardMutedWords"`);
|
||||
}
|
||||
}
|
@@ -23,7 +23,8 @@
|
||||
"jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit",
|
||||
"jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache",
|
||||
"test": "pnpm jest",
|
||||
"test-and-coverage": "pnpm jest-and-coverage"
|
||||
"test-and-coverage": "pnpm jest-and-coverage",
|
||||
"generate-api-json": "node ./generate_api_json.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-android-arm64": "1.3.11",
|
||||
|
@@ -250,6 +250,12 @@ export class MfmService {
|
||||
}
|
||||
}
|
||||
|
||||
function fnDefault(node: mfm.MfmFn) {
|
||||
const el = doc.createElement('i');
|
||||
appendChildren(node.children, el);
|
||||
return el;
|
||||
}
|
||||
|
||||
const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any } = {
|
||||
bold: (node) => {
|
||||
const el = doc.createElement('b');
|
||||
@@ -276,17 +282,68 @@ export class MfmService {
|
||||
},
|
||||
|
||||
fn: (node) => {
|
||||
if (node.props.name === 'unixtime') {
|
||||
const text = node.children[0]!.type === 'text' ? node.children[0].props.text : '';
|
||||
const date = new Date(parseInt(text, 10) * 1000);
|
||||
const el = doc.createElement('time');
|
||||
el.setAttribute('datetime', date.toISOString());
|
||||
el.textContent = date.toISOString();
|
||||
return el;
|
||||
} else {
|
||||
const el = doc.createElement('i');
|
||||
appendChildren(node.children, el);
|
||||
return el;
|
||||
switch (node.props.name) {
|
||||
case 'unixtime': {
|
||||
const text = node.children[0].type === 'text' ? node.children[0].props.text : '';
|
||||
try {
|
||||
const date = new Date(parseInt(text, 10) * 1000);
|
||||
const el = doc.createElement('time');
|
||||
el.setAttribute('datetime', date.toISOString());
|
||||
el.textContent = date.toISOString();
|
||||
return el;
|
||||
} catch (err) {
|
||||
return fnDefault(node);
|
||||
}
|
||||
}
|
||||
|
||||
case 'ruby': {
|
||||
if (node.children.length === 1) {
|
||||
const child = node.children[0];
|
||||
const text = child.type === 'text' ? child.props.text : '';
|
||||
const rubyEl = doc.createElement('ruby');
|
||||
const rtEl = doc.createElement('rt');
|
||||
|
||||
// ruby未対応のHTMLサニタイザーを通したときにルビが「劉備(りゅうび)」となるようにする
|
||||
const rpStartEl = doc.createElement('rp');
|
||||
rpStartEl.appendChild(doc.createTextNode('('));
|
||||
const rpEndEl = doc.createElement('rp');
|
||||
rpEndEl.appendChild(doc.createTextNode(')'));
|
||||
|
||||
rubyEl.appendChild(doc.createTextNode(text.split(' ')[0]));
|
||||
rtEl.appendChild(doc.createTextNode(text.split(' ')[1]));
|
||||
rubyEl.appendChild(rpStartEl);
|
||||
rubyEl.appendChild(rtEl);
|
||||
rubyEl.appendChild(rpEndEl);
|
||||
return rubyEl;
|
||||
} else {
|
||||
const rt = node.children.at(-1);
|
||||
|
||||
if (!rt) {
|
||||
return fnDefault(node);
|
||||
}
|
||||
|
||||
const text = rt.type === 'text' ? rt.props.text : '';
|
||||
const rubyEl = doc.createElement('ruby');
|
||||
const rtEl = doc.createElement('rt');
|
||||
|
||||
// ruby未対応のHTMLサニタイザーを通したときにルビが「劉備(りゅうび)」となるようにする
|
||||
const rpStartEl = doc.createElement('rp');
|
||||
rpStartEl.appendChild(doc.createTextNode('('));
|
||||
const rpEndEl = doc.createElement('rp');
|
||||
rpEndEl.appendChild(doc.createTextNode(')'));
|
||||
|
||||
appendChildren(node.children.slice(0, node.children.length - 1), rubyEl);
|
||||
rtEl.appendChild(doc.createTextNode(text.trim()));
|
||||
rubyEl.appendChild(rpStartEl);
|
||||
rubyEl.appendChild(rtEl);
|
||||
rubyEl.appendChild(rpEndEl);
|
||||
return rubyEl;
|
||||
}
|
||||
}
|
||||
|
||||
default: {
|
||||
return fnDefault(node);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@@ -473,6 +473,7 @@ export class UserEntityService implements OnModuleInit {
|
||||
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
||||
unreadNotificationsCount: notificationsInfo?.unreadCount,
|
||||
mutedWords: profile!.mutedWords,
|
||||
hardMutedWords: profile!.hardMutedWords,
|
||||
mutedInstances: profile!.mutedInstances,
|
||||
mutingNotificationTypes: [], // 後方互換性のため
|
||||
notificationRecieveConfig: profile!.notificationRecieveConfig,
|
||||
|
@@ -215,7 +215,12 @@ export class MiUserProfile {
|
||||
@Column('jsonb', {
|
||||
default: [],
|
||||
})
|
||||
public mutedWords: string[][];
|
||||
public mutedWords: (string[] | string)[];
|
||||
|
||||
@Column('jsonb', {
|
||||
default: [],
|
||||
})
|
||||
public hardMutedWords: (string[] | string)[];
|
||||
|
||||
@Column('jsonb', {
|
||||
default: [],
|
||||
|
@@ -534,6 +534,18 @@ export const packedMeDetailedOnlySchema = {
|
||||
},
|
||||
},
|
||||
},
|
||||
hardMutedWords: {
|
||||
type: 'array',
|
||||
nullable: false, optional: false,
|
||||
items: {
|
||||
type: 'array',
|
||||
nullable: false, optional: false,
|
||||
items: {
|
||||
type: 'string',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
mutedInstances: {
|
||||
type: 'array',
|
||||
nullable: true, optional: false,
|
||||
|
@@ -126,7 +126,7 @@ export class SignupApiService {
|
||||
code: invitationCode,
|
||||
});
|
||||
|
||||
if (ticket == null) {
|
||||
if (ticket == null || ticket.usedById != null) {
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
|
@@ -16,12 +16,9 @@ export const meta = {
|
||||
requireCredential: false,
|
||||
|
||||
res: {
|
||||
oneOf: [{
|
||||
type: 'object',
|
||||
ref: 'FederationInstance',
|
||||
}, {
|
||||
type: 'null',
|
||||
}],
|
||||
type: 'object',
|
||||
optional: false, nullable: true,
|
||||
ref: 'FederationInstance',
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
@@ -124,6 +124,11 @@ export const meta = {
|
||||
},
|
||||
} as const;
|
||||
|
||||
const muteWordsType = { type: 'array', items: { oneOf: [
|
||||
{ type: 'array', items: { type: 'string' } },
|
||||
{ type: 'string' }
|
||||
] } } as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -172,7 +177,8 @@ export const paramDef = {
|
||||
autoSensitive: { type: 'boolean' },
|
||||
ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
|
||||
pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
mutedWords: { type: 'array' },
|
||||
mutedWords: muteWordsType,
|
||||
hardMutedWords: muteWordsType,
|
||||
mutedInstances: { type: 'array', items: {
|
||||
type: 'string',
|
||||
} },
|
||||
@@ -235,15 +241,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
if (ps.location !== undefined) profileUpdates.location = ps.location;
|
||||
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
|
||||
if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility;
|
||||
if (ps.mutedWords !== undefined) {
|
||||
const length = ps.mutedWords.length;
|
||||
if (length > (await this.roleService.getUserPolicies(user.id)).wordMuteLimit) {
|
||||
|
||||
function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) {
|
||||
const length = mutedWords.length;
|
||||
if (length > limit) {
|
||||
throw new ApiError(meta.errors.tooManyMutedWords);
|
||||
}
|
||||
}
|
||||
|
||||
// validate regular expression syntax
|
||||
ps.mutedWords.filter(x => !Array.isArray(x)).forEach(x => {
|
||||
const regexp = x.match(/^\/(.+)\/(.*)$/);
|
||||
function validateMuteWordRegex(mutedWords: (string[] | string)[]) {
|
||||
for (const mutedWord of mutedWords) {
|
||||
if (typeof mutedWord !== "string") continue;
|
||||
|
||||
const regexp = mutedWord.match(/^\/(.+)\/(.*)$/);
|
||||
if (!regexp) throw new ApiError(meta.errors.invalidRegexp);
|
||||
|
||||
try {
|
||||
@@ -251,11 +261,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
} catch (err) {
|
||||
throw new ApiError(meta.errors.invalidRegexp);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (ps.mutedWords !== undefined) {
|
||||
checkMuteWordCount(ps.mutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit);
|
||||
validateMuteWordRegex(ps.mutedWords);
|
||||
|
||||
profileUpdates.mutedWords = ps.mutedWords;
|
||||
profileUpdates.enableWordMute = ps.mutedWords.length > 0;
|
||||
}
|
||||
if (ps.hardMutedWords !== undefined) {
|
||||
checkMuteWordCount(ps.hardMutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit);
|
||||
validateMuteWordRegex(ps.hardMutedWords);
|
||||
profileUpdates.hardMutedWords = ps.hardMutedWords;
|
||||
}
|
||||
if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances;
|
||||
if (ps.notificationRecieveConfig !== undefined) profileUpdates.notificationRecieveConfig = ps.notificationRecieveConfig;
|
||||
if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked;
|
||||
|
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import type { Config } from '@/config.js';
|
||||
import endpoints from '../endpoints.js';
|
||||
import endpoints, { IEndpoint } from '../endpoints.js';
|
||||
import { errors as basicErrors } from './errors.js';
|
||||
import { schemas, convertSchemaToOpenApiSchema } from './schemas.js';
|
||||
|
||||
@@ -33,16 +33,17 @@ export function genOpenapiSpec(config: Config) {
|
||||
schemas: schemas,
|
||||
|
||||
securitySchemes: {
|
||||
ApiKeyAuth: {
|
||||
type: 'apiKey',
|
||||
in: 'body',
|
||||
name: 'i',
|
||||
bearerAuth: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) {
|
||||
// 書き換えたりするのでディープコピーしておく。そのまま編集するとメモリ上の値が汚れて次回以降の出力に影響する
|
||||
const copiedEndpoints = JSON.parse(JSON.stringify(endpoints)) as IEndpoint[];
|
||||
for (const endpoint of copiedEndpoints.filter(ep => !ep.meta.secure)) {
|
||||
const errors = {} as any;
|
||||
|
||||
if (endpoint.meta.errors) {
|
||||
@@ -79,6 +80,13 @@ export function genOpenapiSpec(config: Config) {
|
||||
schema.required = [...schema.required ?? [], 'file'];
|
||||
}
|
||||
|
||||
if (schema.required && schema.required.length <= 0) {
|
||||
// 空配列は許可されない
|
||||
schema.required = undefined;
|
||||
}
|
||||
|
||||
const hasBody = (schema.type === 'object' && schema.properties && Object.keys(schema.properties).length >= 1);
|
||||
|
||||
const info = {
|
||||
operationId: endpoint.name,
|
||||
summary: endpoint.name,
|
||||
@@ -92,17 +100,19 @@ export function genOpenapiSpec(config: Config) {
|
||||
} : {}),
|
||||
...(endpoint.meta.requireCredential ? {
|
||||
security: [{
|
||||
ApiKeyAuth: [],
|
||||
bearerAuth: [],
|
||||
}],
|
||||
} : {}),
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
[requestType]: {
|
||||
schema,
|
||||
...(hasBody ? {
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
[requestType]: {
|
||||
schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} : {}),
|
||||
responses: {
|
||||
...(endpoint.meta.res ? {
|
||||
'200': {
|
||||
@@ -118,6 +128,11 @@ export function genOpenapiSpec(config: Config) {
|
||||
description: 'OK (without any results)',
|
||||
},
|
||||
}),
|
||||
...(endpoint.meta.res?.optional === true || endpoint.meta.res?.nullable === true ? {
|
||||
'204': {
|
||||
description: 'OK (without any results)',
|
||||
},
|
||||
} : {}),
|
||||
'400': {
|
||||
description: 'Client error',
|
||||
content: {
|
||||
@@ -190,6 +205,7 @@ export function genOpenapiSpec(config: Config) {
|
||||
};
|
||||
|
||||
spec.paths['/' + endpoint.name] = {
|
||||
...(endpoint.meta.allowGet ? { get: info } : {}),
|
||||
post: info,
|
||||
};
|
||||
}
|
||||
|
@@ -7,10 +7,16 @@ import type { Schema } from '@/misc/json-schema.js';
|
||||
import { refs } from '@/misc/json-schema.js';
|
||||
|
||||
export function convertSchemaToOpenApiSchema(schema: Schema) {
|
||||
const res: any = schema;
|
||||
// optional, refはスキーマ定義に含まれないので分離しておく
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { optional, ref, ...res }: any = schema;
|
||||
|
||||
if (schema.type === 'object' && schema.properties) {
|
||||
res.required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k);
|
||||
const required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k);
|
||||
if (required.length > 0) {
|
||||
// 空配列は許可されない
|
||||
res.required = required;
|
||||
}
|
||||
|
||||
for (const k of Object.keys(schema.properties)) {
|
||||
res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k]);
|
||||
|
@@ -169,6 +169,7 @@ describe('ユーザー', () => {
|
||||
hasPendingReceivedFollowRequest: user.hasPendingReceivedFollowRequest,
|
||||
unreadAnnouncements: user.unreadAnnouncements,
|
||||
mutedWords: user.mutedWords,
|
||||
hardMutedWords: user.hardMutedWords,
|
||||
mutedInstances: user.mutedInstances,
|
||||
mutingNotificationTypes: user.mutingNotificationTypes,
|
||||
notificationRecieveConfig: user.notificationRecieveConfig,
|
||||
|
Reference in New Issue
Block a user