Merge branch 'develop' of https://codeberg.org/calckey/calckey into note-improvements
This commit is contained in:
commit
adc8684081
1068 changed files with 48021 additions and 43445 deletions
|
@ -14,9 +14,3 @@ redis/
|
|||
files/
|
||||
misskey-assets/
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
|
11
.gitignore
vendored
11
.gitignore
vendored
|
@ -12,17 +12,6 @@ packages/backend/.idea/vcs.xml
|
|||
node_modules
|
||||
report.*.json
|
||||
|
||||
# Yarn
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
packages/client/.yarn/cache
|
||||
packages/backend/.yarn/cache
|
||||
packages/sw/.yarn/cache
|
||||
|
||||
# Cypress
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
{
|
||||
"eslint.packageManager": "yarn",
|
||||
"eslint.nodePath": ".yarn/sdks",
|
||||
"workspace.workspaceFolderCheckCwd": false,
|
||||
"tsserver.tsdk": ".yarn/sdks/typescript/lib"
|
||||
"eslint.packageManager": "pnpm",
|
||||
"workspace.workspaceFolderCheckCwd": false
|
||||
}
|
||||
|
|
5
.vscode/extensions.json
vendored
5
.vscode/extensions.json
vendored
|
@ -2,9 +2,10 @@
|
|||
"recommendations": [
|
||||
"editorconfig.editorconfig",
|
||||
"eg2.vscode-npm-script",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"rome.rome",
|
||||
"Vue.volar",
|
||||
"Vue.vscode-typescript-vue-plugin",
|
||||
"arcanis.vscode-zipfs"
|
||||
"arcanis.vscode-zipfs",
|
||||
"Orta.vscode-twoslash-queries"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,26 +1,21 @@
|
|||
pipeline:
|
||||
migrate:
|
||||
testCommit:
|
||||
image: node:latest
|
||||
commands:
|
||||
- cp .config/ci.yml .config/default.yml
|
||||
- corepack enable
|
||||
- yarn set version berry
|
||||
- yarn install --immutable
|
||||
- yarn build
|
||||
- yarn migrate
|
||||
- corepack prepare pnpm@latest --activate
|
||||
- pnpm i --frozen-lockfile
|
||||
- pnpm run build
|
||||
- pnpm run migrate
|
||||
|
||||
services:
|
||||
database:
|
||||
image: postgres:${DATABASE}
|
||||
image: postgres:15
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=test
|
||||
redis:
|
||||
image: redis
|
||||
|
||||
matrix:
|
||||
DATABASE:
|
||||
- 12
|
||||
- latest
|
||||
|
||||
branches:
|
||||
include: [ main, develop, feature/* ]
|
|
@ -1,16 +0,0 @@
|
|||
pipeline:
|
||||
build:
|
||||
image: node:${NODE_VERSION}
|
||||
commands:
|
||||
- corepack enable
|
||||
- yarn set version berry
|
||||
- yarn install --immutable
|
||||
- yarn build
|
||||
|
||||
matrix:
|
||||
NODE_VERSION:
|
||||
- 18.12.1
|
||||
- latest
|
||||
|
||||
branches:
|
||||
include: [ main, develop, feature/* ]
|
|
@ -1,7 +0,0 @@
|
|||
pipeline:
|
||||
build:
|
||||
image: node:latest
|
||||
commands:
|
||||
- corepack enable
|
||||
- yarn set version berry
|
||||
- yarn install --immutable
|
546
.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
vendored
546
.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
823
.yarn/releases/yarn-3.3.1.cjs
vendored
823
.yarn/releases/yarn-3.3.1.cjs
vendored
File diff suppressed because one or more lines are too long
20
.yarn/sdks/eslint/bin/eslint.js
vendored
20
.yarn/sdks/eslint/bin/eslint.js
vendored
|
@ -1,20 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require eslint/bin/eslint.js
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real eslint/bin/eslint.js your application uses
|
||||
module.exports = absRequire(`eslint/bin/eslint.js`);
|
20
.yarn/sdks/eslint/lib/api.js
vendored
20
.yarn/sdks/eslint/lib/api.js
vendored
|
@ -1,20 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require eslint
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real eslint your application uses
|
||||
module.exports = absRequire(`eslint`);
|
6
.yarn/sdks/eslint/package.json
vendored
6
.yarn/sdks/eslint/package.json
vendored
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"name": "eslint",
|
||||
"version": "8.30.0-sdk",
|
||||
"main": "./lib/api.js",
|
||||
"type": "commonjs"
|
||||
}
|
6
.yarn/sdks/integrations.yml
vendored
6
.yarn/sdks/integrations.yml
vendored
|
@ -1,6 +0,0 @@
|
|||
# This file is automatically generated by @yarnpkg/sdks.
|
||||
# Manual changes might be lost!
|
||||
|
||||
integrations:
|
||||
- vscode
|
||||
- vim
|
20
.yarn/sdks/typescript/bin/tsc
vendored
20
.yarn/sdks/typescript/bin/tsc
vendored
|
@ -1,20 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript/bin/tsc
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real typescript/bin/tsc your application uses
|
||||
module.exports = absRequire(`typescript/bin/tsc`);
|
20
.yarn/sdks/typescript/bin/tsserver
vendored
20
.yarn/sdks/typescript/bin/tsserver
vendored
|
@ -1,20 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript/bin/tsserver
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real typescript/bin/tsserver your application uses
|
||||
module.exports = absRequire(`typescript/bin/tsserver`);
|
20
.yarn/sdks/typescript/lib/tsc.js
vendored
20
.yarn/sdks/typescript/lib/tsc.js
vendored
|
@ -1,20 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript/lib/tsc.js
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real typescript/lib/tsc.js your application uses
|
||||
module.exports = absRequire(`typescript/lib/tsc.js`);
|
223
.yarn/sdks/typescript/lib/tsserver.js
vendored
223
.yarn/sdks/typescript/lib/tsserver.js
vendored
|
@ -1,223 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
const moduleWrapper = tsserver => {
|
||||
if (!process.versions.pnp) {
|
||||
return tsserver;
|
||||
}
|
||||
|
||||
const {isAbsolute} = require(`path`);
|
||||
const pnpApi = require(`pnpapi`);
|
||||
|
||||
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
|
||||
const isPortal = str => str.startsWith("portal:/");
|
||||
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
|
||||
|
||||
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
|
||||
return `${locator.name}@${locator.reference}`;
|
||||
}));
|
||||
|
||||
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
|
||||
// doesn't understand. This layer makes sure to remove the protocol
|
||||
// before forwarding it to TS, and to add it back on all returned paths.
|
||||
|
||||
function toEditorPath(str) {
|
||||
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
|
||||
if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) {
|
||||
// We also take the opportunity to turn virtual paths into physical ones;
|
||||
// this makes it much easier to work with workspaces that list peer
|
||||
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
|
||||
// file instances instead of the real ones.
|
||||
//
|
||||
// We only do this to modules owned by the the dependency tree roots.
|
||||
// This avoids breaking the resolution when jumping inside a vendor
|
||||
// with peer dep (otherwise jumping into react-dom would show resolution
|
||||
// errors on react).
|
||||
//
|
||||
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
|
||||
if (resolved) {
|
||||
const locator = pnpApi.findPackageLocator(resolved);
|
||||
if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) {
|
||||
str = resolved;
|
||||
}
|
||||
}
|
||||
|
||||
str = normalize(str);
|
||||
|
||||
if (str.match(/\.zip\//)) {
|
||||
switch (hostInfo) {
|
||||
// Absolute VSCode `Uri.fsPath`s need to start with a slash.
|
||||
// VSCode only adds it automatically for supported schemes,
|
||||
// so we have to do it manually for the `zip` scheme.
|
||||
// The path needs to start with a caret otherwise VSCode doesn't handle the protocol
|
||||
//
|
||||
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
|
||||
//
|
||||
// 2021-10-08: VSCode changed the format in 1.61.
|
||||
// Before | ^zip:/c:/foo/bar.zip/package.json
|
||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||
//
|
||||
// 2022-04-06: VSCode changed the format in 1.66.
|
||||
// Before | ^/zip//c:/foo/bar.zip/package.json
|
||||
// After | ^/zip/c:/foo/bar.zip/package.json
|
||||
//
|
||||
// 2022-05-06: VSCode changed the format in 1.68
|
||||
// Before | ^/zip/c:/foo/bar.zip/package.json
|
||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||
//
|
||||
case `vscode <1.61`: {
|
||||
str = `^zip:${str}`;
|
||||
} break;
|
||||
|
||||
case `vscode <1.66`: {
|
||||
str = `^/zip/${str}`;
|
||||
} break;
|
||||
|
||||
case `vscode <1.68`: {
|
||||
str = `^/zip${str}`;
|
||||
} break;
|
||||
|
||||
case `vscode`: {
|
||||
str = `^/zip/${str}`;
|
||||
} break;
|
||||
|
||||
// To make "go to definition" work,
|
||||
// We have to resolve the actual file system path from virtual path
|
||||
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
|
||||
case `coc-nvim`: {
|
||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||
str = resolve(`zipfile:${str}`);
|
||||
} break;
|
||||
|
||||
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
|
||||
// We have to resolve the actual file system path from virtual path,
|
||||
// everything else is up to neovim
|
||||
case `neovim`: {
|
||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||
str = `zipfile://${str}`;
|
||||
} break;
|
||||
|
||||
default: {
|
||||
str = `zip:${str}`;
|
||||
} break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
function fromEditorPath(str) {
|
||||
switch (hostInfo) {
|
||||
case `coc-nvim`: {
|
||||
str = str.replace(/\.zip::/, `.zip/`);
|
||||
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
|
||||
// So in order to convert it back, we use .* to match all the thing
|
||||
// before `zipfile:`
|
||||
return process.platform === `win32`
|
||||
? str.replace(/^.*zipfile:\//, ``)
|
||||
: str.replace(/^.*zipfile:/, ``);
|
||||
} break;
|
||||
|
||||
case `neovim`: {
|
||||
str = str.replace(/\.zip::/, `.zip/`);
|
||||
// The path for neovim is in format of zipfile:///<pwd>/.yarn/...
|
||||
return str.replace(/^zipfile:\/\//, ``);
|
||||
} break;
|
||||
|
||||
case `vscode`:
|
||||
default: {
|
||||
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`)
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
// Force enable 'allowLocalPluginLoads'
|
||||
// TypeScript tries to resolve plugins using a path relative to itself
|
||||
// which doesn't work when using the global cache
|
||||
// https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238
|
||||
// VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but
|
||||
// TypeScript already does local loads and if this code is running the user trusts the workspace
|
||||
// https://github.com/microsoft/vscode/issues/45856
|
||||
const ConfiguredProject = tsserver.server.ConfiguredProject;
|
||||
const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype;
|
||||
ConfiguredProject.prototype.enablePluginsWithOptions = function() {
|
||||
this.projectService.allowLocalPluginLoads = true;
|
||||
return originalEnablePluginsWithOptions.apply(this, arguments);
|
||||
};
|
||||
|
||||
// And here is the point where we hijack the VSCode <-> TS communications
|
||||
// by adding ourselves in the middle. We locate everything that looks
|
||||
// like an absolute path of ours and normalize it.
|
||||
|
||||
const Session = tsserver.server.Session;
|
||||
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype;
|
||||
let hostInfo = `unknown`;
|
||||
|
||||
Object.assign(Session.prototype, {
|
||||
onMessage(/** @type {string | object} */ message) {
|
||||
const isStringMessage = typeof message === 'string';
|
||||
const parsedMessage = isStringMessage ? JSON.parse(message) : message;
|
||||
|
||||
if (
|
||||
parsedMessage != null &&
|
||||
typeof parsedMessage === `object` &&
|
||||
parsedMessage.arguments &&
|
||||
typeof parsedMessage.arguments.hostInfo === `string`
|
||||
) {
|
||||
hostInfo = parsedMessage.arguments.hostInfo;
|
||||
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
|
||||
const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match(
|
||||
// The RegExp from https://semver.org/ but without the caret at the start
|
||||
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
|
||||
) ?? []).map(Number)
|
||||
|
||||
if (major === 1) {
|
||||
if (minor < 61) {
|
||||
hostInfo += ` <1.61`;
|
||||
} else if (minor < 66) {
|
||||
hostInfo += ` <1.66`;
|
||||
} else if (minor < 68) {
|
||||
hostInfo += ` <1.68`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
|
||||
return typeof value === 'string' ? fromEditorPath(value) : value;
|
||||
});
|
||||
|
||||
return originalOnMessage.call(
|
||||
this,
|
||||
isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON)
|
||||
);
|
||||
},
|
||||
|
||||
send(/** @type {any} */ msg) {
|
||||
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
|
||||
return typeof value === `string` ? toEditorPath(value) : value;
|
||||
})));
|
||||
}
|
||||
});
|
||||
|
||||
return tsserver;
|
||||
};
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript/lib/tsserver.js
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real typescript/lib/tsserver.js your application uses
|
||||
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserver.js`));
|
223
.yarn/sdks/typescript/lib/tsserverlibrary.js
vendored
223
.yarn/sdks/typescript/lib/tsserverlibrary.js
vendored
|
@ -1,223 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
const moduleWrapper = tsserver => {
|
||||
if (!process.versions.pnp) {
|
||||
return tsserver;
|
||||
}
|
||||
|
||||
const {isAbsolute} = require(`path`);
|
||||
const pnpApi = require(`pnpapi`);
|
||||
|
||||
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
|
||||
const isPortal = str => str.startsWith("portal:/");
|
||||
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
|
||||
|
||||
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
|
||||
return `${locator.name}@${locator.reference}`;
|
||||
}));
|
||||
|
||||
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
|
||||
// doesn't understand. This layer makes sure to remove the protocol
|
||||
// before forwarding it to TS, and to add it back on all returned paths.
|
||||
|
||||
function toEditorPath(str) {
|
||||
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
|
||||
if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) {
|
||||
// We also take the opportunity to turn virtual paths into physical ones;
|
||||
// this makes it much easier to work with workspaces that list peer
|
||||
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
|
||||
// file instances instead of the real ones.
|
||||
//
|
||||
// We only do this to modules owned by the the dependency tree roots.
|
||||
// This avoids breaking the resolution when jumping inside a vendor
|
||||
// with peer dep (otherwise jumping into react-dom would show resolution
|
||||
// errors on react).
|
||||
//
|
||||
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
|
||||
if (resolved) {
|
||||
const locator = pnpApi.findPackageLocator(resolved);
|
||||
if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) {
|
||||
str = resolved;
|
||||
}
|
||||
}
|
||||
|
||||
str = normalize(str);
|
||||
|
||||
if (str.match(/\.zip\//)) {
|
||||
switch (hostInfo) {
|
||||
// Absolute VSCode `Uri.fsPath`s need to start with a slash.
|
||||
// VSCode only adds it automatically for supported schemes,
|
||||
// so we have to do it manually for the `zip` scheme.
|
||||
// The path needs to start with a caret otherwise VSCode doesn't handle the protocol
|
||||
//
|
||||
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
|
||||
//
|
||||
// 2021-10-08: VSCode changed the format in 1.61.
|
||||
// Before | ^zip:/c:/foo/bar.zip/package.json
|
||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||
//
|
||||
// 2022-04-06: VSCode changed the format in 1.66.
|
||||
// Before | ^/zip//c:/foo/bar.zip/package.json
|
||||
// After | ^/zip/c:/foo/bar.zip/package.json
|
||||
//
|
||||
// 2022-05-06: VSCode changed the format in 1.68
|
||||
// Before | ^/zip/c:/foo/bar.zip/package.json
|
||||
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||
//
|
||||
case `vscode <1.61`: {
|
||||
str = `^zip:${str}`;
|
||||
} break;
|
||||
|
||||
case `vscode <1.66`: {
|
||||
str = `^/zip/${str}`;
|
||||
} break;
|
||||
|
||||
case `vscode <1.68`: {
|
||||
str = `^/zip${str}`;
|
||||
} break;
|
||||
|
||||
case `vscode`: {
|
||||
str = `^/zip/${str}`;
|
||||
} break;
|
||||
|
||||
// To make "go to definition" work,
|
||||
// We have to resolve the actual file system path from virtual path
|
||||
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
|
||||
case `coc-nvim`: {
|
||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||
str = resolve(`zipfile:${str}`);
|
||||
} break;
|
||||
|
||||
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
|
||||
// We have to resolve the actual file system path from virtual path,
|
||||
// everything else is up to neovim
|
||||
case `neovim`: {
|
||||
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||
str = `zipfile://${str}`;
|
||||
} break;
|
||||
|
||||
default: {
|
||||
str = `zip:${str}`;
|
||||
} break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
function fromEditorPath(str) {
|
||||
switch (hostInfo) {
|
||||
case `coc-nvim`: {
|
||||
str = str.replace(/\.zip::/, `.zip/`);
|
||||
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
|
||||
// So in order to convert it back, we use .* to match all the thing
|
||||
// before `zipfile:`
|
||||
return process.platform === `win32`
|
||||
? str.replace(/^.*zipfile:\//, ``)
|
||||
: str.replace(/^.*zipfile:/, ``);
|
||||
} break;
|
||||
|
||||
case `neovim`: {
|
||||
str = str.replace(/\.zip::/, `.zip/`);
|
||||
// The path for neovim is in format of zipfile:///<pwd>/.yarn/...
|
||||
return str.replace(/^zipfile:\/\//, ``);
|
||||
} break;
|
||||
|
||||
case `vscode`:
|
||||
default: {
|
||||
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`)
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
// Force enable 'allowLocalPluginLoads'
|
||||
// TypeScript tries to resolve plugins using a path relative to itself
|
||||
// which doesn't work when using the global cache
|
||||
// https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238
|
||||
// VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but
|
||||
// TypeScript already does local loads and if this code is running the user trusts the workspace
|
||||
// https://github.com/microsoft/vscode/issues/45856
|
||||
const ConfiguredProject = tsserver.server.ConfiguredProject;
|
||||
const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype;
|
||||
ConfiguredProject.prototype.enablePluginsWithOptions = function() {
|
||||
this.projectService.allowLocalPluginLoads = true;
|
||||
return originalEnablePluginsWithOptions.apply(this, arguments);
|
||||
};
|
||||
|
||||
// And here is the point where we hijack the VSCode <-> TS communications
|
||||
// by adding ourselves in the middle. We locate everything that looks
|
||||
// like an absolute path of ours and normalize it.
|
||||
|
||||
const Session = tsserver.server.Session;
|
||||
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype;
|
||||
let hostInfo = `unknown`;
|
||||
|
||||
Object.assign(Session.prototype, {
|
||||
onMessage(/** @type {string | object} */ message) {
|
||||
const isStringMessage = typeof message === 'string';
|
||||
const parsedMessage = isStringMessage ? JSON.parse(message) : message;
|
||||
|
||||
if (
|
||||
parsedMessage != null &&
|
||||
typeof parsedMessage === `object` &&
|
||||
parsedMessage.arguments &&
|
||||
typeof parsedMessage.arguments.hostInfo === `string`
|
||||
) {
|
||||
hostInfo = parsedMessage.arguments.hostInfo;
|
||||
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
|
||||
const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match(
|
||||
// The RegExp from https://semver.org/ but without the caret at the start
|
||||
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
|
||||
) ?? []).map(Number)
|
||||
|
||||
if (major === 1) {
|
||||
if (minor < 61) {
|
||||
hostInfo += ` <1.61`;
|
||||
} else if (minor < 66) {
|
||||
hostInfo += ` <1.66`;
|
||||
} else if (minor < 68) {
|
||||
hostInfo += ` <1.68`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
|
||||
return typeof value === 'string' ? fromEditorPath(value) : value;
|
||||
});
|
||||
|
||||
return originalOnMessage.call(
|
||||
this,
|
||||
isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON)
|
||||
);
|
||||
},
|
||||
|
||||
send(/** @type {any} */ msg) {
|
||||
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
|
||||
return typeof value === `string` ? toEditorPath(value) : value;
|
||||
})));
|
||||
}
|
||||
});
|
||||
|
||||
return tsserver;
|
||||
};
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript/lib/tsserverlibrary.js
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real typescript/lib/tsserverlibrary.js your application uses
|
||||
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserverlibrary.js`));
|
20
.yarn/sdks/typescript/lib/typescript.js
vendored
20
.yarn/sdks/typescript/lib/typescript.js
vendored
|
@ -1,20 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const {existsSync} = require(`fs`);
|
||||
const {createRequire} = require(`module`);
|
||||
const {resolve} = require(`path`);
|
||||
|
||||
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||
|
||||
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||
const absRequire = createRequire(absPnpApiPath);
|
||||
|
||||
if (existsSync(absPnpApiPath)) {
|
||||
if (!process.versions.pnp) {
|
||||
// Setup the environment to be able to require typescript/lib/typescript.js
|
||||
require(absPnpApiPath).setup();
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the real typescript/lib/typescript.js your application uses
|
||||
module.exports = absRequire(`typescript/lib/typescript.js`);
|
6
.yarn/sdks/typescript/package.json
vendored
6
.yarn/sdks/typescript/package.json
vendored
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"name": "typescript",
|
||||
"version": "4.9.4-sdk",
|
||||
"main": "./lib/typescript.js",
|
||||
"type": "commonjs"
|
||||
}
|
40
.yarnrc.yml
40
.yarnrc.yml
|
@ -1,40 +0,0 @@
|
|||
httpTimeout: 600000
|
||||
|
||||
nmHoistingLimits: none
|
||||
|
||||
nodeLinker: pnpm
|
||||
|
||||
packageExtensions:
|
||||
"@bull-board/api@*":
|
||||
peerDependencies:
|
||||
"@bull-board/ui": "*"
|
||||
"@vitejs/plugin-vue@*":
|
||||
dependencies:
|
||||
supports-color: "*"
|
||||
chartjs-adapter-date-fns@*:
|
||||
peerDependencies:
|
||||
date-fns: "*"
|
||||
consolidate@*:
|
||||
dependencies:
|
||||
ejs: "*"
|
||||
koa-views@*:
|
||||
dependencies:
|
||||
pug: "*"
|
||||
swiper@*:
|
||||
peerDependencies:
|
||||
vue: "*"
|
||||
vite@*:
|
||||
dependencies:
|
||||
bufferutil: "*"
|
||||
supports-color: "*"
|
||||
utf-8-validate: "*"
|
||||
|
||||
plugins:
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
||||
spec: "@yarnpkg/plugin-interactive-tools"
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
|
||||
spec: "@yarnpkg/plugin-workspace-tools"
|
||||
|
||||
progressBarStyle: patrick
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.3.1.cjs
|
|
@ -41,7 +41,7 @@
|
|||
## Implemented
|
||||
|
||||
- A lot of general bugfixes
|
||||
- Yarn 3
|
||||
- pnpm instead of yarn
|
||||
- Fix Dockerfile @hanna
|
||||
- Upgrade packages with security vunrabilities
|
||||
- Saner defaults
|
||||
|
@ -104,6 +104,10 @@
|
|||
- Improve blocking instances
|
||||
- Release notes
|
||||
- New post style
|
||||
- Admins set default reaction emoji
|
||||
- Allows custom emoji
|
||||
- Fix lint errors
|
||||
- Use Rome instead of ESLint
|
||||
- MissV: [fix Misskey Forkbomb](https://code.vtopia.live/Vtopia/MissV/commit/40b23c070bd4adbb3188c73546c6c625138fb3c1)
|
||||
- [Make showing ads optional](https://github.com/misskey-dev/misskey/pull/8996)
|
||||
- [Tapping avatar in mobile opens account modal](https://github.com/misskey-dev/misskey/pull/9056)
|
||||
|
|
|
@ -49,7 +49,7 @@ Thank you for your PR! Before creating a PR, please check the following:
|
|||
- Check if there are any documents that need to be created or updated due to this change.
|
||||
- If you have added a feature or fixed a bug, please add a test case if possible.
|
||||
- Please make sure that tests and Lint are passed in advance.
|
||||
- You can run it with `yarn test` and `yarn lint`. [See more info](#testing)
|
||||
- You can run it with `pnpm run test` and `pnpm run lint`. [See more info](#testing)
|
||||
- If this PR includes UI changes, please attach a screenshot in the text.
|
||||
|
||||
Thanks for your cooperation 🤗
|
||||
|
@ -255,7 +255,7 @@ MongoDBは`null`で返してきてたので、その感覚で`if (x === null)`
|
|||
### Migration作成方法
|
||||
packages/backendで:
|
||||
```sh
|
||||
yarn dlx typeorm migration:generate -d ormconfig.js -o <migration name>
|
||||
pnpm dlx typeorm migration:generate -d ormconfig.js -o <migration name>
|
||||
```
|
||||
|
||||
- 生成後、ファイルをmigration下に移してください
|
||||
|
|
13
Dockerfile
13
Dockerfile
|
@ -1,5 +1,4 @@
|
|||
FROM node:19-alpine
|
||||
ENV YARN_CHECKSUM_BEHAVIOR=update
|
||||
ARG NODE_ENV=production
|
||||
WORKDIR /calckey
|
||||
|
||||
|
@ -10,17 +9,17 @@ COPY . ./
|
|||
RUN apk update
|
||||
RUN apk add git ffmpeg tini alpine-sdk python3
|
||||
|
||||
# Configure corepack and yarn
|
||||
# Configure corepack and pnpm
|
||||
RUN corepack enable
|
||||
RUN yarn set version berry
|
||||
RUN yarn install --immutable
|
||||
RUN yarn plugin import workspace-tools
|
||||
RUN corepack prepare pnpm@latest --activate
|
||||
RUN pnpm i --frozen-lockfile
|
||||
ARG NODE_ENV=production
|
||||
|
||||
# Build project (pnp dependencies are installed)
|
||||
RUN yarn run rebuild
|
||||
RUN pnpm run build
|
||||
|
||||
# Remove git files
|
||||
RUN rm -rf .git
|
||||
|
||||
ENTRYPOINT [ "/sbin/tini", "--" ]
|
||||
CMD [ "yarn", "run", "migrateandstart" ]
|
||||
CMD [ "pnpm", "run", "migrateandstart" ]
|
||||
|
|
34
README.md
34
README.md
|
@ -5,13 +5,13 @@
|
|||
|
||||
**🌎 **[Calckey](https://i.calckey.cloud/)** is an open source, decentralized social media platform that's free forever! 🚀**
|
||||
|
||||
[![no-github-badge](https://nogithub.codeberg.page/badge.svg)](https://nogithub.codeberg.page/)
|
||||
[![status-badge](https://ci.codeberg.org/api/badges/calckey/calckey/status.svg)](https://ci.codeberg.org/calckey/calckey)
|
||||
[![liberapay-badge](https://img.shields.io/liberapay/receives/ThatOneCalculator?logo=liberapay)](https://liberapay.com/ThatOneCalculator)
|
||||
[![no github badge](https://nogithub.codeberg.page/badge.svg)](https://nogithub.codeberg.page/)
|
||||
[![status badge](https://ci.lavaforge.org/api/badges/calckey/calckey/status.svg)](https://ci.lavaforge.org/calckey/calckey)
|
||||
[![liberapay badge](https://img.shields.io/liberapay/receives/ThatOneCalculator?logo=liberapay)](https://liberapay.com/ThatOneCalculator)
|
||||
[![translate-badge](https://hosted.weblate.org/widgets/calckey/-/svg-badge.svg)](https://hosted.weblate.org/engage/calckey/)
|
||||
[![docker-badge](https://img.shields.io/docker/pulls/thatonecalculator/calckey?logo=docker)](https://hub.docker.com/r/thatonecalculator/calckey)
|
||||
[![docker badge](https://img.shields.io/docker/pulls/thatonecalculator/calckey?logo=docker)](https://hub.docker.com/r/thatonecalculator/calckey)
|
||||
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](./CODE_OF_CONDUCT.md)
|
||||
[![codeberg-badge](https://custom-icon-badges.demolab.com/badge/hosted%20on-codeberg-blue.svg?logo=codeberg&logoColor=white)](https://codeberg.org/calckey/calckey/)
|
||||
[![lavaforge badge](https://custom-icon-badges.demolab.com/badge/hosted%20on-lavaforge-FF8066.svg?logo=lavaforge&logoColor=white)](https://lavaforge.org/calckey/calckey/)
|
||||
|
||||
</div>
|
||||
|
||||
|
@ -58,6 +58,13 @@
|
|||
|
||||
This guide will work for both **starting from scratch** and **migrating from Misskey**.
|
||||
|
||||
## 🔰 Easy installers
|
||||
|
||||
If you have access to a server that supports one of the sources below, I recommend you use it! Note that these methods *won't* allow you to migrate from Misskey without manual intervention.
|
||||
|
||||
[![Install on Ubuntu](https://pool.jortage.com/voringme/misskey/3b62a443-1b44-45cf-8f9e-f1c588f803ed.png)](https://codeberg.org/calckey/ubuntu-bash-install) [![Install on the Arch User Repository](https://pool.jortage.com/voringme/misskey/ba2a5c07-f078-43f1-8483-2e01acca9c40.png)](https://aur.archlinux.org/packages/calckey) [![Install Calckey with YunoHost](https://install-app.yunohost.org/install-with-yunohost.svg)](https://install-app.yunohost.org/?app=calckey)
|
||||
|
||||
|
||||
## 🧑💻 Dependencies
|
||||
|
||||
- 🐢 At least [NodeJS](https://nodejs.org/en/) v18.12.1 (v19 recommended)
|
||||
|
@ -85,7 +92,7 @@ This guide will work for both **starting from scratch** and **migrating from Mis
|
|||
## 👀 Get folder ready
|
||||
|
||||
```sh
|
||||
git clone https://codeberg.org/calckey/calckey.git
|
||||
git clone https://lavaforge.org/calckey/calckey.git
|
||||
cd calckey/
|
||||
# git checkout main # if you want only stable versions
|
||||
```
|
||||
|
@ -95,6 +102,8 @@ cd calckey/
|
|||
```sh
|
||||
# nvm install 19 && nvm use 19
|
||||
corepack enable
|
||||
corepack prepare pnpm@latest --activate
|
||||
pnpm i
|
||||
```
|
||||
|
||||
## 🐘 Create database
|
||||
|
@ -110,7 +119,7 @@ psql postgres -c "create database calckey with encoding = 'UTF8';"
|
|||
- To add custom CSS for all users, edit `./custom/assets/instance.css`.
|
||||
- To add static assets (such as images for the splash screen), place them in the `./custom/assets/` directory. They'll then be available on `https://yourinstance.tld/static-assets/filename.ext`.
|
||||
- To add custom locales, place them in the `./custom/locales/` directory. If you name your custom locale the same as an existing locale, it will overwrite it. If you give it a unique name, it will be added to the list. Also make sure that the first part of the filename matches the locale you're basing it on. (Example: `en-FOO.yml`)
|
||||
- To update custom assets without rebuilding, just run `yarn run gulp`.
|
||||
- To update custom assets without rebuilding, just run `pnpm run gulp`.
|
||||
|
||||
## 🧑🔬 Configuring a new instance
|
||||
|
||||
|
@ -124,7 +133,7 @@ psql postgres -c "create database calckey with encoding = 'UTF8';"
|
|||
|
||||
```sh
|
||||
cp ../misskey/.config/default.yml ./.config/default.yml # replace `../misskey/` with misskey path, add `docker.env` if you use Docker
|
||||
cp -r ../misskey/files . # if you don't use object storage
|
||||
cp -r ../misskey/files .
|
||||
```
|
||||
|
||||
## 🍀 NGINX
|
||||
|
@ -144,9 +153,8 @@ cp -r ../misskey/files . # if you don't use object storage
|
|||
|
||||
```sh
|
||||
# git pull
|
||||
yarn install
|
||||
NODE_ENV=production yarn run rebuild && yarn run migrate
|
||||
pm2 start "NODE_ENV=production yarn start" --name Calckey
|
||||
NODE_ENV=production pnpm install && pnpm run build && pnpm run migrate
|
||||
pm2 start "NODE_ENV=production pnpm run start" --name Calckey
|
||||
```
|
||||
|
||||
### 🐋 Docker
|
||||
|
@ -156,10 +164,10 @@ pm2 start "NODE_ENV=production yarn start" --name Calckey
|
|||
## 😉 Tips & Tricks
|
||||
|
||||
- When editing the config file, please don't fill out the settings at the bottom. They're designed *only* for managed hosting, not self hosting. Those settings are much better off being set in Calckey's control panel.
|
||||
- Port 3000 (used in the default config) might be already used on your server for something else. To find an open port for Calckey, run `for p in $(seq 3000 4000); do ss -tlnH | tr -s ' ' | cut -d" " -sf4 | grep -q "${p}$" || echo "${p}"; done | head -n 1`
|
||||
- Port 3000 (used in the default config) might be already used on your server for something else. To find an open port for Calckey, run `for p in {3000..4000}; do ss -tlnH | tr -s ' ' | cut -d" " -sf4 | grep -q "${p}$" || echo "${p}"; done | head -n 1`. Replace 3000 with the minimum port and 4000 with the maximum port if you need it.
|
||||
- I'd recommend you use a S3 Bucket/CDN for Object Storage, especially if you use Docker.
|
||||
- I'd ***strongly*** recommend against using CloudFlare, but if you do, make sure to turn code minification off.
|
||||
- For push notifications, run `npx web-push generate-vapid-keys`, the put the public and private keys into Control Panel > General > ServiceWorker.
|
||||
- For push notifications, run `npx web-push generate-vapid-keys`, then put the public and private keys into Control Panel > General > ServiceWorker.
|
||||
- For translations, make a [DeepL](https://deepl.com) account and generate an API key, then put it into Control Panel > General > DeepL Translation.
|
||||
- To add another admin account:
|
||||
- Go to the user's page > 3 Dots > About > Moderation > turn on "Moderator"
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
|
|
|
@ -42,6 +42,6 @@ Once the instance is up you can use a web browser to access the web interface at
|
|||
```sh
|
||||
cd dev/
|
||||
docker-compose build
|
||||
docker-compose run --rm web yarn run init
|
||||
docker-compose run --rm web pnpm run init
|
||||
docker-compose up -d
|
||||
```
|
70
issue_template/bug.yaml
Normal file
70
issue_template/bug.yaml
Normal file
|
@ -0,0 +1,70 @@
|
|||
name: Bug Report
|
||||
about: File a bug report
|
||||
title: "[Bug]: "
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Please give us a brief description of what happened.
|
||||
placeholder: Tell us what you see!
|
||||
value: "A bug happened!"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: what-is-expected
|
||||
attributes:
|
||||
label: What did you expect to happen?
|
||||
description: Please give us a brief description of what you expected to happen.
|
||||
placeholder: Tell us what you wish happened!
|
||||
value: "Instead of x, y should happen instead!"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version of calckey is your instance running? You can find this by clicking your instance's logo at the bottom left and then clicking instance information.
|
||||
placeholder: Calckey Version 13.0.4
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: instance
|
||||
attributes:
|
||||
label: Instance
|
||||
description: What instance of calckey are you using?
|
||||
placeholder: stop.voring.me
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
label: What browser are you using?
|
||||
multiple: false
|
||||
options:
|
||||
- Firefox
|
||||
- Chrome
|
||||
- Brave
|
||||
- Librewolf
|
||||
- Chromium
|
||||
- Safari
|
||||
- Microsoft Edge
|
||||
- Other (Please Specify)
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. You can find your log by inspecting the page, and going to the "console" tab. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Contribution Guidelines
|
||||
description: By submitting this issue, you agree to follow our [Contribution Guidelines](https://lavaforge.org/calckey/calckey/src/branch/develop/CONTRIBUTING.md)
|
||||
options:
|
||||
- label: I agree to follow this project's Contribution Guidelines
|
||||
required: true
|
70
issue_template/feature.yaml
Normal file
70
issue_template/feature.yaml
Normal file
|
@ -0,0 +1,70 @@
|
|||
name: Feature Request
|
||||
about: Request a Feature
|
||||
title: "[Feature]: "
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this feature request!
|
||||
- type: textarea
|
||||
id: what-feature
|
||||
attributes:
|
||||
label: What feature would you like implemented?
|
||||
description: Please give us a brief description of what you'd like.
|
||||
placeholder: Tell us what you want!
|
||||
value: "x feature would be great!"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: why-add-feature
|
||||
attributes:
|
||||
label: Why should we add this feature?
|
||||
description: Please give us a brief description of why your feature is important.
|
||||
placeholder: Tell us why you want this feature!
|
||||
value: "x feature is super useful because y!"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version of calckey is your instance running? You can find this by clicking your instance's logo at the bottom left and then clicking instance information.
|
||||
placeholder: Calckey Version 13.0.4
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: instance
|
||||
attributes:
|
||||
label: Instance
|
||||
description: What instance of calckey are you using?
|
||||
placeholder: stop.voring.me
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
label: What browser are you using?
|
||||
multiple: false
|
||||
options:
|
||||
- Firefox
|
||||
- Chrome
|
||||
- Brave
|
||||
- Librewolf
|
||||
- Chromium
|
||||
- Safari
|
||||
- Microsoft Edge
|
||||
- Other (Please Specify)
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. You can find your log by inspecting the page, and going to the "console" tab. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Contribution Guidelines
|
||||
description: By submitting this issue, you agree to follow our [Contribution Guidelines](https://lavaforge.org/calckey/calckey/src/branch/develop/CONTRIBUTING.md)
|
||||
options:
|
||||
- label: I agree to follow this project's Contribution Guidelines
|
||||
required: true
|
|
@ -556,7 +556,6 @@ tokenRequested: "منح حق الوصول إلى الحساب"
|
|||
pluginTokenRequestedDescription: "ستتمكن الإضافة من استخدام هذه الأذونات."
|
||||
notificationType: "أنواع الإشعارات"
|
||||
edit: "التعديل"
|
||||
useStarForReactionFallback: "استخدم ★ كبديل إذا كان التفاعل مجهولًا"
|
||||
emailServer: "خادم البريد الإلكتروني"
|
||||
emailConfigInfo: "يستخدم لتأكيد عنوان بريدك الإلكتروني ولإعادة تعيين كلمة المرور إن نسيتها."
|
||||
email: "البريد الإلكتروني "
|
||||
|
|
|
@ -577,7 +577,6 @@ tokenRequested: "অ্যাকাউন্টে অ্যাক্সেস
|
|||
pluginTokenRequestedDescription: "এই প্লাগইনটি এখানে দেওয়া অনুমুতিসমূহ ব্যাবহার করবে"
|
||||
notificationType: "বিজ্ঞপ্তির ধরন"
|
||||
edit: "সম্পাদনা"
|
||||
useStarForReactionFallback: "রিঅ্যাকশনের ইমোজি না জানলে ★ ব্যবহার করুন"
|
||||
emailServer: "ইমেইল সার্ভার"
|
||||
enableEmail: "ইমেইল বিতরণ চালু করুন"
|
||||
emailConfigInfo: "আপনার ইমেল ঠিকানা নিশ্চিত করতে এবং আপনার পাসওয়ার্ড পুনরায় সেট করতে ব্যবহৃত হয়"
|
||||
|
|
|
@ -581,7 +581,6 @@ tokenRequested: "Zugriff zum Benutzerkonto gewähren"
|
|||
pluginTokenRequestedDescription: "Dieses Plugin wird die hier konfigurierten Berechtigungen verwenden können."
|
||||
notificationType: "Art der Benachrichtigung"
|
||||
edit: "Bearbeiten"
|
||||
useStarForReactionFallback: "Verwende ★ falls das Reaktions-Emoji unbekannt ist"
|
||||
emailServer: "Email-Server"
|
||||
enableEmail: "Email-Versand aktivieren"
|
||||
emailConfigInfo: "Zur Email-Bestätigung bei Registrierung oder zum Zurücksetzen des Passworts verwendet"
|
||||
|
|
408
locales/el-GR.yml
Normal file
408
locales/el-GR.yml
Normal file
|
@ -0,0 +1,408 @@
|
|||
---
|
||||
_lang_: "Ελληνικά"
|
||||
monthAndDay: "{μήνας}/{ημέρα}"
|
||||
search: "Αναζήτηση"
|
||||
notifications: "Ειδοποιήσεις"
|
||||
username: "Όνομα μέλους"
|
||||
password: "Κωδικός πρόσβασης"
|
||||
forgotPassword: "Ξέχασα τον κωδικό πρόσβασης"
|
||||
fetchingAsApObject: "Μαζεύοντας από το Fediverse..."
|
||||
ok: "Εντάξει"
|
||||
gotIt: "Τό'πιασα!"
|
||||
cancel: "Ακύρωση"
|
||||
enterUsername: "Εισάγετε το όνομα μέλους"
|
||||
renotedBy: "Κοινοποιήθηκε από {user}"
|
||||
noNotes: "Δεν υπάρχουν σημειώματα"
|
||||
noNotifications: "Δεν υπάρχουν ειδοποιήσεις"
|
||||
settings: "Ρυθμίσεις"
|
||||
basicSettings: "Βασικές ρυθμίσεις"
|
||||
otherSettings: "Άλλες ρυθμίσεις"
|
||||
openInWindow: "Άνοιγμα σε παράθυρο"
|
||||
profile: "Προφίλ"
|
||||
timeline: "Χρονολόγιο"
|
||||
noAccountDescription: "Αυτό το μέλος δεν έχει γράψει βιογραφικό ακόμη."
|
||||
login: "Σύνδεση"
|
||||
loggingIn: "Συνδέεστε"
|
||||
logout: "Αποσύνδεση"
|
||||
signup: "Δημιουργία λογαριασμού"
|
||||
uploading: "Ανέβασμα..."
|
||||
save: "Αποθήκευση"
|
||||
users: "Μέλη"
|
||||
addUser: "Προσθήκη μέλους"
|
||||
favorite: "Προσθήκη στα αγαπημένα"
|
||||
favorites: "Αγαπημένα"
|
||||
unfavorite: "Αφαίρεση από αγαπημένα"
|
||||
favorited: "Προστέθηκε στα αγαπημένα."
|
||||
alreadyFavorited: "Έχει ήδη προστεθεί στα αγαπημένα."
|
||||
cantFavorite: "Αδυναμία προσθήκης στα αγαπημένα."
|
||||
pin: "Καρφίτσωμα στο προφίλ"
|
||||
unpin: "Ξεκαρφίτσωμα από το προφίλ"
|
||||
copyContent: "Αντιγραφή περιεχομένων"
|
||||
copyLink: "Αντιγραφή συνδέσμου"
|
||||
delete: "Διαγραφή"
|
||||
deleteAndEdit: "Διαγραφή και επεξεργασία"
|
||||
deleteAndEditConfirm: "Σίγουρα θέλετε να διαγράψετε αυτό το σημείωμα και να το επεξεργαστείτε; Θα χάσετε όλες τις αντιδράσεις, κοινοποιήσεις και απαντήσεις σε αυτό."
|
||||
addToList: "Προσθήκη στη λίστα"
|
||||
sendMessage: "Αποστολή μηνύματος"
|
||||
copyUsername: "Αντιγραφή ονόματος μέλους"
|
||||
searchUser: "Αναζήτηση μέλους"
|
||||
reply: "Απάντηση"
|
||||
loadMore: "Φόρτωσε περισσότερα"
|
||||
showMore: "Δείξε περισσότερα"
|
||||
showLess: "Κλείσιμο"
|
||||
youGotNewFollower: "σε ακολούθησε"
|
||||
receiveFollowRequest: "Λάβατε αίτημα ακολούθησης"
|
||||
followRequestAccepted: "Το αίτημα ακολούθησης έγινε δεκτό"
|
||||
mention: "Επισήμανση"
|
||||
mentions: "Επισημάνσεις"
|
||||
directNotes: "Απευθείας σημειώματα"
|
||||
importAndExport: "Εισαγωγή / Εξαγωγή"
|
||||
import: "Εισαγωγή"
|
||||
export: "Εξαγωγή"
|
||||
files: "Αρχεία"
|
||||
download: "Λήψη"
|
||||
driveFileDeleteConfirm: "Θέλετε σίγουρα να διαγράψετε το αρχείο \"{name}\"; Τα σημειώματα με αυτό το συνημμένο αρχείο επίσης θα διαγραφούν."
|
||||
unfollowConfirm: "Θέλετε σίγουρα να σταματήσετε να ακολουθείτε το μέλος {name};"
|
||||
exportRequested: "Ζητήσατε μία εξαγωγή. Αυτό μπορεί να πάρει κάποιον χρόνο. Επίσης θα προστεθεί στον Δίσκο σας μόλις ολοκληρωθεί."
|
||||
importRequested: "Ζητήσατε μία εισαγωγή. Αυτό μπορεί να πάρει κάποιον χρόνο."
|
||||
lists: "Λίστες"
|
||||
noLists: "Δεν έχετε λίστες"
|
||||
note: "Σημείωμα"
|
||||
notes: "Σημειώματα"
|
||||
following: "Ακολουθεί"
|
||||
followers: "Ακολουθούν"
|
||||
followsYou: "Σε ακολουθεί"
|
||||
createList: "Δημιουργία λίστας"
|
||||
manageLists: "Διαχείριση λιστών"
|
||||
error: "Σφάλμα"
|
||||
somethingHappened: "Προέκυψε ένα σφάλμα"
|
||||
retry: "Προσπάθεια ξανά"
|
||||
pageLoadError: "Ένα σφάλμα προέκυψε φορτώνοντας τη σελίδα."
|
||||
pageLoadErrorDescription: "Αυτό κανονικά προκαλείται από σφάλματα δικτύου ή από την προσωρινή μνήμη του προγράμματος περιήγησης. Δοκιμάστε να σβήσετε την προσωρινή μνήμη (cache) και ξαναδοκιμάστε μετά από λίγο."
|
||||
serverIsDead: "Αυτός ο server δεν αποκρίνεται. Παρακαλώ περιμέντε λίγο και δοκιμάστε ξανά."
|
||||
youShouldUpgradeClient: "Για να δείτε αυτή τη σελίδα, παρακαλώ επαναφορτώστε για να ενημερωθεί το πρόγραμμα."
|
||||
enterListName: "Πληκτρολογήστε ένα όνομα για τη λίστα"
|
||||
privacy: "Ιδιωτικότητα"
|
||||
makeFollowManuallyApprove: "Τα αιτήματα ακολούθησης χρειάζονται έγκριση"
|
||||
defaultNoteVisibility: "Προεπιλεγμένη ορατότητα"
|
||||
follow: "Ακολουθήστε"
|
||||
followRequest: "Στείλτε αίτημα ακολούθησης"
|
||||
followRequests: "Αιτήματα ακολούθησης"
|
||||
unfollow: "Να μην ακολουθώ"
|
||||
followRequestPending: "Το αίτημα ακολούθησης εκκρεμεί"
|
||||
enterEmoji: "Εισάγετε ένα emoji"
|
||||
renote: "Κοινοποίηση σημειώματος"
|
||||
unrenote: "Ακύρωση κοινοποίησης"
|
||||
renoted: "Κοινοποιήθηκε."
|
||||
cantRenote: "Αυτή η δημοσίευση δεν μπορεί να κοινοποιηθεί."
|
||||
cantReRenote: "Μία κοινοποίηση δεν μπορεί να κοινοποιηθεί."
|
||||
quote: "Παράθεση"
|
||||
pinnedNote: "Καρφιτσωμένο σημείωμα"
|
||||
pinned: "Καρφίτσωμα στο προφίλ"
|
||||
you: "Εσύ"
|
||||
clickToShow: "Κάντε κλικ για εμφάνιση"
|
||||
add: "Προσθέστε"
|
||||
reaction: "Αντιδράσεις"
|
||||
reactionSetting: "Αντιδράσεις για εμφάνιση στην επιλογή αντίδρασης"
|
||||
reactionSettingDescription2: "Σύρετε για να αλλάξετε τη σειρά, κάντε κλικ για να διαγράψετε, πατήστε \"+\" για να προσθέσετε."
|
||||
rememberNoteVisibility: "Θυμήσου τις ρυθμίσεις ορατότητας σημειώματος"
|
||||
attachCancel: "Διαγραφή αρχείου"
|
||||
enterFileName: "Πληκτρολογήστε όνομα αρχείου"
|
||||
mute: "Σίγαση"
|
||||
unmute: "Άρση σίγασης"
|
||||
block: "Μπλοκάρισμα"
|
||||
unblock: "Άρση μπλοκαρίσματος"
|
||||
suspend: "Αποβολή"
|
||||
unsuspend: "Άρση αποβολής"
|
||||
blockConfirm: "Θέλετε σίγουρα να μπλοκάρετε αυτόν τον λογαριασμό;"
|
||||
unblockConfirm: "Θέλετε σίγουρα να ξεμπλοκάρετε αυτόν τον λογαριασμό;"
|
||||
suspendConfirm: "Θέλετε σίγουρα να αποβάλλετε αυτόν τον λογαριασμό;"
|
||||
unsuspendConfirm: "Θέλετε σίγουρα να άρετε την αποβολή αυτού του λογαριασμού;"
|
||||
selectList: "Επιλέξτε μία λίστα"
|
||||
selectAntenna: "Επιλέξτε μία αντένα"
|
||||
selectWidget: "Επιλέξτε ένα μαραφέτι"
|
||||
editWidgets: "Επεξεργασία μαραφετίων"
|
||||
editWidgetsExit: "Ολοκληρώθηκε"
|
||||
customEmojis: "Επιπλέον emoji"
|
||||
emojiName: "Όνομα emoji"
|
||||
addEmoji: "Προσθήκη emoji"
|
||||
settingGuide: "Συνιστώμενες ρυθμίσεις"
|
||||
flagAsBot: "Αυτός ο λογαριασμός είναι bot"
|
||||
flagAsCat: "Αυτός ο λογαριασμός είναι γάτα"
|
||||
flagShowTimelineReplies: "Εμφάνιση απαντήσεων στο χρονολόγιο"
|
||||
addAccount: "Προσθήκη λογαριασμού"
|
||||
general: "Γενικές"
|
||||
wallpaper: "Ταπετσαρία"
|
||||
setWallpaper: "Ορισμός ταπετσαρίας"
|
||||
removeWallpaper: "Διαγραφή ταπετσαρίας"
|
||||
searchWith: "Αναζήτηση: {q}"
|
||||
youHaveNoLists: "Δεν έχετε λίστες"
|
||||
followConfirm: "Θέλετε σίγουρα να ακολουθήσετε τον λογαριασμό {name};"
|
||||
host: "Φιλοξενεί"
|
||||
selectUser: "Επιλέξτε ένα μέλος"
|
||||
recipient: "Αποδέκτης-τρια"
|
||||
annotation: "Σχόλια"
|
||||
federation: "Ομοσπονδία"
|
||||
storageUsage: "Χρήση χώρου"
|
||||
version: "Έκδοση"
|
||||
metadata: "Μεταδεδομένα"
|
||||
network: "Δίκτυο"
|
||||
disk: "Δίσκος"
|
||||
instanceInfo: "Πληροφορίες του instance"
|
||||
statistics: "Στατιστικά"
|
||||
clearQueue: "Εκκαθάριση ουράς"
|
||||
clearQueueConfirmTitle: "Θέλετε να διαγράψετε την ουρά;"
|
||||
clearCachedFiles: "Εκκαθάριση προσωρινής μνήμης"
|
||||
done: "Ολοκληρώθηκε"
|
||||
attachFile: "Επισύναψη αρχείων"
|
||||
more: "Περισσότερα!"
|
||||
noSuchUser: "Το μέλος δεν βρέθηκε"
|
||||
announcements: "Ανακοινώσεις"
|
||||
imageUrl: "URL εικόνας"
|
||||
remove: "Διαγραφή"
|
||||
removed: "Η διαγραφή ολοκληρώθηκε επιτυχώς"
|
||||
saved: "Αποθηκεύτηκε"
|
||||
messaging: "Συνομιλία"
|
||||
upload: "Ανεβάστε"
|
||||
fromDrive: "Από τον Αποθηκευτικό Χώρο"
|
||||
fromUrl: "Από URL"
|
||||
uploadFromUrl: "Ανεβάστε από URL"
|
||||
explore: "Εξερευνήστε"
|
||||
messageRead: "Διαβάστηκε"
|
||||
startMessaging: "Ξεκινήστε μία συνομιλία"
|
||||
nUsersRead: "διαβάστηκε από {n}"
|
||||
tos: "Όροι χρήσης"
|
||||
start: "Ας αρχίσουμε"
|
||||
home: "Κεντρικό"
|
||||
activity: "Δραστηριότητα"
|
||||
images: "Εικόνες"
|
||||
birthday: "Γενέθλια"
|
||||
registeredDate: "Έγινε μέλος στις"
|
||||
location: "Τοποθεσία"
|
||||
theme: "Θέματα"
|
||||
light: "Ανοιχτόχρωμο"
|
||||
dark: "Σκούρο"
|
||||
drive: "Αποθηκευτικός Χώρος"
|
||||
fileName: "Όνομα αρχείου"
|
||||
selectFile: "Επιλέξτε ένα αρχείο"
|
||||
selectFiles: "Επιλέξτε αρχεία"
|
||||
selectFolder: "Επιλέξτε φάκελο"
|
||||
selectFolders: "Επιλέξτε φακέλους"
|
||||
renameFile: "Μετονομασία αρχείου"
|
||||
addFile: "Προσθήκη αρχείου"
|
||||
emptyDrive: "Ο Αποθηκευτικός Χώρος σας είναι άδειος"
|
||||
copyUrl: "Αντιγραφή URL"
|
||||
rename: "Αλλαγή ονόματος"
|
||||
avatar: "Εικονίδιο"
|
||||
banner: "Πανό"
|
||||
reload: "Ανανέωση"
|
||||
doNothing: "Αγνόηση"
|
||||
watch: "Παρακολούθηση"
|
||||
unwatch: "Τέλος παρακολούθησης"
|
||||
accept: "Αποδοχή"
|
||||
reject: "Απόρριψη"
|
||||
normal: "Κανονικό"
|
||||
instanceName: "Όνομα instance"
|
||||
thisYear: "Έτος"
|
||||
thisMonth: "Μήνας"
|
||||
today: "Σήμερα"
|
||||
dayX: "{day}"
|
||||
pages: "Σελίδες"
|
||||
connectService: "Σύνδεση"
|
||||
disconnectService: "Αποσύνδεση"
|
||||
registration: "Εγγραφή"
|
||||
pinnedPages: "Καρφιτσωμένες Σελίδες"
|
||||
pinnedNotes: "Καρφιτσωμένα σημειώματα"
|
||||
antennas: "Αντένες"
|
||||
manageAntennas: "Διαχείριση αντενών"
|
||||
name: "Όνομα"
|
||||
antennaSource: "Πηγή αντένας"
|
||||
antennaKeywords: "Λέξεις-κλειδιά για παρακολούθηση"
|
||||
antennaExcludeKeywords: "Λέξεις-κλειδιά για αποκλεισμό"
|
||||
notifyAntenna: "Ειδοποίηση για νέα σημειώματα"
|
||||
withFileAntenna: "Μόνο σημειώματα με αρχεία"
|
||||
caseSensitive: "Διάκριση Πεζών-Κεφαλαίων"
|
||||
popularTags: "Δημοφιλείς ετικέτες"
|
||||
userList: "Λίστες"
|
||||
about: "Πληροφορίες"
|
||||
moderator: "Συντονιστής"
|
||||
moderation: "Συντονισμός"
|
||||
cacheClear: "Εκκαθάριση προσωρινής μνήμης"
|
||||
markAsReadAllNotifications: "Όλες οι ειδοποιήσεις διαβάστηκαν"
|
||||
group: "Ομάδα"
|
||||
groups: "Ομάδες"
|
||||
createGroup: "Δημιουργία ομάδας"
|
||||
ownedGroups: "Οι ομάδες σας"
|
||||
groupName: "Όνομα ομάδας"
|
||||
members: "Μέλη"
|
||||
transfer: "Μεταφορά"
|
||||
messagingWithUser: "Ιδιωτική συνομιλία"
|
||||
messagingWithGroup: "Ομαδική συνομιλία"
|
||||
title: "Τίτλος"
|
||||
text: "Κείμενο"
|
||||
enable: "Ενεργοποίηση"
|
||||
next: "Επόμενο"
|
||||
noteOf: "Σημείωμα από {user}"
|
||||
inviteToGroup: "Πρόσκληση στην ομάδα"
|
||||
quoteAttached: "Παράθεση"
|
||||
signinRequired: "Παρακαλούμε δημιουργήστε λογαριασμό ή συνδεθείτε πριν συνεχίσετε"
|
||||
category: "Κατηγορία"
|
||||
tags: "Ετικέτες"
|
||||
createAccount: "Δημιουργία λογαριασμού"
|
||||
local: "Τοπικό"
|
||||
remote: "Απομακρυσμένo"
|
||||
total: "Σύνολο"
|
||||
appearance: "Εμφάνιση"
|
||||
accountSettings: "Ρυθμίσεις λογαριασμού"
|
||||
sounds: "Ήχοι"
|
||||
sound: "Ήχοι"
|
||||
listen: "Ακρόαση"
|
||||
showInPage: "Εμφάνιση στη σελίδα"
|
||||
volume: "Ένταση"
|
||||
masterVolume: "Κύρια ένταση"
|
||||
details: "Λεπτομέρειες"
|
||||
install: "Εγκατάσταση"
|
||||
uninstall: "Κατάργηση εγκατάστασης"
|
||||
manage: "Διαχείριση"
|
||||
smtpHost: "Φιλοξενεί"
|
||||
smtpUser: "Όνομα μέλους"
|
||||
smtpPass: "Κωδικός πρόσβασης"
|
||||
notificationSetting: "Ρυθμίσεις ειδοποιήσεων"
|
||||
notificationSettingDesc: "Επιλέξτε τους τύπους ειδοποιήσεων που εμφανίζονται"
|
||||
switchUi: "Αλλαγή UI"
|
||||
clip: "Κλιπ"
|
||||
driveFilesCount: "Αριθμός αρχείων Αποθηκευτικού Χώρου"
|
||||
driveUsage: "Χρήση Αποθηκευτικού Χώρου"
|
||||
noteFavoritesCount: "Αριθμός αγαπημένων σημειωμάτων"
|
||||
clips: "Κλιπ"
|
||||
clearCache: "Εκκαθάριση προσωρινής μνήμης"
|
||||
emailNotification: "Ειδοποιήσεις μέσω mail"
|
||||
inChannelSearch: "Αναζήτηση στο κανάλι"
|
||||
info: "Πληροφορίες"
|
||||
notRecommended: "Δεν προτείνεται"
|
||||
switchAccount: "Αλλαγή λογαριασμού"
|
||||
user: "Μέλη"
|
||||
administration: "Διαχείριση"
|
||||
switch: "Εναλλαγή"
|
||||
gallery: "Γκαλερί"
|
||||
global: "Παγκόσμιο"
|
||||
searchResult: "Αποτελέσματα αναζήτησης"
|
||||
learnMore: "Μάθετε περισσότερα"
|
||||
controlPanel: "Πίνακας ελέγχου"
|
||||
manageAccounts: "Διαχείριση Λογαριασμών"
|
||||
searchByGoogle: "Αναζήτηση"
|
||||
file: "Αρχεία"
|
||||
recommended: "Προτεινόμενα"
|
||||
cannotUploadBecauseNoFreeSpace: "Το ανέβασμα απέτυχε λόγω ανεπαρκούς Αποθηκευτικού Χώρου"
|
||||
_email:
|
||||
_follow:
|
||||
title: "Έχετε ένα νέο ακόλουθο"
|
||||
_mfm:
|
||||
mention: "Επισήμανση"
|
||||
quote: "Παράθεση"
|
||||
emoji: "Επιπλέον emoji"
|
||||
search: "Αναζήτηση"
|
||||
_channel:
|
||||
featured: "Δημοφιλή"
|
||||
_theme:
|
||||
keys:
|
||||
panel: "Πίνακας"
|
||||
mention: "Επισήμανση"
|
||||
renote: "Κοινοποίηση σημειώματος"
|
||||
_sfx:
|
||||
note: "Σημειώματα"
|
||||
notification: "Ειδοποιήσεις"
|
||||
chat: "Συνομιλία"
|
||||
chatBg: "Συνομιλία (Παρασκήνιο)"
|
||||
antenna: "Αντένες"
|
||||
channel: "Ειδοποιήσεις καναλιών"
|
||||
_ago:
|
||||
future: "Μελλοντικό"
|
||||
justNow: "Μόλις τώρα"
|
||||
secondsAgo: "{n} δευτερόλεπτο(α) πριν"
|
||||
minutesAgo: "{n} λεπτό(ά) πριν"
|
||||
hoursAgo: "{n} ώρα(ες) πριν"
|
||||
daysAgo: "{n} μέρα(ες) πριν"
|
||||
weeksAgo: "{n} εβδομάδα(ες) πριν"
|
||||
monthsAgo: "{n} μήνα(ες) πριν"
|
||||
yearsAgo: "{n} έτος(η) πριν"
|
||||
_permissions:
|
||||
"write:drive": "Επεξεργαστείτε ή διαγράψτε τα αρχεία και τους φακέλους του Αποθηκευτικού Χώρου σας"
|
||||
"read:favorites": "Δείτε τη λίστα των αγαπημένων σας"
|
||||
"write:favorites": "Επεξεργαστείτε τη λίστα των αγαπημένων σας"
|
||||
"read:messaging": "Δείτε τις συνομιλίες σας"
|
||||
"write:messaging": "Γράψτε ή διαγράψτε μηνύματα συνομιλίας"
|
||||
"read:notifications": "Δείτε τις ειδοποιήσεις σας"
|
||||
"write:notifications": "Διαχειριστείτε τις ειδοποιήσεις σας"
|
||||
"read:pages": "Δείτε τις Σελίδες σας"
|
||||
"write:pages": "Επεξεργαστείτε ή διαγράψτε τις σελίδες σας"
|
||||
_antennaSources:
|
||||
all: "Όλα τα σημειώματα"
|
||||
homeTimeline: "Σημειώματα από μέλη που ακολουθείτε"
|
||||
users: "Σημειώματα από συγκεκριμένα μέλη"
|
||||
userList: "Σημειώματα από καθορισμένη λίστα μελών"
|
||||
userGroup: "Σημειώματα από μέλη καθορισμένης ομάδας"
|
||||
_widgets:
|
||||
profile: "Προφίλ"
|
||||
instanceInfo: "Πληροφορίες του instance"
|
||||
notifications: "Ειδοποιήσεις"
|
||||
timeline: "Χρονολόγιο"
|
||||
calendar: "Ημερολόγιο"
|
||||
trends: "Δημοφιλή"
|
||||
clock: "Ρολόι"
|
||||
activity: "Δραστηριότητα"
|
||||
photos: "Φωτογραφίες"
|
||||
digitalClock: "Ψηφιακό ρολόι"
|
||||
federation: "Ομοσπονδία"
|
||||
postForm: "Φόρμα δημοσίευσης"
|
||||
button: "Κουμπί"
|
||||
onlineUsers: "Συνδεδεμένα μέλη"
|
||||
_userList:
|
||||
chooseList: "Επιλέξτε μία λίστα"
|
||||
_cw:
|
||||
show: "Δείτε περισσότερα"
|
||||
_visibility:
|
||||
home: "Κεντρικό"
|
||||
homeDescription: "Δημοσίευση στο κεντρικό χρονολόγιο μόνο"
|
||||
followers: "Ακολουθούν"
|
||||
_profile:
|
||||
name: "Όνομα"
|
||||
username: "Όνομα μέλους"
|
||||
_exportOrImport:
|
||||
allNotes: "Όλα τα σημειώματα"
|
||||
followingList: "Ακολουθεί"
|
||||
muteList: "Μέλη σε σίγαση"
|
||||
blockingList: "Μπλοκαρισμένα μέλη"
|
||||
userLists: "Λίστες"
|
||||
_charts:
|
||||
federation: "Ομοσπονδία"
|
||||
_timelines:
|
||||
home: "Κεντρικό"
|
||||
local: "Τοπικό"
|
||||
social: "Κοινωνικό"
|
||||
global: "Παγκόσμιο"
|
||||
_pages:
|
||||
viewPage: "Δείτε τις Σελίδες σας"
|
||||
blocks:
|
||||
image: "Εικόνες"
|
||||
_notification:
|
||||
youGotMessagingMessageFromUser: "{name} σάς έστειλε ένα μήνυμα συνομιλίας"
|
||||
youWereFollowed: "σε ακολούθησε"
|
||||
_types:
|
||||
follow: "Νέοι ακόλουθοι"
|
||||
mention: "Επισήμανση"
|
||||
renote: "Κοινοποίηση σημειώματος"
|
||||
quote: "Παράθεση"
|
||||
reaction: "Αντιδράσεις"
|
||||
_actions:
|
||||
reply: "Απάντηση"
|
||||
renote: "Κοινοποίηση σημειώματος"
|
||||
_deck:
|
||||
widgetsIntroduction: "Παρακαλούμε επιλέξτε \"Επεξεργασία μαραφετίων\" στο μενού και προσθέστε μαραφέτι."
|
||||
_columns:
|
||||
widgets: "Μαραφέτια"
|
||||
notifications: "Ειδοποιήσεις"
|
||||
tl: "Χρονολόγιο"
|
||||
antenna: "Αντένες"
|
||||
list: "Λίστα"
|
||||
mentions: "Επισημάνσεις"
|
|
@ -583,7 +583,6 @@ tokenRequested: "Grant access to account"
|
|||
pluginTokenRequestedDescription: "This plugin will be able to use the permissions set here."
|
||||
notificationType: "Notification type"
|
||||
edit: "Edit"
|
||||
useStarForReactionFallback: "Use ★ as fallback if the reaction emoji is unknown"
|
||||
emailServer: "Email server"
|
||||
enableEmail: "Enable email distribution"
|
||||
emailConfigInfo: "Used to confirm your email during sign-up or if you forget your password"
|
||||
|
@ -644,7 +643,7 @@ instanceTicker: "Instance information of posts"
|
|||
waitingFor: "Waiting for {x}"
|
||||
random: "Random"
|
||||
system: "System"
|
||||
switchUi: "Switch UI"
|
||||
switchUi: "Layout"
|
||||
desktop: "Desktop"
|
||||
clip: "Clip"
|
||||
createNew: "Create new"
|
||||
|
@ -930,6 +929,7 @@ moveFrom: "Move to this account from an older account"
|
|||
moveFromLabel: "Account you're moving from:"
|
||||
moveFromDescription: "This will set an alias of your old account so that you can move from that account to this current one. Do this BEFORE moving from your older account. Please enter the tag of the account formatted like @person@instance.com"
|
||||
migrationConfirm: "Are you absolutely sure you want to migrate your acccount to {account}? Once you do this, you won't be able to reverse it, and you won't be able to use your account normally again.\nAlso, please ensure that you've set this current account as the account you're moving from."
|
||||
defaultReaction: "Default emoji reaction for outgoing and incoming posts"
|
||||
|
||||
_sensitiveMediaDetection:
|
||||
description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server."
|
||||
|
@ -1329,7 +1329,9 @@ _widgets:
|
|||
jobQueue: "Job Queue"
|
||||
serverMetric: "Server metrics"
|
||||
aiscript: "AiScript console"
|
||||
aichan: "Ai"
|
||||
userList: "User list"
|
||||
_userList:
|
||||
chooseList: "Select a list"
|
||||
_cw:
|
||||
hide: "Hide"
|
||||
show: "Show content"
|
||||
|
|
|
@ -580,7 +580,6 @@ tokenRequested: "Permiso de acceso a la cuenta"
|
|||
pluginTokenRequestedDescription: "Este plugin podrá usar los permisos descritos aquí"
|
||||
notificationType: "Tipo de notificación"
|
||||
edit: "Editar"
|
||||
useStarForReactionFallback: "En caso de que los emojis de reacciones no sean claros, usar en su lugar una estrella"
|
||||
emailServer: "Servidor de correo"
|
||||
enableEmail: "Activar el envío de correos electrónicos"
|
||||
emailConfigInfo: "Usar en caso de validación de correo electrónico y pedido de contraseña"
|
||||
|
|
|
@ -567,14 +567,13 @@ large: "Grand"
|
|||
medium: "Moyen"
|
||||
small: "Petit"
|
||||
generateAccessToken: "Générer un jeton d'accès"
|
||||
permission: "Autorisations "
|
||||
permission: "Autorisations"
|
||||
enableAll: "Tout activer"
|
||||
disableAll: "Tout désactiver"
|
||||
tokenRequested: "Autoriser l'accès au compte"
|
||||
pluginTokenRequestedDescription: "Ce plugin pourra utiliser les autorisations définies ici."
|
||||
notificationType: "Type de notifications"
|
||||
edit: "Editer"
|
||||
useStarForReactionFallback: "Utiliser ★ comme alternative si l’émoji de réaction est inconnu"
|
||||
emailServer: "Serveur mail"
|
||||
enableEmail: "Activer la distribution de courriel"
|
||||
emailConfigInfo: "Utilisé pour confirmer votre adresse de courriel et la réinitialisation de votre mot de passe en cas d’oubli."
|
||||
|
|
|
@ -577,7 +577,6 @@ tokenRequested: "Berikan ijin akses ke akun"
|
|||
pluginTokenRequestedDescription: "Plugin ini dapat menggunakan setelan ijin disini."
|
||||
notificationType: "Jenis pemberitahuan"
|
||||
edit: "Sunting"
|
||||
useStarForReactionFallback: "Gunakan ★ sebagai fallback jika reaksi emoji tidak diketahui"
|
||||
emailServer: "Peladen surel"
|
||||
enableEmail: "Nyalakan distribusi surel"
|
||||
emailConfigInfo: "Digunakan untuk mengonfirmasi surel kamu disaat mendaftar dan lupa kata sandi"
|
||||
|
|
|
@ -573,7 +573,6 @@ tokenRequested: "Autorizza accesso all'account"
|
|||
pluginTokenRequestedDescription: "Il plugin potrà utilizzare le autorizzazioni impostate qui."
|
||||
notificationType: "Tipo di notifiche"
|
||||
edit: "Modifica"
|
||||
useStarForReactionFallback: "Se è sconosciuto l'emoji di reazione, usare la ★ come alternativa."
|
||||
emailServer: "Server email"
|
||||
enableEmail: "Abilita consegna email"
|
||||
emailConfigInfo: "Utilizzato per verificare il tuo indirizzo di posta elettronica e per reimpostare la tua password"
|
||||
|
|
|
@ -583,7 +583,6 @@ tokenRequested: "アカウントへのアクセス許可"
|
|||
pluginTokenRequestedDescription: "このプラグインはここで設定した権限を行使できるようになります。"
|
||||
notificationType: "通知の種類"
|
||||
edit: "編集"
|
||||
useStarForReactionFallback: "リアクション絵文字が不明な場合、代わりに★を使う"
|
||||
emailServer: "メールサーバー"
|
||||
enableEmail: "メール配信機能を有効化する"
|
||||
emailConfigInfo: "メールアドレスの確認やパスワードリセットの際に使います"
|
||||
|
|
|
@ -579,7 +579,6 @@ tokenRequested: "アカウントへのアクセス許可"
|
|||
pluginTokenRequestedDescription: "このプラグインはここで設定した権限を使えるようになるで。"
|
||||
notificationType: "通知の種類"
|
||||
edit: "編集"
|
||||
useStarForReactionFallback: "リアクションがようわからん場合、★を使う"
|
||||
emailServer: "メールサーバー"
|
||||
enableEmail: "メール配信を受け取る"
|
||||
emailConfigInfo: "メールアドレスの確認とかパスワードリセットの時に使うで"
|
||||
|
|
|
@ -580,7 +580,6 @@ tokenRequested: "계정 접근 허용"
|
|||
pluginTokenRequestedDescription: "이 플러그인은 여기서 설정한 권한을 사용할 수 있게 됩니다."
|
||||
notificationType: "알림 유형"
|
||||
edit: "편집"
|
||||
useStarForReactionFallback: "알 수 없는 리액션 이모지 대신 ★ 사용"
|
||||
emailServer: "메일 서버"
|
||||
enableEmail: "이메일 송신 기능 활성화"
|
||||
emailConfigInfo: "가입 시 메일 주소 확인이나 비밀번호 초기화 시에 사용합니다."
|
||||
|
|
|
@ -572,7 +572,6 @@ tokenRequested: "Przydziel dostęp do konta"
|
|||
pluginTokenRequestedDescription: "Ta wtyczka będzie mogła korzystać z ustawionych tu uprawnień."
|
||||
notificationType: "Rodzaj powiadomień"
|
||||
edit: "Edytuj"
|
||||
useStarForReactionFallback: "Użyj ★ jako zapasowego emoji, gdy emoji reakcji jest nieznane"
|
||||
emailServer: "Serwer poczty e-mail"
|
||||
enableEmail: "Włącz dostarczanie wiadomości e-mail"
|
||||
emailConfigInfo: "Wykorzystywany do potwierdzenia adresu e-mail w trakcie rejestracji, lub gdy zapomnisz hasła"
|
||||
|
|
|
@ -576,7 +576,6 @@ tokenRequested: "Acordă acces la cont"
|
|||
pluginTokenRequestedDescription: "Acest plugin va putea să folosească permisiunile setate aici."
|
||||
notificationType: "Tipul notificării"
|
||||
edit: "Editează"
|
||||
useStarForReactionFallback: "Folosește ★ ca fallback dacă emoji-ul este necunoscut"
|
||||
emailServer: "Server email"
|
||||
enableEmail: "Activează distribuția de emailuri"
|
||||
emailConfigInfo: "Folosit pentru a confirma emailul tău în timpul logări dacă îți uiți parola"
|
||||
|
|
|
@ -580,7 +580,6 @@ tokenRequested: "Открыть доступ к учётной записи"
|
|||
pluginTokenRequestedDescription: "Это расширение сможет пользоваться разрешениями, установленными здесь."
|
||||
notificationType: "Тип уведомления"
|
||||
edit: "Изменить"
|
||||
useStarForReactionFallback: "Ставить ★ в качестве реакции вместо неизвестного эмодзи"
|
||||
emailServer: "Сервер электронной почты"
|
||||
enableEmail: "Включить обмен электронной почтой"
|
||||
emailConfigInfo: "Используется для подтверждения адреса электронной почты и сброса пароля."
|
||||
|
|
|
@ -579,7 +579,6 @@ tokenRequested: "Povoliť prístup k účtu"
|
|||
pluginTokenRequestedDescription: "Tento plugin bude môcť používať oprávnenia nastavené tu."
|
||||
notificationType: "Typ oznámenia"
|
||||
edit: "Upraviť"
|
||||
useStarForReactionFallback: "Použiť ★ keď emoji reakcie nie je známe"
|
||||
emailServer: "Email server"
|
||||
enableEmail: "Zapnúť email"
|
||||
emailConfigInfo: "Používa sa na overenie emaily pri registrácii alebo pri zabudnutí hesla"
|
||||
|
|
|
@ -580,7 +580,6 @@ tokenRequested: "ให้สิทธิ์การเข้าถึงบั
|
|||
pluginTokenRequestedDescription: "ปลั๊กอินนี้จะสามารถใช้การอนุญาตที่ตั้งค่าไว้ที่นี่นะ"
|
||||
notificationType: "ประเภทการแจ้งเตือน"
|
||||
edit: "แก้ไข"
|
||||
useStarForReactionFallback: "ใช้ ★ เป็นทางเลือกแทนถ้าหากไม่ทราบอิโมจิ"
|
||||
emailServer: "อีเมล์เซิร์ฟเวอร์"
|
||||
enableEmail: "เปิดใช้งานการกระจายอีเมล"
|
||||
emailConfigInfo: "ใช้เพื่อยืนยันอีเมลของคุณระหว่างการสมัครหรือถ้าหากคุณลืมรหัสผ่าน"
|
||||
|
|
|
@ -577,7 +577,6 @@ tokenRequested: "Надати доступ до акаунту"
|
|||
pluginTokenRequestedDescription: "Цей плагін зможе використовувати дозволи які тут вказані."
|
||||
notificationType: "Тип сповіщення"
|
||||
edit: "Редагувати"
|
||||
useStarForReactionFallback: "Використовувати ★ як запасний варіант, якщо емодзі реакції невідомий"
|
||||
emailServer: "Сервер електронної пошти"
|
||||
enableEmail: "Увімкнути функцію доставки пошти"
|
||||
emailConfigInfo: "Використовується для підтвердження електронної пошти підчас реєстрації, а також для відновлення паролю."
|
||||
|
|
|
@ -580,7 +580,6 @@ tokenRequested: "Cấp quyền truy cập vào tài khoản"
|
|||
pluginTokenRequestedDescription: "Plugin này sẽ có thể sử dụng các quyền được đặt ở đây."
|
||||
notificationType: "Loại thông báo"
|
||||
edit: "Sửa"
|
||||
useStarForReactionFallback: "Dùng ★ nếu emoji biểu cảm không có"
|
||||
emailServer: "Email máy chủ"
|
||||
enableEmail: "Bật phân phối email"
|
||||
emailConfigInfo: "Được dùng để xác minh email của bạn lúc đăng ký hoặc nếu bạn quên mật khẩu của mình"
|
||||
|
|
|
@ -580,7 +580,6 @@ tokenRequested: "允许访问账户"
|
|||
pluginTokenRequestedDescription: "此插件将能够拥有此处设置的权限"
|
||||
notificationType: "通知类型"
|
||||
edit: "编辑"
|
||||
useStarForReactionFallback: "如果回应的是未知表情符号,则使用★作为代替"
|
||||
emailServer: "邮件服务器"
|
||||
enableEmail: "启用发送邮件功能"
|
||||
emailConfigInfo: "用于确认电子邮件和密码重置"
|
||||
|
|
|
@ -580,7 +580,6 @@ tokenRequested: "允許存取帳戶"
|
|||
pluginTokenRequestedDescription: "此外掛將擁有在此設定的權限。"
|
||||
notificationType: "通知形式"
|
||||
edit: "編輯"
|
||||
useStarForReactionFallback: "以★代替未知的表情符號"
|
||||
emailServer: "電郵伺服器"
|
||||
enableEmail: "啟用發送電郵功能"
|
||||
emailConfigInfo: "用於確認電郵地址及密碼重置"
|
||||
|
|
51
package.json
51
package.json
|
@ -1,49 +1,45 @@
|
|||
{
|
||||
"name": "calckey",
|
||||
"version": "13.0.6-rc",
|
||||
"version": "13.0.8-rc4",
|
||||
"codename": "aqua",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://codeberg.org/calckey/calckey.git"
|
||||
"url": "https://lavaforge.org/calckey/calckey.git"
|
||||
},
|
||||
"packageManager": "yarn@3.3.1",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"packageManager": "pnpm@7.25.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"rebuild": "yarn clean && yarn workspaces foreach run build && yarn run gulp",
|
||||
"build": "yarn workspaces foreach run build && yarn run gulp",
|
||||
"start": "yarn workspace backend run start",
|
||||
"start:test": "yarn workspace backend run start:test",
|
||||
"init": "yarn migrate",
|
||||
"migrate": "yarn workspace backend run migrate",
|
||||
"revertmigration": "yarn workspace backend run revertmigration",
|
||||
"migrateandstart": "yarn migrate && yarn start",
|
||||
"rebuild": "pnpm run clean && pnpm -r run build && pnpm run gulp",
|
||||
"build": "pnpm -r run build && pnpm run gulp",
|
||||
"start": "pnpm --filter backend run start",
|
||||
"start:test": "pnpm --filter backend run start:test",
|
||||
"init": "pnpm run migrate",
|
||||
"migrate": "pnpm --filter backend run migrate",
|
||||
"revertmigration": "pnpm --filter backend run revertmigration",
|
||||
"migrateandstart": "pnpm run migrate && pnpm run start",
|
||||
"gulp": "gulp build",
|
||||
"watch": "yarn dev",
|
||||
"dev": "yarn node ./scripts/dev.js",
|
||||
"lint": "yarn workspaces foreach run lint",
|
||||
"watch": "pnpm run dev",
|
||||
"dev": "pnpm node ./scripts/dev.js",
|
||||
"lint": "pnpm -r run lint",
|
||||
"cy:open": "cypress open --browser --e2e --config-file=cypress.config.ts",
|
||||
"cy:run": "cypress run",
|
||||
"e2e": "start-server-and-test start:test http://localhost:61812 cy:run",
|
||||
"mocha": "yarn workspace backend run mocha",
|
||||
"test": "yarn mocha",
|
||||
"mocha": "pnpm --filter backend run mocha",
|
||||
"test": "pnpm run mocha",
|
||||
"format": "gulp format",
|
||||
"clean": "yarn node ./scripts/clean.js",
|
||||
"clean-all": "yarn node ./scripts/clean-all.js",
|
||||
"cleanall": "yarn clean-all"
|
||||
"clean": "pnpm node ./scripts/clean.js",
|
||||
"clean-all": "pnpm node ./scripts/clean-all.js",
|
||||
"cleanall": "pnpm run clean-all"
|
||||
},
|
||||
"resolutions": {
|
||||
"chokidar": "^3.3.1",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bull-board/api": "^4.6.4",
|
||||
"@bull-board/ui": "^4.6.4",
|
||||
"@bull-board/api": "^4.10.2",
|
||||
"@bull-board/ui": "^4.10.2",
|
||||
"@tensorflow/tfjs": "^3.21.0",
|
||||
"calckey-js": "^0.0.20",
|
||||
"eslint": "^8.31.0",
|
||||
"execa": "5.1.1",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-cssnano": "2.1.3",
|
||||
|
@ -58,12 +54,11 @@
|
|||
"devDependencies": {
|
||||
"@types/gulp": "4.0.10",
|
||||
"@types/gulp-rename": "2.0.1",
|
||||
"@typescript-eslint/parser": "5.46.1",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "10.11.0",
|
||||
"install-peers": "^1.0.4",
|
||||
"rome": "^11.0.0",
|
||||
"start-server-and-test": "1.15.2",
|
||||
"typescript": "4.9.4",
|
||||
"vue-eslint-parser": "^9.1.0"
|
||||
"typescript": "4.9.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
node_modules
|
||||
/built
|
||||
/.eslintrc.js
|
||||
/@types/**/*
|
|
@ -1,32 +0,0 @@
|
|||
module.exports = {
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ['./tsconfig.json'],
|
||||
},
|
||||
extends: [
|
||||
'../shared/.eslintrc.js',
|
||||
],
|
||||
rules: {
|
||||
'import/order': ['warn', {
|
||||
'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
|
||||
'pathGroups': [
|
||||
{
|
||||
'pattern': '@/**',
|
||||
'group': 'external',
|
||||
'position': 'after'
|
||||
}
|
||||
],
|
||||
}],
|
||||
'no-restricted-globals': [
|
||||
'error',
|
||||
{
|
||||
'name': '__dirname',
|
||||
'message': 'Not in ESModule. Use `import.meta.url` instead.'
|
||||
},
|
||||
{
|
||||
'name': '__filename',
|
||||
'message': 'Not in ESModule. Use `import.meta.url` instead.'
|
||||
}
|
||||
]
|
||||
},
|
||||
};
|
25
packages/backend/.swcrc
Normal file
25
packages/backend/.swcrc
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/swcrc",
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"dynamicImport": true,
|
||||
"decorators": true
|
||||
},
|
||||
"transform": {
|
||||
"legacyDecorator": true,
|
||||
"decoratorMetadata": true
|
||||
},
|
||||
"experimental": {
|
||||
"keepImportAssertions": true
|
||||
},
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
"target": "es2022"
|
||||
},
|
||||
"minify": false
|
||||
}
|
12
packages/backend/migration/1672882664294-DefaultReaction.js
Normal file
12
packages/backend/migration/1672882664294-DefaultReaction.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
export class DefaultReaction1672882664294 {
|
||||
name = 'DefaultReaction1672882664294'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "defaultReaction" character varying(256) NOT NULL DEFAULT '⭐'`);
|
||||
await queryRunner.query(`COMMENT ON COLUMN "meta"."defaultReaction" IS 'The fallback reaction for emoji reacts'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "defaultReaction"`);
|
||||
}
|
||||
}
|
11
packages/backend/migration/1673336077243-PollChoiceLength.js
Normal file
11
packages/backend/migration/1673336077243-PollChoiceLength.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
export class PollChoiceLength1673336077243 {
|
||||
name = 'PollChoiceLength1673336077243'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "poll" ALTER COLUMN "choices" TYPE character varying(256) array`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "poll" ALTER COLUMN "choices" TYPE character varying(128) array`);
|
||||
}
|
||||
}
|
|
@ -4,21 +4,22 @@
|
|||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "yarn node ./built/index.js",
|
||||
"start:test": "NODE_ENV=test yarn node ./built/index.js",
|
||||
"start": "pnpm node ./built/index.js",
|
||||
"start:test": "NODE_ENV=test pnpm node ./built/index.js",
|
||||
"migrate": "typeorm migration:run -d ormconfig.js",
|
||||
"revertmigration": "typeorm migration:revert -d ormconfig.js",
|
||||
"build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json",
|
||||
"watch": "yarn node watch.mjs",
|
||||
"lint": "eslint --quiet \"src/**/*.ts\"",
|
||||
"build": "pnpm swc src -d built -D",
|
||||
"watch": "pnpm swc src -d built -D -w",
|
||||
"lint": "pnpm rome check \"src/**/*.ts\"",
|
||||
"mocha": "cross-env NODE_ENV=test TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha",
|
||||
"test": "yarn run mocha"
|
||||
"test": "pnpm run mocha"
|
||||
},
|
||||
"resolutions": {
|
||||
"chokidar": "^3.3.1",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-android-arm64": "1.3.11",
|
||||
"@tensorflow/tfjs-node": "3.21.1"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -29,9 +30,12 @@
|
|||
"@elastic/elasticsearch": "7.17.0",
|
||||
"@koa/cors": "3.4.3",
|
||||
"@koa/multer": "3.0.0",
|
||||
"@koa/router": "9.4.0",
|
||||
"@koa/router": "9.0.1",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@redocly/openapi-core": "1.0.0-beta.120",
|
||||
"@sinonjs/fake-timers": "9.1.2",
|
||||
"@swc/cli": "^0.1.59",
|
||||
"@swc/core": "^1.3.26",
|
||||
"@syuilo/aiscript": "0.11.1",
|
||||
"@tensorflow/tfjs": "^4.2.0",
|
||||
"ajv": "8.11.2",
|
||||
|
@ -59,7 +63,7 @@
|
|||
"fluent-ffmpeg": "2.1.2",
|
||||
"got": "12.5.3",
|
||||
"hpagent": "0.1.2",
|
||||
"ioredis": "4.28.5",
|
||||
"ioredis": "5.2.4",
|
||||
"ip-cidr": "3.0.11",
|
||||
"is-svg": "4.3.2",
|
||||
"js-yaml": "4.1.0",
|
||||
|
@ -77,7 +81,7 @@
|
|||
"koa-send": "5.0.1",
|
||||
"koa-slow": "2.1.0",
|
||||
"koa-views": "7.0.2",
|
||||
"mfm-js": "0.23.0",
|
||||
"mfm-js": "0.23.2",
|
||||
"mime-types": "2.1.35",
|
||||
"mocha": "10.2.0",
|
||||
"multer": "1.4.4-lts.1",
|
||||
|
@ -106,6 +110,7 @@
|
|||
"rss-parser": "3.12.0",
|
||||
"s-age": "1.1.2",
|
||||
"sanitize-html": "2.8.1",
|
||||
"seedrandom": "^3.0.5",
|
||||
"semver": "7.3.8",
|
||||
"sharp": "0.31.3",
|
||||
"speakeasy": "2.0.0",
|
||||
|
@ -119,7 +124,6 @@
|
|||
"tmp": "0.2.1",
|
||||
"ts-loader": "9.4.2",
|
||||
"ts-node": "10.9.1",
|
||||
"tsc-alias": "1.8.2",
|
||||
"tsconfig-paths": "4.1.2",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typeorm": "0.3.11",
|
||||
|
@ -132,7 +136,6 @@
|
|||
"xev": "3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@redocly/openapi-core": "1.0.0-beta.119",
|
||||
"@types/bcryptjs": "2.4.2",
|
||||
"@types/bull": "3.15.9",
|
||||
"@types/cbor": "6.0.0",
|
||||
|
@ -176,12 +179,11 @@
|
|||
"@types/web-push": "3.3.2",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.3",
|
||||
"@typescript-eslint/eslint-plugin": "5.46.1",
|
||||
"@typescript-eslint/parser": "5.46.1",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.31.0",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"eslint": "^8.31.0",
|
||||
"execa": "6.1.0",
|
||||
"typescript": "4.9.4"
|
||||
"swc-loader": "^0.2.3",
|
||||
"typescript": "4.9.4",
|
||||
"webpack": "^5.75.0"
|
||||
}
|
||||
}
|
||||
|
|
9
packages/backend/src/@types/hcaptcha.d.ts
vendored
9
packages/backend/src/@types/hcaptcha.d.ts
vendored
|
@ -1,11 +1,14 @@
|
|||
declare module 'hcaptcha' {
|
||||
declare module "hcaptcha" {
|
||||
interface IVerifyResponse {
|
||||
success: boolean;
|
||||
challenge_ts: string;
|
||||
hostname: string;
|
||||
credit?: boolean;
|
||||
'error-codes'?: unknown[];
|
||||
"error-codes"?: unknown[];
|
||||
}
|
||||
|
||||
export function verify(secret: string, token: string): Promise<IVerifyResponse>;
|
||||
export function verify(
|
||||
secret: string,
|
||||
token: string,
|
||||
): Promise<IVerifyResponse>;
|
||||
}
|
||||
|
|
43
packages/backend/src/@types/http-signature.d.ts
vendored
43
packages/backend/src/@types/http-signature.d.ts
vendored
|
@ -1,5 +1,5 @@
|
|||
declare module '@peertube/http-signature' {
|
||||
import { IncomingMessage, ClientRequest } from 'node:http';
|
||||
declare module "@peertube/http-signature" {
|
||||
import type { IncomingMessage, ClientRequest } from "node:http";
|
||||
|
||||
interface ISignature {
|
||||
keyId: string;
|
||||
|
@ -28,8 +28,8 @@ declare module '@peertube/http-signature' {
|
|||
}
|
||||
|
||||
type RequestSignerConstructorOptions =
|
||||
IRequestSignerConstructorOptionsFromProperties |
|
||||
IRequestSignerConstructorOptionsFromFunction;
|
||||
| IRequestSignerConstructorOptionsFromProperties
|
||||
| IRequestSignerConstructorOptionsFromFunction;
|
||||
|
||||
interface IRequestSignerConstructorOptionsFromProperties {
|
||||
keyId: string;
|
||||
|
@ -59,11 +59,23 @@ declare module '@peertube/http-signature' {
|
|||
httpVersion?: string;
|
||||
}
|
||||
|
||||
export function parse(request: IncomingMessage, options?: IParseRequestOptions): IParsedSignature;
|
||||
export function parseRequest(request: IncomingMessage, options?: IParseRequestOptions): IParsedSignature;
|
||||
export function parse(
|
||||
request: IncomingMessage,
|
||||
options?: IParseRequestOptions,
|
||||
): IParsedSignature;
|
||||
export function parseRequest(
|
||||
request: IncomingMessage,
|
||||
options?: IParseRequestOptions,
|
||||
): IParsedSignature;
|
||||
|
||||
export function sign(request: ClientRequest, options: ISignRequestOptions): boolean;
|
||||
export function signRequest(request: ClientRequest, options: ISignRequestOptions): boolean;
|
||||
export function sign(
|
||||
request: ClientRequest,
|
||||
options: ISignRequestOptions,
|
||||
): boolean;
|
||||
export function signRequest(
|
||||
request: ClientRequest,
|
||||
options: ISignRequestOptions,
|
||||
): boolean;
|
||||
export function createSigner(): RequestSigner;
|
||||
export function isSigner(obj: any): obj is RequestSigner;
|
||||
|
||||
|
@ -71,7 +83,16 @@ declare module '@peertube/http-signature' {
|
|||
export function sshKeyFingerprint(key: string): string;
|
||||
export function pemToRsaSSHKey(pem: string, comment: string): string;
|
||||
|
||||
export function verify(parsedSignature: IParsedSignature, pubkey: string | Buffer): boolean;
|
||||
export function verifySignature(parsedSignature: IParsedSignature, pubkey: string | Buffer): boolean;
|
||||
export function verifyHMAC(parsedSignature: IParsedSignature, secret: string): boolean;
|
||||
export function verify(
|
||||
parsedSignature: IParsedSignature,
|
||||
pubkey: string | Buffer,
|
||||
): boolean;
|
||||
export function verifySignature(
|
||||
parsedSignature: IParsedSignature,
|
||||
pubkey: string | Buffer,
|
||||
): boolean;
|
||||
export function verifyHMAC(
|
||||
parsedSignature: IParsedSignature,
|
||||
secret: string,
|
||||
): boolean;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
declare module 'koa-json-body' {
|
||||
import { Middleware } from 'koa';
|
||||
declare module "koa-json-body" {
|
||||
import type { Middleware } from "koa";
|
||||
|
||||
interface IKoaJsonBodyOptions {
|
||||
strict: boolean;
|
||||
|
|
4
packages/backend/src/@types/koa-slow.d.ts
vendored
4
packages/backend/src/@types/koa-slow.d.ts
vendored
|
@ -1,5 +1,5 @@
|
|||
declare module 'koa-slow' {
|
||||
import { Middleware } from 'koa';
|
||||
declare module "koa-slow" {
|
||||
import type { Middleware } from "koa";
|
||||
|
||||
interface ISlowOptions {
|
||||
url?: RegExp;
|
||||
|
|
7
packages/backend/src/@types/os-utils.d.ts
vendored
7
packages/backend/src/@types/os-utils.d.ts
vendored
|
@ -1,4 +1,4 @@
|
|||
declare module 'os-utils' {
|
||||
declare module "os-utils" {
|
||||
type FreeCommandCallback = (usedmem: number) => void;
|
||||
|
||||
type HarddriveCallback = (total: number, free: number, used: number) => void;
|
||||
|
@ -20,7 +20,10 @@ declare module 'os-utils' {
|
|||
export function harddrive(callback: HarddriveCallback): void;
|
||||
|
||||
export function getProcesses(callback: GetProcessesCallback): void;
|
||||
export function getProcesses(nProcess: number, callback: GetProcessesCallback): void;
|
||||
export function getProcesses(
|
||||
nProcess: number,
|
||||
callback: GetProcessesCallback,
|
||||
): void;
|
||||
|
||||
export function allLoadavg(): string;
|
||||
export function loadavg(_time?: number): number;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
declare module '*/package.json' {
|
||||
declare module "*/package.json" {
|
||||
interface IRepository {
|
||||
type: string;
|
||||
url: string;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
declare module 'probe-image-size' {
|
||||
import { ReadStream } from 'node:fs';
|
||||
declare module "probe-image-size" {
|
||||
import type { ReadStream } from "node:fs";
|
||||
|
||||
type ProbeOptions = {
|
||||
retries: 1;
|
||||
|
@ -12,14 +12,24 @@ declare module 'probe-image-size' {
|
|||
length?: number;
|
||||
type: string;
|
||||
mime: string;
|
||||
wUnits: 'in' | 'mm' | 'cm' | 'pt' | 'pc' | 'px' | 'em' | 'ex';
|
||||
hUnits: 'in' | 'mm' | 'cm' | 'pt' | 'pc' | 'px' | 'em' | 'ex';
|
||||
wUnits: "in" | "mm" | "cm" | "pt" | "pc" | "px" | "em" | "ex";
|
||||
hUnits: "in" | "mm" | "cm" | "pt" | "pc" | "px" | "em" | "ex";
|
||||
url?: string;
|
||||
};
|
||||
|
||||
function probeImageSize(src: string | ReadStream, options?: ProbeOptions): Promise<ProbeResult>;
|
||||
function probeImageSize(src: string | ReadStream, callback: (err: Error | null, result?: ProbeResult) => void): void;
|
||||
function probeImageSize(src: string | ReadStream, options: ProbeOptions, callback: (err: Error | null, result?: ProbeResult) => void): void;
|
||||
function probeImageSize(
|
||||
src: string | ReadStream,
|
||||
options?: ProbeOptions,
|
||||
): Promise<ProbeResult>;
|
||||
function probeImageSize(
|
||||
src: string | ReadStream,
|
||||
callback: (err: Error | null, result?: ProbeResult) => void,
|
||||
): void;
|
||||
function probeImageSize(
|
||||
src: string | ReadStream,
|
||||
options: ProbeOptions,
|
||||
callback: (err: Error | null, result?: ProbeResult) => void,
|
||||
): void;
|
||||
|
||||
namespace probeImageSize {} // Hack
|
||||
|
||||
|
|
|
@ -1,28 +1,27 @@
|
|||
import cluster from 'node:cluster';
|
||||
import chalk from 'chalk';
|
||||
import Xev from 'xev';
|
||||
import cluster from "node:cluster";
|
||||
import chalk from "chalk";
|
||||
import Xev from "xev";
|
||||
|
||||
import Logger from '@/services/logger.js';
|
||||
import { envOption } from '../env.js';
|
||||
import Logger from "@/services/logger.js";
|
||||
import { envOption } from "../env.js";
|
||||
|
||||
// for typeorm
|
||||
import 'reflect-metadata';
|
||||
import { masterMain } from './master.js';
|
||||
import { workerMain } from './worker.js';
|
||||
import "reflect-metadata";
|
||||
import { masterMain } from "./master.js";
|
||||
import { workerMain } from "./worker.js";
|
||||
|
||||
const logger = new Logger('core', 'cyan');
|
||||
const clusterLogger = logger.createSubLogger('cluster', 'orange', false);
|
||||
const logger = new Logger("core", "cyan");
|
||||
const clusterLogger = logger.createSubLogger("cluster", "orange", false);
|
||||
const ev = new Xev();
|
||||
|
||||
/**
|
||||
* Init process
|
||||
*/
|
||||
export default async function () {
|
||||
process.title = `Calckey (${cluster.isPrimary ? 'master' : 'worker'})`;
|
||||
process.title = `Calckey (${cluster.isPrimary ? "master" : "worker"})`;
|
||||
|
||||
if (cluster.isPrimary || envOption.disableClustering) {
|
||||
await masterMain();
|
||||
|
||||
if (cluster.isPrimary) {
|
||||
ev.mount();
|
||||
}
|
||||
|
@ -35,24 +34,24 @@ export default async function() {
|
|||
// For when Calckey is started in a child process during unit testing.
|
||||
// Otherwise, process.send cannot be used, so start it.
|
||||
if (process.send) {
|
||||
process.send('ok');
|
||||
process.send("ok");
|
||||
}
|
||||
}
|
||||
|
||||
//#region Events
|
||||
|
||||
// Listen new workers
|
||||
cluster.on('fork', worker => {
|
||||
cluster.on("fork", (worker) => {
|
||||
clusterLogger.debug(`Process forked: [${worker.id}]`);
|
||||
});
|
||||
|
||||
// Listen online workers
|
||||
cluster.on('online', worker => {
|
||||
cluster.on("online", (worker) => {
|
||||
clusterLogger.debug(`Process is now online: [${worker.id}]`);
|
||||
});
|
||||
|
||||
// Listen for dying workers
|
||||
cluster.on('exit', worker => {
|
||||
cluster.on("exit", (worker) => {
|
||||
// Replace the dead worker,
|
||||
// we're not sentimental
|
||||
clusterLogger.error(chalk.red(`[${worker.id}] died :(`));
|
||||
|
@ -61,18 +60,18 @@ cluster.on('exit', worker => {
|
|||
|
||||
// Display detail of unhandled promise rejection
|
||||
if (!envOption.quiet) {
|
||||
process.on('unhandledRejection', console.dir);
|
||||
process.on("unhandledRejection", console.dir);
|
||||
}
|
||||
|
||||
// Display detail of uncaught exception
|
||||
process.on('uncaughtException', err => {
|
||||
process.on("uncaughtException", (err) => {
|
||||
try {
|
||||
logger.error(err);
|
||||
} catch {}
|
||||
});
|
||||
|
||||
// Dying away...
|
||||
process.on('exit', code => {
|
||||
process.on("exit", (code) => {
|
||||
logger.info(`The process is going to exit with code ${code}`);
|
||||
});
|
||||
|
||||
|
|
|
@ -1,50 +1,64 @@
|
|||
import * as fs from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname } from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import cluster from 'node:cluster';
|
||||
import chalk from 'chalk';
|
||||
import chalkTemplate from 'chalk-template';
|
||||
import semver from 'semver';
|
||||
import * as fs from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname } from "node:path";
|
||||
import * as os from "node:os";
|
||||
import cluster from "node:cluster";
|
||||
import chalk from "chalk";
|
||||
import chalkTemplate from "chalk-template";
|
||||
import semver from "semver";
|
||||
|
||||
import Logger from '@/services/logger.js';
|
||||
import loadConfig from '@/config/load.js';
|
||||
import { Config } from '@/config/types.js';
|
||||
import { lessThan } from '@/prelude/array.js';
|
||||
import { envOption } from '../env.js';
|
||||
import { showMachineInfo } from '@/misc/show-machine-info.js';
|
||||
import { db, initDb } from '../db/postgre.js';
|
||||
import Logger from "@/services/logger.js";
|
||||
import loadConfig from "@/config/load.js";
|
||||
import type { Config } from "@/config/types.js";
|
||||
import { lessThan } from "@/prelude/array.js";
|
||||
import { envOption } from "../env.js";
|
||||
import { showMachineInfo } from "@/misc/show-machine-info.js";
|
||||
import { db, initDb } from "../db/postgre.js";
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
|
||||
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8'));
|
||||
const meta = JSON.parse(
|
||||
fs.readFileSync(`${_dirname}/../../../../built/meta.json`, "utf-8"),
|
||||
);
|
||||
|
||||
const logger = new Logger('core', 'cyan');
|
||||
const bootLogger = logger.createSubLogger('boot', 'magenta', false);
|
||||
const logger = new Logger("core", "cyan");
|
||||
const bootLogger = logger.createSubLogger("boot", "magenta", false);
|
||||
|
||||
const themeColor = chalk.hex('#31748f');
|
||||
const themeColor = chalk.hex("#31748f");
|
||||
|
||||
function greet() {
|
||||
if (!envOption.quiet) {
|
||||
//#region Calckey logo
|
||||
const v = `v${meta.version}`;
|
||||
console.log(themeColor(' ___ _ _ '));
|
||||
console.log(themeColor(' / __\\__ _| | ___| | _____ _ _ '));
|
||||
console.log(themeColor(' / / / _` | |/ __| |/ / _ \ | | |'));
|
||||
console.log(themeColor('/ /__| (_| | | (__| < __/ |_| |'));
|
||||
console.log(themeColor('\\____/\\__,_|_|\\___|_|\\_\\___|\\__, |'));
|
||||
console.log(themeColor(' (___/ '));
|
||||
console.log(themeColor(" ___ _ _ "));
|
||||
console.log(themeColor(" / __\\__ _| | ___| | _____ _ _ "));
|
||||
console.log(themeColor(" / / / _` | |/ __| |/ / _ | | |"));
|
||||
console.log(themeColor("/ /__| (_| | | (__| < __/ |_| |"));
|
||||
console.log(themeColor("\\____/\\__,_|_|\\___|_|\\_\\___|\\__, |"));
|
||||
console.log(themeColor(" (___/ "));
|
||||
//#endregion
|
||||
|
||||
console.log(' Calckey is an open-source decentralized microblogging platform.');
|
||||
console.log(chalk.rgb(255, 136, 0)(' If you like Calckey, please consider starring or contributing to the repo. https://codeberg.org/calckey/calckey'));
|
||||
console.log(
|
||||
" Calckey is an open-source decentralized microblogging platform.",
|
||||
);
|
||||
console.log(
|
||||
chalk.rgb(
|
||||
255,
|
||||
136,
|
||||
0,
|
||||
)(
|
||||
" If you like Calckey, please consider starring or contributing to the repo. https://lavaforge.org/calckey/calckey",
|
||||
),
|
||||
);
|
||||
|
||||
console.log('');
|
||||
console.log(chalkTemplate`--- ${os.hostname()} {gray (PID: ${process.pid.toString()})} ---`);
|
||||
console.log("");
|
||||
console.log(
|
||||
chalkTemplate`--- ${os.hostname()} {gray (PID: ${process.pid.toString()})} ---`,
|
||||
);
|
||||
}
|
||||
|
||||
bootLogger.info('Welcome to Calckey!');
|
||||
bootLogger.info("Welcome to Calckey!");
|
||||
bootLogger.info(`Calckey v${meta.version}`, null, true);
|
||||
}
|
||||
|
||||
|
@ -63,42 +77,50 @@ export async function masterMain() {
|
|||
config = loadConfigBoot();
|
||||
await connectDb();
|
||||
} catch (e) {
|
||||
bootLogger.error('Fatal error occurred during initialization', null, true);
|
||||
bootLogger.error("Fatal error occurred during initialization", null, true);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
bootLogger.succ('Calckey initialized');
|
||||
bootLogger.succ("Calckey initialized");
|
||||
|
||||
if (!envOption.disableClustering) {
|
||||
await spawnWorkers(config.clusterLimit);
|
||||
}
|
||||
|
||||
bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true);
|
||||
bootLogger.succ(
|
||||
`Now listening on port ${config.port} on ${config.url}`,
|
||||
null,
|
||||
true,
|
||||
);
|
||||
|
||||
if (!envOption.noDaemons) {
|
||||
import('../daemons/server-stats.js').then(x => x.default());
|
||||
import('../daemons/queue-stats.js').then(x => x.default());
|
||||
import('../daemons/janitor.js').then(x => x.default());
|
||||
import("../daemons/server-stats.js").then((x) => x.default());
|
||||
import("../daemons/queue-stats.js").then((x) => x.default());
|
||||
import("../daemons/janitor.js").then((x) => x.default());
|
||||
}
|
||||
}
|
||||
|
||||
function showEnvironment(): void {
|
||||
const env = process.env.NODE_ENV;
|
||||
const logger = bootLogger.createSubLogger('env');
|
||||
logger.info(typeof env === 'undefined' ? 'NODE_ENV is not set' : `NODE_ENV: ${env}`);
|
||||
const logger = bootLogger.createSubLogger("env");
|
||||
logger.info(
|
||||
typeof env === "undefined" ? "NODE_ENV is not set" : `NODE_ENV: ${env}`,
|
||||
);
|
||||
|
||||
if (env !== 'production') {
|
||||
logger.warn('The environment is not in production mode.');
|
||||
logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', null, true);
|
||||
if (env !== "production") {
|
||||
logger.warn("The environment is not in production mode.");
|
||||
logger.warn("DO NOT USE FOR PRODUCTION PURPOSE!", null, true);
|
||||
}
|
||||
}
|
||||
|
||||
function showNodejsVersion(): void {
|
||||
const nodejsLogger = bootLogger.createSubLogger('nodejs');
|
||||
const nodejsLogger = bootLogger.createSubLogger("nodejs");
|
||||
|
||||
nodejsLogger.info(`Version ${process.version} detected.`);
|
||||
|
||||
const minVersion = fs.readFileSync(`${_dirname}/../../../../.node-version`, 'utf-8').trim();
|
||||
const minVersion = fs
|
||||
.readFileSync(`${_dirname}/../../../../.node-version`, "utf-8")
|
||||
.trim();
|
||||
if (semver.lt(process.version, minVersion)) {
|
||||
nodejsLogger.error(`At least Node.js ${minVersion} required!`);
|
||||
process.exit(1);
|
||||
|
@ -106,14 +128,14 @@ function showNodejsVersion(): void {
|
|||
}
|
||||
|
||||
function loadConfigBoot(): Config {
|
||||
const configLogger = bootLogger.createSubLogger('config');
|
||||
const configLogger = bootLogger.createSubLogger("config");
|
||||
let config;
|
||||
|
||||
try {
|
||||
config = loadConfig();
|
||||
} catch (exception) {
|
||||
if (exception.code === 'ENOENT') {
|
||||
configLogger.error('Configuration file not found', null, true);
|
||||
if (exception.code === "ENOENT") {
|
||||
configLogger.error("Configuration file not found", null, true);
|
||||
process.exit(1);
|
||||
} else if (e instanceof Error) {
|
||||
configLogger.error(e.message);
|
||||
|
@ -122,22 +144,24 @@ function loadConfigBoot(): Config {
|
|||
throw exception;
|
||||
}
|
||||
|
||||
configLogger.succ('Loaded');
|
||||
configLogger.succ("Loaded");
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
async function connectDb(): Promise<void> {
|
||||
const dbLogger = bootLogger.createSubLogger('db');
|
||||
const dbLogger = bootLogger.createSubLogger("db");
|
||||
|
||||
// Try to connect to DB
|
||||
try {
|
||||
dbLogger.info('Connecting...');
|
||||
dbLogger.info("Connecting...");
|
||||
await initDb();
|
||||
const v = await db.query('SHOW server_version').then(x => x[0].server_version);
|
||||
const v = await db
|
||||
.query("SHOW server_version")
|
||||
.then((x) => x[0].server_version);
|
||||
dbLogger.succ(`Connected: v${v}`);
|
||||
} catch (e) {
|
||||
dbLogger.error('Cannot connect', null, true);
|
||||
dbLogger.error("Cannot connect", null, true);
|
||||
dbLogger.error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
|
@ -145,20 +169,20 @@ async function connectDb(): Promise<void> {
|
|||
|
||||
async function spawnWorkers(limit: number = 1) {
|
||||
const workers = Math.min(limit, os.cpus().length);
|
||||
bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`);
|
||||
bootLogger.info(`Starting ${workers} worker${workers === 1 ? "" : "s"}...`);
|
||||
await Promise.all([...Array(workers)].map(spawnWorker));
|
||||
bootLogger.succ('All workers started');
|
||||
bootLogger.succ("All workers started");
|
||||
}
|
||||
|
||||
function spawnWorker(): Promise<void> {
|
||||
return new Promise(res => {
|
||||
return new Promise((res) => {
|
||||
const worker = cluster.fork();
|
||||
worker.on('message', message => {
|
||||
if (message === 'listenFailed') {
|
||||
bootLogger.error(`The server Listen failed due to the previous error.`);
|
||||
worker.on("message", (message) => {
|
||||
if (message === "listenFailed") {
|
||||
bootLogger.error("The server Listen failed due to the previous error.");
|
||||
process.exit(1);
|
||||
}
|
||||
if (message !== 'ready') return;
|
||||
if (message !== "ready") return;
|
||||
res();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import cluster from 'node:cluster';
|
||||
import { initDb } from '../db/postgre.js';
|
||||
import cluster from "node:cluster";
|
||||
import { initDb } from "../db/postgre.js";
|
||||
|
||||
/**
|
||||
* Init worker process
|
||||
|
@ -8,13 +8,13 @@ export async function workerMain() {
|
|||
await initDb();
|
||||
|
||||
// start server
|
||||
await import('../server/index.js').then(x => x.default());
|
||||
await import("../server/index.js").then((x) => x.default());
|
||||
|
||||
// start job queue
|
||||
import('../queue/index.js').then(x => x.default());
|
||||
import("../queue/index.js").then((x) => x.default());
|
||||
|
||||
if (cluster.isWorker) {
|
||||
// Send a 'ready' message to parent process
|
||||
process.send!('ready');
|
||||
process.send!("ready");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import load from './load.js';
|
||||
import load from "./load.js";
|
||||
|
||||
export default load();
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
* Config loader
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname } from 'node:path';
|
||||
import * as yaml from 'js-yaml';
|
||||
import type { Source, Mixin } from './types.js';
|
||||
import * as fs from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname } from "node:path";
|
||||
import * as yaml from "js-yaml";
|
||||
import type { Source, Mixin } from "./types.js";
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
|
@ -19,14 +19,20 @@ const dir = `${_dirname}/../../../../.config`;
|
|||
/**
|
||||
* Path of configuration file
|
||||
*/
|
||||
const path = process.env.NODE_ENV === 'test'
|
||||
? `${dir}/test.yml`
|
||||
: `${dir}/default.yml`;
|
||||
const path =
|
||||
process.env.NODE_ENV === "test" ? `${dir}/test.yml` : `${dir}/default.yml`;
|
||||
|
||||
export default function load() {
|
||||
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8'));
|
||||
const clientManifest = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/_client_dist_/manifest.json`, 'utf-8'));
|
||||
const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
|
||||
const meta = JSON.parse(
|
||||
fs.readFileSync(`${_dirname}/../../../../built/meta.json`, "utf-8"),
|
||||
);
|
||||
const clientManifest = JSON.parse(
|
||||
fs.readFileSync(
|
||||
`${_dirname}/../../../../built/_client_dist_/manifest.json`,
|
||||
"utf-8",
|
||||
),
|
||||
);
|
||||
const config = yaml.load(fs.readFileSync(path, "utf-8")) as Source;
|
||||
|
||||
const mixin = {} as Mixin;
|
||||
|
||||
|
@ -34,19 +40,19 @@ export default function load() {
|
|||
|
||||
config.url = url.origin;
|
||||
|
||||
config.port = config.port || parseInt(process.env.PORT || '', 10);
|
||||
config.port = config.port || parseInt(process.env.PORT || "", 10);
|
||||
|
||||
mixin.version = meta.version;
|
||||
mixin.host = url.host;
|
||||
mixin.hostname = url.hostname;
|
||||
mixin.scheme = url.protocol.replace(/:$/, '');
|
||||
mixin.wsScheme = mixin.scheme.replace('http', 'ws');
|
||||
mixin.scheme = url.protocol.replace(/:$/, "");
|
||||
mixin.wsScheme = mixin.scheme.replace("http", "ws");
|
||||
mixin.wsUrl = `${mixin.wsScheme}://${mixin.host}`;
|
||||
mixin.apiUrl = `${mixin.scheme}://${mixin.host}/api`;
|
||||
mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`;
|
||||
mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`;
|
||||
mixin.userAgent = `Calckey/${meta.version} (${config.url})`;
|
||||
mixin.clientEntry = clientManifest['src/init.ts'];
|
||||
mixin.clientEntry = clientManifest["src/init.ts"];
|
||||
|
||||
if (!config.redis.prefix) config.redis.prefix = mixin.host;
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ export type Source = {
|
|||
|
||||
id: string;
|
||||
|
||||
outgoingAddressFamily?: 'ipv4' | 'ipv6' | 'dual';
|
||||
outgoingAddressFamily?: "ipv4" | "ipv6" | "dual";
|
||||
|
||||
deliverJobConcurrency?: number;
|
||||
inboxJobConcurrency?: number;
|
||||
|
@ -81,7 +81,6 @@ export type Source = {
|
|||
user?: string;
|
||||
pass?: string;
|
||||
useImplicitSslTls?: boolean;
|
||||
|
||||
};
|
||||
objectStorage: {
|
||||
managed?: boolean;
|
||||
|
|
|
@ -1,55 +1,63 @@
|
|||
import config from '@/config/index.js';
|
||||
import config from "@/config/index.js";
|
||||
|
||||
export const MAX_NOTE_TEXT_LENGTH = config.maxNoteLength != null ? config.maxNoteLength : 3000;
|
||||
export const MAX_NOTE_TEXT_LENGTH =
|
||||
config.maxNoteLength != null ? config.maxNoteLength : 3000; // <- should we increase this?
|
||||
|
||||
export const SECOND = 1000;
|
||||
export const SEC = 1000;
|
||||
export const SEC = 1000; // why do we need this duplicate here?
|
||||
export const MINUTE = 60 * SEC;
|
||||
export const MIN = 60 * SEC;
|
||||
export const MIN = 60 * SEC; // why do we need this duplicate here?
|
||||
export const HOUR = 60 * MIN;
|
||||
export const DAY = 24 * HOUR;
|
||||
|
||||
export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min
|
||||
export const USER_ACTIVE_THRESHOLD = 1000 * 60 * 60 * 24 * 3; // 3days
|
||||
export const USER_ONLINE_THRESHOLD = 10 * MINUTE;
|
||||
export const USER_ACTIVE_THRESHOLD = 3 * DAY;
|
||||
|
||||
// ブラウザで直接表示することを許可するファイルの種類のリスト
|
||||
// ここに含まれないものは application/octet-stream としてレスポンスされる
|
||||
// SVGはXSSを生むので許可しない
|
||||
// List of file types allowed to be viewed directly in the browser
|
||||
// Anything not included here will be responded as application/octet-stream
|
||||
// SVG is not allowed because it generates XSS <- we need to fix this and later allow it to be viewed directly
|
||||
export const FILE_TYPE_BROWSERSAFE = [
|
||||
// Images
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'image/jpeg',
|
||||
'image/webp',
|
||||
'image/apng',
|
||||
'image/bmp',
|
||||
'image/tiff',
|
||||
'image/x-icon',
|
||||
"image/png",
|
||||
"image/gif", // TODO: deprecated, but still used by old notes, new gifs should be converted to webp in the future
|
||||
"image/jpeg",
|
||||
"image/webp", // TODO: make this the default image format
|
||||
"image/apng",
|
||||
"image/bmp",
|
||||
"image/tiff",
|
||||
"image/x-icon",
|
||||
"image/avif", // not as good supported now, but its good to introduce initial support for the future
|
||||
|
||||
// OggS
|
||||
'audio/opus',
|
||||
'video/ogg',
|
||||
'audio/ogg',
|
||||
'application/ogg',
|
||||
"audio/opus",
|
||||
"video/ogg",
|
||||
"audio/ogg",
|
||||
"application/ogg",
|
||||
|
||||
// ISO/IEC base media file format
|
||||
'video/quicktime',
|
||||
'video/mp4',
|
||||
'audio/mp4',
|
||||
'video/x-m4v',
|
||||
'audio/x-m4a',
|
||||
'video/3gpp',
|
||||
'video/3gpp2',
|
||||
"video/quicktime",
|
||||
"video/mp4", // TODO: we need to check for av1 later
|
||||
"video/vnd.avi", // also av1
|
||||
"audio/mp4",
|
||||
"video/x-m4v",
|
||||
"audio/x-m4a",
|
||||
"video/3gpp",
|
||||
"video/3gpp2",
|
||||
"video/3gp2",
|
||||
"audio/3gpp",
|
||||
"audio/3gpp2",
|
||||
"audio/3gp2",
|
||||
|
||||
'video/mpeg',
|
||||
'audio/mpeg',
|
||||
"video/mpeg",
|
||||
"audio/mpeg",
|
||||
|
||||
'video/webm',
|
||||
'audio/webm',
|
||||
"video/webm",
|
||||
"audio/webm",
|
||||
|
||||
'audio/aac',
|
||||
'audio/x-flac',
|
||||
'audio/vnd.wave',
|
||||
"audio/aac",
|
||||
"audio/x-flac",
|
||||
"audio/flac",
|
||||
"audio/vnd.wave",
|
||||
];
|
||||
/*
|
||||
https://github.com/sindresorhus/file-type/blob/main/supported.js
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
// TODO: 消したい
|
||||
|
||||
const interval = 30 * 60 * 1000;
|
||||
import { AttestationChallenges } from '@/models/index.js';
|
||||
import { LessThan } from 'typeorm';
|
||||
import { AttestationChallenges } from "@/models/index.js";
|
||||
import { LessThan } from "typeorm";
|
||||
|
||||
/**
|
||||
* Clean up database occasionally
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Xev from 'xev';
|
||||
import { deliverQueue, inboxQueue } from '../queue/queues.js';
|
||||
import Xev from "xev";
|
||||
import { deliverQueue, inboxQueue } from "../queue/queues.js";
|
||||
|
||||
const ev = new Xev();
|
||||
|
||||
|
@ -11,18 +11,18 @@ const interval = 10000;
|
|||
export default function () {
|
||||
const log = [] as any[];
|
||||
|
||||
ev.on('requestQueueStatsLog', x => {
|
||||
ev.on("requestQueueStatsLog", (x) => {
|
||||
ev.emit(`queueStatsLog:${x.id}`, log.slice(0, x.length || 50));
|
||||
});
|
||||
|
||||
let activeDeliverJobs = 0;
|
||||
let activeInboxJobs = 0;
|
||||
|
||||
deliverQueue.on('global:active', () => {
|
||||
deliverQueue.on("global:active", () => {
|
||||
activeDeliverJobs++;
|
||||
});
|
||||
|
||||
inboxQueue.on('global:active', () => {
|
||||
inboxQueue.on("global:active", () => {
|
||||
activeInboxJobs++;
|
||||
});
|
||||
|
||||
|
@ -45,7 +45,7 @@ export default function() {
|
|||
},
|
||||
};
|
||||
|
||||
ev.emit('queueStats', stats);
|
||||
ev.emit("queueStats", stats);
|
||||
|
||||
log.unshift(stats);
|
||||
if (log.length > 200) log.pop();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import si from 'systeminformation';
|
||||
import Xev from 'xev';
|
||||
import * as osUtils from 'os-utils';
|
||||
import si from "systeminformation";
|
||||
import Xev from "xev";
|
||||
import * as osUtils from "os-utils";
|
||||
|
||||
const ev = new Xev();
|
||||
|
||||
|
@ -15,7 +15,7 @@ const round = (num: number) => Math.round(num * 10) / 10;
|
|||
export default function () {
|
||||
const log = [] as any[];
|
||||
|
||||
ev.on('requestServerStatsLog', x => {
|
||||
ev.on("requestServerStatsLog", (x) => {
|
||||
ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length || 50));
|
||||
});
|
||||
|
||||
|
@ -40,7 +40,7 @@ export default function() {
|
|||
w: round(Math.max(0, fsStats.wIO_sec ?? 0)),
|
||||
},
|
||||
};
|
||||
ev.emit('serverStats', stats);
|
||||
ev.emit("serverStats", stats);
|
||||
log.unshift(stats);
|
||||
if (log.length > 200) log.pop();
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import * as elasticsearch from '@elastic/elasticsearch';
|
||||
import config from '@/config/index.js';
|
||||
import * as elasticsearch from "@elastic/elasticsearch";
|
||||
import config from "@/config/index.js";
|
||||
|
||||
const index = {
|
||||
settings: {
|
||||
analysis: {
|
||||
analyzer: {
|
||||
ngram: {
|
||||
tokenizer: 'ngram',
|
||||
tokenizer: "ngram",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -14,16 +14,16 @@ const index = {
|
|||
mappings: {
|
||||
properties: {
|
||||
text: {
|
||||
type: 'text',
|
||||
type: "text",
|
||||
index: true,
|
||||
analyzer: 'ngram',
|
||||
analyzer: "ngram",
|
||||
},
|
||||
userId: {
|
||||
type: 'keyword',
|
||||
type: "keyword",
|
||||
index: true,
|
||||
},
|
||||
userHost: {
|
||||
type: 'keyword',
|
||||
type: "keyword",
|
||||
index: true,
|
||||
},
|
||||
},
|
||||
|
@ -31,22 +31,31 @@ const index = {
|
|||
};
|
||||
|
||||
// Init ElasticSearch connection
|
||||
const client = config.elasticsearch ? new elasticsearch.Client({
|
||||
node: `${config.elasticsearch.ssl ? 'https://' : 'http://'}${config.elasticsearch.host}:${config.elasticsearch.port}`,
|
||||
auth: (config.elasticsearch.user && config.elasticsearch.pass) ? {
|
||||
const client = config.elasticsearch
|
||||
? new elasticsearch.Client({
|
||||
node: `${config.elasticsearch.ssl ? "https://" : "http://"}${
|
||||
config.elasticsearch.host
|
||||
}:${config.elasticsearch.port}`,
|
||||
auth:
|
||||
config.elasticsearch.user && config.elasticsearch.pass
|
||||
? {
|
||||
username: config.elasticsearch.user,
|
||||
password: config.elasticsearch.pass,
|
||||
} : undefined,
|
||||
}
|
||||
: undefined,
|
||||
pingTimeout: 30000,
|
||||
}) : null;
|
||||
})
|
||||
: null;
|
||||
|
||||
if (client) {
|
||||
client.indices.exists({
|
||||
index: config.elasticsearch.index || 'misskey_note',
|
||||
}).then(exist => {
|
||||
client.indices
|
||||
.exists({
|
||||
index: config.elasticsearch.index || "misskey_note",
|
||||
})
|
||||
.then((exist) => {
|
||||
if (!exist.body) {
|
||||
client.indices.create({
|
||||
index: config.elasticsearch.index || 'misskey_note',
|
||||
index: config.elasticsearch.index || "misskey_note",
|
||||
body: index,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import Logger from '@/services/logger.js';
|
||||
import Logger from "@/services/logger.js";
|
||||
|
||||
export const dbLogger = new Logger('db');
|
||||
export const dbLogger = new Logger("db");
|
||||
|
|
|
@ -1,87 +1,89 @@
|
|||
// https://github.com/typeorm/typeorm/issues/2400
|
||||
import pg from 'pg';
|
||||
import pg from "pg";
|
||||
pg.types.setTypeParser(20, Number);
|
||||
|
||||
import { Logger, DataSource } from 'typeorm';
|
||||
import * as highlight from 'cli-highlight';
|
||||
import config from '@/config/index.js';
|
||||
import type { Logger } from "typeorm";
|
||||
import { DataSource } from "typeorm";
|
||||
import * as highlight from "cli-highlight";
|
||||
import config from "@/config/index.js";
|
||||
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||
import { DriveFolder } from '@/models/entities/drive-folder.js';
|
||||
import { AccessToken } from '@/models/entities/access-token.js';
|
||||
import { App } from '@/models/entities/app.js';
|
||||
import { PollVote } from '@/models/entities/poll-vote.js';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import { NoteReaction } from '@/models/entities/note-reaction.js';
|
||||
import { NoteWatching } from '@/models/entities/note-watching.js';
|
||||
import { NoteThreadMuting } from '@/models/entities/note-thread-muting.js';
|
||||
import { NoteUnread } from '@/models/entities/note-unread.js';
|
||||
import { Notification } from '@/models/entities/notification.js';
|
||||
import { Meta } from '@/models/entities/meta.js';
|
||||
import { Following } from '@/models/entities/following.js';
|
||||
import { Instance } from '@/models/entities/instance.js';
|
||||
import { Muting } from '@/models/entities/muting.js';
|
||||
import { SwSubscription } from '@/models/entities/sw-subscription.js';
|
||||
import { Blocking } from '@/models/entities/blocking.js';
|
||||
import { UserList } from '@/models/entities/user-list.js';
|
||||
import { UserListJoining } from '@/models/entities/user-list-joining.js';
|
||||
import { UserGroup } from '@/models/entities/user-group.js';
|
||||
import { UserGroupJoining } from '@/models/entities/user-group-joining.js';
|
||||
import { UserGroupInvitation } from '@/models/entities/user-group-invitation.js';
|
||||
import { Hashtag } from '@/models/entities/hashtag.js';
|
||||
import { NoteFavorite } from '@/models/entities/note-favorite.js';
|
||||
import { AbuseUserReport } from '@/models/entities/abuse-user-report.js';
|
||||
import { RegistrationTicket } from '@/models/entities/registration-tickets.js';
|
||||
import { MessagingMessage } from '@/models/entities/messaging-message.js';
|
||||
import { Signin } from '@/models/entities/signin.js';
|
||||
import { AuthSession } from '@/models/entities/auth-session.js';
|
||||
import { FollowRequest } from '@/models/entities/follow-request.js';
|
||||
import { Emoji } from '@/models/entities/emoji.js';
|
||||
import { UserNotePining } from '@/models/entities/user-note-pining.js';
|
||||
import { Poll } from '@/models/entities/poll.js';
|
||||
import { UserKeypair } from '@/models/entities/user-keypair.js';
|
||||
import { UserPublickey } from '@/models/entities/user-publickey.js';
|
||||
import { UserProfile } from '@/models/entities/user-profile.js';
|
||||
import { UserSecurityKey } from '@/models/entities/user-security-key.js';
|
||||
import { AttestationChallenge } from '@/models/entities/attestation-challenge.js';
|
||||
import { Page } from '@/models/entities/page.js';
|
||||
import { PageLike } from '@/models/entities/page-like.js';
|
||||
import { GalleryPost } from '@/models/entities/gallery-post.js';
|
||||
import { GalleryLike } from '@/models/entities/gallery-like.js';
|
||||
import { ModerationLog } from '@/models/entities/moderation-log.js';
|
||||
import { UsedUsername } from '@/models/entities/used-username.js';
|
||||
import { Announcement } from '@/models/entities/announcement.js';
|
||||
import { AnnouncementRead } from '@/models/entities/announcement-read.js';
|
||||
import { Clip } from '@/models/entities/clip.js';
|
||||
import { ClipNote } from '@/models/entities/clip-note.js';
|
||||
import { Antenna } from '@/models/entities/antenna.js';
|
||||
import { AntennaNote } from '@/models/entities/antenna-note.js';
|
||||
import { PromoNote } from '@/models/entities/promo-note.js';
|
||||
import { PromoRead } from '@/models/entities/promo-read.js';
|
||||
import { Relay } from '@/models/entities/relay.js';
|
||||
import { MutedNote } from '@/models/entities/muted-note.js';
|
||||
import { Channel } from '@/models/entities/channel.js';
|
||||
import { ChannelFollowing } from '@/models/entities/channel-following.js';
|
||||
import { ChannelNotePining } from '@/models/entities/channel-note-pining.js';
|
||||
import { RegistryItem } from '@/models/entities/registry-item.js';
|
||||
import { Ad } from '@/models/entities/ad.js';
|
||||
import { PasswordResetRequest } from '@/models/entities/password-reset-request.js';
|
||||
import { UserPending } from '@/models/entities/user-pending.js';
|
||||
import { Webhook } from '@/models/entities/webhook.js';
|
||||
import { UserIp } from '@/models/entities/user-ip.js';
|
||||
import { User } from "@/models/entities/user.js";
|
||||
import { DriveFile } from "@/models/entities/drive-file.js";
|
||||
import { DriveFolder } from "@/models/entities/drive-folder.js";
|
||||
import { AccessToken } from "@/models/entities/access-token.js";
|
||||
import { App } from "@/models/entities/app.js";
|
||||
import { PollVote } from "@/models/entities/poll-vote.js";
|
||||
import { Note } from "@/models/entities/note.js";
|
||||
import { NoteReaction } from "@/models/entities/note-reaction.js";
|
||||
import { NoteWatching } from "@/models/entities/note-watching.js";
|
||||
import { NoteThreadMuting } from "@/models/entities/note-thread-muting.js";
|
||||
import { NoteUnread } from "@/models/entities/note-unread.js";
|
||||
import { Notification } from "@/models/entities/notification.js";
|
||||
import { Meta } from "@/models/entities/meta.js";
|
||||
import { Following } from "@/models/entities/following.js";
|
||||
import { Instance } from "@/models/entities/instance.js";
|
||||
import { Muting } from "@/models/entities/muting.js";
|
||||
import { SwSubscription } from "@/models/entities/sw-subscription.js";
|
||||
import { Blocking } from "@/models/entities/blocking.js";
|
||||
import { UserList } from "@/models/entities/user-list.js";
|
||||
import { UserListJoining } from "@/models/entities/user-list-joining.js";
|
||||
import { UserGroup } from "@/models/entities/user-group.js";
|
||||
import { UserGroupJoining } from "@/models/entities/user-group-joining.js";
|
||||
import { UserGroupInvitation } from "@/models/entities/user-group-invitation.js";
|
||||
import { Hashtag } from "@/models/entities/hashtag.js";
|
||||
import { NoteFavorite } from "@/models/entities/note-favorite.js";
|
||||
import { AbuseUserReport } from "@/models/entities/abuse-user-report.js";
|
||||
import { RegistrationTicket } from "@/models/entities/registration-tickets.js";
|
||||
import { MessagingMessage } from "@/models/entities/messaging-message.js";
|
||||
import { Signin } from "@/models/entities/signin.js";
|
||||
import { AuthSession } from "@/models/entities/auth-session.js";
|
||||
import { FollowRequest } from "@/models/entities/follow-request.js";
|
||||
import { Emoji } from "@/models/entities/emoji.js";
|
||||
import { UserNotePining } from "@/models/entities/user-note-pining.js";
|
||||
import { Poll } from "@/models/entities/poll.js";
|
||||
import { UserKeypair } from "@/models/entities/user-keypair.js";
|
||||
import { UserPublickey } from "@/models/entities/user-publickey.js";
|
||||
import { UserProfile } from "@/models/entities/user-profile.js";
|
||||
import { UserSecurityKey } from "@/models/entities/user-security-key.js";
|
||||
import { AttestationChallenge } from "@/models/entities/attestation-challenge.js";
|
||||
import { Page } from "@/models/entities/page.js";
|
||||
import { PageLike } from "@/models/entities/page-like.js";
|
||||
import { GalleryPost } from "@/models/entities/gallery-post.js";
|
||||
import { GalleryLike } from "@/models/entities/gallery-like.js";
|
||||
import { ModerationLog } from "@/models/entities/moderation-log.js";
|
||||
import { UsedUsername } from "@/models/entities/used-username.js";
|
||||
import { Announcement } from "@/models/entities/announcement.js";
|
||||
import { AnnouncementRead } from "@/models/entities/announcement-read.js";
|
||||
import { Clip } from "@/models/entities/clip.js";
|
||||
import { ClipNote } from "@/models/entities/clip-note.js";
|
||||
import { Antenna } from "@/models/entities/antenna.js";
|
||||
import { AntennaNote } from "@/models/entities/antenna-note.js";
|
||||
import { PromoNote } from "@/models/entities/promo-note.js";
|
||||
import { PromoRead } from "@/models/entities/promo-read.js";
|
||||
import { Relay } from "@/models/entities/relay.js";
|
||||
import { MutedNote } from "@/models/entities/muted-note.js";
|
||||
import { Channel } from "@/models/entities/channel.js";
|
||||
import { ChannelFollowing } from "@/models/entities/channel-following.js";
|
||||
import { ChannelNotePining } from "@/models/entities/channel-note-pining.js";
|
||||
import { RegistryItem } from "@/models/entities/registry-item.js";
|
||||
import { Ad } from "@/models/entities/ad.js";
|
||||
import { PasswordResetRequest } from "@/models/entities/password-reset-request.js";
|
||||
import { UserPending } from "@/models/entities/user-pending.js";
|
||||
import { Webhook } from "@/models/entities/webhook.js";
|
||||
import { UserIp } from "@/models/entities/user-ip.js";
|
||||
|
||||
import { entities as charts } from '@/services/chart/entities.js';
|
||||
import { envOption } from '../env.js';
|
||||
import { dbLogger } from './logger.js';
|
||||
import { redisClient } from './redis.js';
|
||||
import { entities as charts } from "@/services/chart/entities.js";
|
||||
import { envOption } from "../env.js";
|
||||
import { dbLogger } from "./logger.js";
|
||||
import { redisClient } from "./redis.js";
|
||||
|
||||
const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false);
|
||||
const sqlLogger = dbLogger.createSubLogger("sql", "gray", false);
|
||||
|
||||
class MyCustomLogger implements Logger {
|
||||
private highlight(sql: string) {
|
||||
return highlight.highlight(sql, {
|
||||
language: 'sql', ignoreIllegals: true,
|
||||
language: "sql",
|
||||
ignoreIllegals: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -178,10 +180,10 @@ export const entities = [
|
|||
...charts,
|
||||
];
|
||||
|
||||
const log = process.env.NODE_ENV !== 'production';
|
||||
const log = process.env.NODE_ENV !== "production";
|
||||
|
||||
export const db = new DataSource({
|
||||
type: 'postgres',
|
||||
type: "postgres",
|
||||
host: config.db.host,
|
||||
port: config.db.port,
|
||||
username: config.db.user,
|
||||
|
@ -191,10 +193,11 @@ export const db = new DataSource({
|
|||
statement_timeout: 1000 * 10,
|
||||
...config.db.extra,
|
||||
},
|
||||
synchronize: process.env.NODE_ENV === 'test',
|
||||
dropSchema: process.env.NODE_ENV === 'test',
|
||||
cache: !config.db.disableCache ? {
|
||||
type: 'ioredis',
|
||||
synchronize: process.env.NODE_ENV === "test",
|
||||
dropSchema: process.env.NODE_ENV === "test",
|
||||
cache: !config.db.disableCache
|
||||
? {
|
||||
type: "ioredis",
|
||||
options: {
|
||||
host: config.redis.host,
|
||||
port: config.redis.port,
|
||||
|
@ -203,12 +206,13 @@ export const db = new DataSource({
|
|||
keyPrefix: `${config.redis.prefix}:query:`,
|
||||
db: config.redis.db || 0,
|
||||
},
|
||||
} : false,
|
||||
}
|
||||
: false,
|
||||
logging: log,
|
||||
logger: log ? new MyCustomLogger() : undefined,
|
||||
maxQueryExecutionTime: 300,
|
||||
entities: entities,
|
||||
migrations: ['../../migration/*.js'],
|
||||
migrations: ["../../migration/*.js"],
|
||||
});
|
||||
|
||||
export async function initDb(force = false) {
|
||||
|
@ -247,7 +251,7 @@ export async function resetDb() {
|
|||
if (i === 3) {
|
||||
throw e;
|
||||
} else {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Redis from 'ioredis';
|
||||
import config from '@/config/index.js';
|
||||
import Redis from "ioredis";
|
||||
import config from "@/config/index.js";
|
||||
|
||||
export function createConnection() {
|
||||
return new Redis({
|
||||
|
|
|
@ -10,11 +10,16 @@ const envOption = {
|
|||
};
|
||||
|
||||
for (const key of Object.keys(envOption) as (keyof typeof envOption)[]) {
|
||||
if (process.env['MK_' + key.replace(/[A-Z]/g, letter => `_${letter}`).toUpperCase()]) envOption[key] = true;
|
||||
if (
|
||||
process.env[
|
||||
`MK_${key.replace(/[A-Z]/g, (letter) => `_${letter}`).toUpperCase()}`
|
||||
]
|
||||
)
|
||||
envOption[key] = true;
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'test') envOption.disableClustering = true;
|
||||
if (process.env.NODE_ENV === 'test') envOption.quiet = true;
|
||||
if (process.env.NODE_ENV === 'test') envOption.noDaemons = true;
|
||||
if (process.env.NODE_ENV === "test") envOption.disableClustering = true;
|
||||
if (process.env.NODE_ENV === "test") envOption.quiet = true;
|
||||
if (process.env.NODE_ENV === "test") envOption.noDaemons = true;
|
||||
|
||||
export { envOption };
|
||||
|
|
1
packages/backend/src/global.d.ts
vendored
1
packages/backend/src/global.d.ts
vendored
|
@ -1 +1,2 @@
|
|||
// rome-ignore lint/suspicious/noExplicitAny: i have no idea
|
||||
type FIXME = any;
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
* Misskey Entry Point!
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'node:events';
|
||||
import boot from './boot/index.js';
|
||||
import { EventEmitter } from "node:events";
|
||||
import boot from "./boot/index.js";
|
||||
|
||||
Error.stackTraceLimit = Infinity;
|
||||
EventEmitter.defaultMaxListeners = 128;
|
||||
|
||||
boot().catch(err => {
|
||||
boot().catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { URL } from 'node:url';
|
||||
import * as parse5 from 'parse5';
|
||||
import * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js';
|
||||
import { URL } from "node:url";
|
||||
import * as parse5 from "parse5";
|
||||
import * as TreeAdapter from "../../node_modules/parse5/dist/tree-adapters/default.js";
|
||||
|
||||
const treeAdapter = TreeAdapter.defaultTreeAdapter;
|
||||
|
||||
|
@ -9,11 +9,11 @@ const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
|
|||
|
||||
export function fromHtml(html: string, hashtagNames?: string[]): string {
|
||||
// some AP servers like Pixelfed use br tags as well as newlines
|
||||
html = html.replace(/<br\s?\/?>\r?\n/gi, '\n');
|
||||
html = html.replace(/<br\s?\/?>\r?\n/gi, "\n");
|
||||
|
||||
const dom = parse5.parseFragment(html);
|
||||
|
||||
let text = '';
|
||||
let text = "";
|
||||
|
||||
for (const n of dom.childNodes) {
|
||||
analyze(n);
|
||||
|
@ -23,14 +23,14 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
|
|||
|
||||
function getText(node: TreeAdapter.Node): string {
|
||||
if (treeAdapter.isTextNode(node)) return node.value;
|
||||
if (!treeAdapter.isElementNode(node)) return '';
|
||||
if (node.nodeName === 'br') return '\n';
|
||||
if (!treeAdapter.isElementNode(node)) return "";
|
||||
if (node.nodeName === "br") return "\n";
|
||||
|
||||
if (node.childNodes) {
|
||||
return node.childNodes.map(n => getText(n)).join('');
|
||||
return node.childNodes.map((n) => getText(n)).join("");
|
||||
}
|
||||
|
||||
return '';
|
||||
return "";
|
||||
}
|
||||
|
||||
function appendChildren(childNodes: TreeAdapter.ChildNode[]): void {
|
||||
|
@ -51,27 +51,30 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
|
|||
if (!treeAdapter.isElementNode(node)) return;
|
||||
|
||||
switch (node.nodeName) {
|
||||
case 'br': {
|
||||
text += '\n';
|
||||
case "br": {
|
||||
text += "\n";
|
||||
break;
|
||||
}
|
||||
|
||||
case 'a':
|
||||
{
|
||||
case "a": {
|
||||
const txt = getText(node);
|
||||
const rel = node.attrs.find(x => x.name === 'rel');
|
||||
const href = node.attrs.find(x => x.name === 'href');
|
||||
const rel = node.attrs.find((x) => x.name === "rel");
|
||||
const href = node.attrs.find((x) => x.name === "href");
|
||||
|
||||
// ハッシュタグ
|
||||
if (hashtagNames && href && hashtagNames.map(x => x.toLowerCase()).includes(txt.toLowerCase())) {
|
||||
if (
|
||||
hashtagNames &&
|
||||
href &&
|
||||
hashtagNames.map((x) => x.toLowerCase()).includes(txt.toLowerCase())
|
||||
) {
|
||||
text += txt;
|
||||
// メンション
|
||||
} else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) {
|
||||
const part = txt.split('@');
|
||||
} else if (txt.startsWith("@") && !rel?.value.match(/^me /)) {
|
||||
const part = txt.split("@");
|
||||
|
||||
if (part.length === 2 && href) {
|
||||
//#region ホスト名部分が省略されているので復元する
|
||||
const acct = `${txt}@${(new URL(href.value)).hostname}`;
|
||||
const acct = `${txt}@${new URL(href.value).hostname}`;
|
||||
text += acct;
|
||||
//#endregion
|
||||
} else if (part.length === 3) {
|
||||
|
@ -80,13 +83,14 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
|
|||
// その他
|
||||
} else {
|
||||
const generateLink = () => {
|
||||
if (!href && !txt) {
|
||||
return '';
|
||||
if (!(href || txt)) {
|
||||
return "";
|
||||
}
|
||||
if (!href) {
|
||||
return txt;
|
||||
}
|
||||
if (!txt || txt === href.value) { // #6383: Missing text node
|
||||
if (!txt || txt === href.value) {
|
||||
// #6383: Missing text node
|
||||
if (href.value.match(urlRegexFull)) {
|
||||
return href.value;
|
||||
} else {
|
||||
|
@ -105,55 +109,53 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
|
|||
break;
|
||||
}
|
||||
|
||||
case 'h1':
|
||||
{
|
||||
text += '【';
|
||||
case "h1": {
|
||||
text += "【";
|
||||
appendChildren(node.childNodes);
|
||||
text += '】\n';
|
||||
text += "】\n";
|
||||
break;
|
||||
}
|
||||
|
||||
case 'b':
|
||||
case 'strong':
|
||||
{
|
||||
text += '**';
|
||||
case "b":
|
||||
case "strong": {
|
||||
text += "**";
|
||||
appendChildren(node.childNodes);
|
||||
text += '**';
|
||||
text += "**";
|
||||
break;
|
||||
}
|
||||
|
||||
case 'small':
|
||||
{
|
||||
text += '<small>';
|
||||
case "small": {
|
||||
text += "<small>";
|
||||
appendChildren(node.childNodes);
|
||||
text += '</small>';
|
||||
text += "</small>";
|
||||
break;
|
||||
}
|
||||
|
||||
case 's':
|
||||
case 'del':
|
||||
{
|
||||
text += '~~';
|
||||
case "s":
|
||||
case "del": {
|
||||
text += "~~";
|
||||
appendChildren(node.childNodes);
|
||||
text += '~~';
|
||||
text += "~~";
|
||||
break;
|
||||
}
|
||||
|
||||
case 'i':
|
||||
case 'em':
|
||||
{
|
||||
text += '<i>';
|
||||
case "i":
|
||||
case "em": {
|
||||
text += "<i>";
|
||||
appendChildren(node.childNodes);
|
||||
text += '</i>';
|
||||
text += "</i>";
|
||||
break;
|
||||
}
|
||||
|
||||
// block code (<pre><code>)
|
||||
case 'pre': {
|
||||
if (node.childNodes.length === 1 && node.childNodes[0].nodeName === 'code') {
|
||||
text += '\n```\n';
|
||||
case "pre": {
|
||||
if (
|
||||
node.childNodes.length === 1 &&
|
||||
node.childNodes[0].nodeName === "code"
|
||||
) {
|
||||
text += "\n```\n";
|
||||
text += getText(node.childNodes[0]);
|
||||
text += '\n```\n';
|
||||
text += "\n```\n";
|
||||
} else {
|
||||
appendChildren(node.childNodes);
|
||||
}
|
||||
|
@ -161,50 +163,48 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
|
|||
}
|
||||
|
||||
// inline code (<code>)
|
||||
case 'code': {
|
||||
text += '`';
|
||||
case "code": {
|
||||
text += "`";
|
||||
appendChildren(node.childNodes);
|
||||
text += '`';
|
||||
text += "`";
|
||||
break;
|
||||
}
|
||||
|
||||
case 'blockquote': {
|
||||
case "blockquote": {
|
||||
const t = getText(node);
|
||||
if (t) {
|
||||
text += '\n> ';
|
||||
text += t.split('\n').join('\n> ');
|
||||
text += "\n> ";
|
||||
text += t.split("\n").join("\n> ");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'p':
|
||||
case 'h2':
|
||||
case 'h3':
|
||||
case 'h4':
|
||||
case 'h5':
|
||||
case 'h6':
|
||||
{
|
||||
text += '\n\n';
|
||||
case "p":
|
||||
case "h2":
|
||||
case "h3":
|
||||
case "h4":
|
||||
case "h5":
|
||||
case "h6": {
|
||||
text += "\n\n";
|
||||
appendChildren(node.childNodes);
|
||||
break;
|
||||
}
|
||||
|
||||
// other block elements
|
||||
case 'div':
|
||||
case 'header':
|
||||
case 'footer':
|
||||
case 'article':
|
||||
case 'li':
|
||||
case 'dt':
|
||||
case 'dd':
|
||||
{
|
||||
text += '\n';
|
||||
case "div":
|
||||
case "header":
|
||||
case "footer":
|
||||
case "article":
|
||||
case "li":
|
||||
case "dt":
|
||||
case "dd": {
|
||||
text += "\n";
|
||||
appendChildren(node.childNodes);
|
||||
break;
|
||||
}
|
||||
|
||||
default: // includes inline elements
|
||||
{
|
||||
default: {
|
||||
// includes inline elements
|
||||
appendChildren(node.childNodes);
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -1,65 +1,71 @@
|
|||
import { JSDOM } from 'jsdom';
|
||||
import * as mfm from 'mfm-js';
|
||||
import config from '@/config/index.js';
|
||||
import { intersperse } from '@/prelude/array.js';
|
||||
import { IMentionedRemoteUsers } from '@/models/entities/note.js';
|
||||
import { JSDOM } from "jsdom";
|
||||
import type * as mfm from "mfm-js";
|
||||
import config from "@/config/index.js";
|
||||
import { intersperse } from "@/prelude/array.js";
|
||||
import type { IMentionedRemoteUsers } from "@/models/entities/note.js";
|
||||
|
||||
export function toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = []) {
|
||||
export function toHtml(
|
||||
nodes: mfm.MfmNode[] | null,
|
||||
mentionedRemoteUsers: IMentionedRemoteUsers = [],
|
||||
) {
|
||||
if (nodes == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { window } = new JSDOM('');
|
||||
const { window } = new JSDOM("");
|
||||
|
||||
const doc = window.document;
|
||||
|
||||
function appendChildren(children: mfm.MfmNode[], targetElement: any): void {
|
||||
if (children) {
|
||||
for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child);
|
||||
for (const child of children.map((x) => (handlers as any)[x.type](x)))
|
||||
targetElement.appendChild(child);
|
||||
}
|
||||
}
|
||||
|
||||
const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any } = {
|
||||
const handlers: {
|
||||
[K in mfm.MfmNode["type"]]: (node: mfm.NodeType<K>) => any;
|
||||
} = {
|
||||
bold(node) {
|
||||
const el = doc.createElement('b');
|
||||
const el = doc.createElement("b");
|
||||
appendChildren(node.children, el);
|
||||
return el;
|
||||
},
|
||||
|
||||
small(node) {
|
||||
const el = doc.createElement('small');
|
||||
const el = doc.createElement("small");
|
||||
appendChildren(node.children, el);
|
||||
return el;
|
||||
},
|
||||
|
||||
strike(node) {
|
||||
const el = doc.createElement('del');
|
||||
const el = doc.createElement("del");
|
||||
appendChildren(node.children, el);
|
||||
return el;
|
||||
},
|
||||
|
||||
italic(node) {
|
||||
const el = doc.createElement('i');
|
||||
const el = doc.createElement("i");
|
||||
appendChildren(node.children, el);
|
||||
return el;
|
||||
},
|
||||
|
||||
fn(node) {
|
||||
const el = doc.createElement('i');
|
||||
const el = doc.createElement("i");
|
||||
appendChildren(node.children, el);
|
||||
return el;
|
||||
},
|
||||
|
||||
blockCode(node) {
|
||||
const pre = doc.createElement('pre');
|
||||
const inner = doc.createElement('code');
|
||||
const pre = doc.createElement("pre");
|
||||
const inner = doc.createElement("code");
|
||||
inner.textContent = node.props.code;
|
||||
pre.appendChild(inner);
|
||||
return pre;
|
||||
},
|
||||
|
||||
center(node) {
|
||||
const el = doc.createElement('div');
|
||||
const el = doc.createElement("div");
|
||||
appendChildren(node.children, el);
|
||||
return el;
|
||||
},
|
||||
|
@ -73,81 +79,90 @@ export function toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMenti
|
|||
},
|
||||
|
||||
hashtag(node) {
|
||||
const a = doc.createElement('a');
|
||||
const a = doc.createElement("a");
|
||||
a.href = `${config.url}/tags/${node.props.hashtag}`;
|
||||
a.textContent = `#${node.props.hashtag}`;
|
||||
a.setAttribute('rel', 'tag');
|
||||
a.setAttribute("rel", "tag");
|
||||
return a;
|
||||
},
|
||||
|
||||
inlineCode(node) {
|
||||
const el = doc.createElement('code');
|
||||
const el = doc.createElement("code");
|
||||
el.textContent = node.props.code;
|
||||
return el;
|
||||
},
|
||||
|
||||
mathInline(node) {
|
||||
const el = doc.createElement('code');
|
||||
const el = doc.createElement("code");
|
||||
el.textContent = node.props.formula;
|
||||
return el;
|
||||
},
|
||||
|
||||
mathBlock(node) {
|
||||
const el = doc.createElement('code');
|
||||
const el = doc.createElement("code");
|
||||
el.textContent = node.props.formula;
|
||||
return el;
|
||||
},
|
||||
|
||||
link(node) {
|
||||
const a = doc.createElement('a');
|
||||
const a = doc.createElement("a");
|
||||
a.href = node.props.url;
|
||||
appendChildren(node.children, a);
|
||||
return a;
|
||||
},
|
||||
|
||||
mention(node) {
|
||||
const a = doc.createElement('a');
|
||||
const a = doc.createElement("a");
|
||||
const { username, host, acct } = node.props;
|
||||
const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host);
|
||||
a.href = remoteUserInfo ? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri) : `${config.url}/${acct}`;
|
||||
a.className = 'u-url mention';
|
||||
const remoteUserInfo = mentionedRemoteUsers.find(
|
||||
(remoteUser) =>
|
||||
remoteUser.username === username && remoteUser.host === host,
|
||||
);
|
||||
a.href = remoteUserInfo
|
||||
? remoteUserInfo.url
|
||||
? remoteUserInfo.url
|
||||
: remoteUserInfo.uri
|
||||
: `${config.url}/${acct}`;
|
||||
a.className = "u-url mention";
|
||||
a.textContent = acct;
|
||||
return a;
|
||||
},
|
||||
|
||||
quote(node) {
|
||||
const el = doc.createElement('blockquote');
|
||||
const el = doc.createElement("blockquote");
|
||||
appendChildren(node.children, el);
|
||||
return el;
|
||||
},
|
||||
|
||||
text(node) {
|
||||
const el = doc.createElement('span');
|
||||
const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x));
|
||||
const el = doc.createElement("span");
|
||||
const nodes = node.props.text
|
||||
.split(/\r\n|\r|\n/)
|
||||
.map((x) => doc.createTextNode(x));
|
||||
|
||||
for (const x of intersperse<FIXME | 'br'>('br', nodes)) {
|
||||
el.appendChild(x === 'br' ? doc.createElement('br') : x);
|
||||
for (const x of intersperse<FIXME | "br">("br", nodes)) {
|
||||
el.appendChild(x === "br" ? doc.createElement("br") : x);
|
||||
}
|
||||
|
||||
return el;
|
||||
},
|
||||
|
||||
url(node) {
|
||||
const a = doc.createElement('a');
|
||||
const a = doc.createElement("a");
|
||||
a.href = node.props.url;
|
||||
a.textContent = node.props.url;
|
||||
return a;
|
||||
},
|
||||
|
||||
search(node) {
|
||||
const a = doc.createElement('a');
|
||||
const a = doc.createElement("a");
|
||||
a.href = `https://search.annoyingorange.xyz/search?q=${node.props.query}`;
|
||||
a.textContent = node.props.content;
|
||||
return a;
|
||||
},
|
||||
|
||||
plain(node) {
|
||||
const el = doc.createElement('span');
|
||||
const el = doc.createElement("span");
|
||||
appendChildren(node.children, el);
|
||||
return el;
|
||||
},
|
||||
|
|
|
@ -4,8 +4,8 @@ export type Acct = {
|
|||
};
|
||||
|
||||
export function parse(acct: string): Acct {
|
||||
if (acct.startsWith('@')) acct = acct.substr(1);
|
||||
const split = acct.split('@', 2);
|
||||
if (acct.startsWith("@")) acct = acct.substr(1);
|
||||
const split = acct.split("@", 2);
|
||||
return { username: split[0], host: split[1] || null };
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Antennas } from '@/models/index.js';
|
||||
import { Antenna } from '@/models/entities/antenna.js';
|
||||
import { subscriber } from '@/db/redis.js';
|
||||
import { Antennas } from "@/models/index.js";
|
||||
import type { Antenna } from "@/models/entities/antenna.js";
|
||||
import { subscriber } from "@/db/redis.js";
|
||||
|
||||
let antennasFetched = false;
|
||||
let antennas: Antenna[] = [];
|
||||
|
@ -14,20 +14,20 @@ export async function getAntennas() {
|
|||
return antennas;
|
||||
}
|
||||
|
||||
subscriber.on('message', async (_, data) => {
|
||||
subscriber.on("message", async (_, data) => {
|
||||
const obj = JSON.parse(data);
|
||||
|
||||
if (obj.channel === 'internal') {
|
||||
if (obj.channel === "internal") {
|
||||
const { type, body } = obj.message;
|
||||
switch (type) {
|
||||
case 'antennaCreated':
|
||||
case "antennaCreated":
|
||||
antennas.push(body);
|
||||
break;
|
||||
case 'antennaUpdated':
|
||||
antennas[antennas.findIndex(a => a.id === body.id)] = body;
|
||||
case "antennaUpdated":
|
||||
antennas[antennas.findIndex((a) => a.id === body.id)] = body;
|
||||
break;
|
||||
case 'antennaDeleted':
|
||||
antennas = antennas.filter(a => a.id !== body.id);
|
||||
case "antennaDeleted":
|
||||
antennas = antennas.filter((a) => a.id !== body.id);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
|
|
@ -1,35 +1,35 @@
|
|||
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',
|
||||
"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).
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import { redisClient } from '../db/redis.js';
|
||||
import { promisify } from 'node:util';
|
||||
import redisLock from 'redis-lock';
|
||||
import { redisClient } from "../db/redis.js";
|
||||
import { promisify } from "node:util";
|
||||
import redisLock from "redis-lock";
|
||||
|
||||
/**
|
||||
* Retry delay (ms) for lock acquisition
|
||||
*/
|
||||
const retryDelay = 100;
|
||||
|
||||
const lock: (key: string, timeout?: number) => Promise<() => void>
|
||||
= redisClient
|
||||
const lock: (key: string, timeout?: number) => Promise<() => void> = redisClient
|
||||
? promisify(redisLock(redisClient, retryDelay))
|
||||
: async () => () => {};
|
||||
|
||||
|
@ -22,7 +21,10 @@ export function getApLock(uri: string, timeout = 30 * 1000) {
|
|||
return lock(`ap-object:${uri}`, timeout);
|
||||
}
|
||||
|
||||
export function getFetchInstanceMetadataLock(host: string, timeout = 30 * 1000) {
|
||||
export function getFetchInstanceMetadataLock(
|
||||
host: string,
|
||||
timeout = 30 * 1000,
|
||||
) {
|
||||
return lock(`instance:${host}`, timeout);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// https://gist.github.com/nfantone/1eaa803772025df69d07f4dbf5df7e58
|
||||
|
||||
'use strict';
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* @callback BeforeShutdownListener
|
||||
|
@ -11,7 +11,7 @@
|
|||
* System signals the app will listen to initiate shutdown.
|
||||
* @const {string[]}
|
||||
*/
|
||||
const SHUTDOWN_SIGNALS = ['SIGINT', 'SIGTERM'];
|
||||
const SHUTDOWN_SIGNALS = ["SIGINT", "SIGTERM"];
|
||||
|
||||
/**
|
||||
* Time in milliseconds to wait before forcing shutdown.
|
||||
|
@ -31,7 +31,10 @@ const shutdownListeners: ((signalOrEvent: string) => void)[] = [];
|
|||
* @param {string[]} signals System signals to listen to.
|
||||
* @param {function(string)} fn Function to execute on shutdown.
|
||||
*/
|
||||
const processOnce = (signals: string[], fn: (signalOrEvent: string) => void) => {
|
||||
const processOnce = (
|
||||
signals: string[],
|
||||
fn: (signalOrEvent: string) => void,
|
||||
) => {
|
||||
for (const sig of signals) {
|
||||
process.once(sig, fn);
|
||||
}
|
||||
|
@ -44,7 +47,9 @@ const processOnce = (signals: string[], fn: (signalOrEvent: string) => void) =>
|
|||
const forceExitAfter = (timeout: number) => () => {
|
||||
setTimeout(() => {
|
||||
// Force shutdown after timeout
|
||||
console.warn(`Could not close resources gracefully after ${timeout}ms: forcing shutdown`);
|
||||
console.warn(
|
||||
`Could not close resources gracefully after ${timeout}ms: forcing shutdown`,
|
||||
);
|
||||
return process.exit(1);
|
||||
}, timeout).unref();
|
||||
};
|
||||
|
@ -56,7 +61,7 @@ const forceExitAfter = (timeout: number) => () => {
|
|||
* @param {string} signalOrEvent The exit signal or event name received on the process.
|
||||
*/
|
||||
async function shutdownHandler(signalOrEvent: string) {
|
||||
if (process.env.NODE_ENV === 'test') return process.exit(0);
|
||||
if (process.env.NODE_ENV === "test") return process.exit(0);
|
||||
|
||||
console.warn(`Shutting down: received [${signalOrEvent}] signal`);
|
||||
|
||||
|
@ -65,7 +70,11 @@ async function shutdownHandler(signalOrEvent: string) {
|
|||
await listener(signalOrEvent);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
console.warn(`A shutdown handler failed before completing with: ${err.message || err}`);
|
||||
console.warn(
|
||||
`A shutdown handler failed before completing with: ${
|
||||
err.message || err
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
export class Cache<T> {
|
||||
public cache: Map<string | null, { date: number; value: T; }>;
|
||||
public cache: Map<string | null, { date: number; value: T }>;
|
||||
private lifetime: number;
|
||||
|
||||
constructor(lifetime: Cache<never>['lifetime']) {
|
||||
constructor(lifetime: Cache<never>["lifetime"]) {
|
||||
this.cache = new Map();
|
||||
this.lifetime = lifetime;
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ export class Cache<T> {
|
|||
public get(key: string | null): T | undefined {
|
||||
const cached = this.cache.get(key);
|
||||
if (cached == null) return undefined;
|
||||
if ((Date.now() - cached.date) > this.lifetime) {
|
||||
if (Date.now() - cached.date > this.lifetime) {
|
||||
this.cache.delete(key);
|
||||
return undefined;
|
||||
}
|
||||
|
@ -32,7 +32,11 @@ export class Cache<T> {
|
|||
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
||||
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
|
||||
*/
|
||||
public async fetch(key: string | null, fetcher: () => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> {
|
||||
public async fetch(
|
||||
key: string | null,
|
||||
fetcher: () => Promise<T>,
|
||||
validator?: (cachedValue: T) => boolean,
|
||||
): Promise<T> {
|
||||
const cachedValue = this.get(key);
|
||||
if (cachedValue !== undefined) {
|
||||
if (validator) {
|
||||
|
@ -56,7 +60,11 @@ export class Cache<T> {
|
|||
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
||||
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
|
||||
*/
|
||||
public async fetchMaybe(key: string | null, fetcher: () => Promise<T | undefined>, validator?: (cachedValue: T) => boolean): Promise<T | undefined> {
|
||||
public async fetchMaybe(
|
||||
key: string | null,
|
||||
fetcher: () => Promise<T | undefined>,
|
||||
validator?: (cachedValue: T) => boolean,
|
||||
): Promise<T | undefined> {
|
||||
const cachedValue = this.get(key);
|
||||
if (cachedValue !== undefined) {
|
||||
if (validator) {
|
||||
|
|
|
@ -1,51 +1,67 @@
|
|||
import fetch from 'node-fetch';
|
||||
import { URLSearchParams } from 'node:url';
|
||||
import { getAgentByUrl } from './fetch.js';
|
||||
import config from '@/config/index.js';
|
||||
import fetch from "node-fetch";
|
||||
import { URLSearchParams } from "node:url";
|
||||
import { getAgentByUrl } from "./fetch.js";
|
||||
import config from "@/config/index.js";
|
||||
|
||||
export async function verifyRecaptcha(secret: string, response: string) {
|
||||
const result = await getCaptchaResponse('https://www.recaptcha.net/recaptcha/api/siteverify', secret, response).catch(e => {
|
||||
const result = await getCaptchaResponse(
|
||||
"https://www.recaptcha.net/recaptcha/api/siteverify",
|
||||
secret,
|
||||
response,
|
||||
).catch((e) => {
|
||||
throw new Error(`recaptcha-request-failed: ${e.message}`);
|
||||
});
|
||||
|
||||
if (result.success !== true) {
|
||||
const errorCodes = result['error-codes'] ? result['error-codes']?.join(', ') : '';
|
||||
const errorCodes = result["error-codes"]
|
||||
? result["error-codes"]?.join(", ")
|
||||
: "";
|
||||
throw new Error(`recaptcha-failed: ${errorCodes}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifyHcaptcha(secret: string, response: string) {
|
||||
const result = await getCaptchaResponse('https://hcaptcha.com/siteverify', secret, response).catch(e => {
|
||||
const result = await getCaptchaResponse(
|
||||
"https://hcaptcha.com/siteverify",
|
||||
secret,
|
||||
response,
|
||||
).catch((e) => {
|
||||
throw new Error(`hcaptcha-request-failed: ${e.message}`);
|
||||
});
|
||||
|
||||
if (result.success !== true) {
|
||||
const errorCodes = result['error-codes'] ? result['error-codes']?.join(', ') : '';
|
||||
const errorCodes = result["error-codes"]
|
||||
? result["error-codes"]?.join(", ")
|
||||
: "";
|
||||
throw new Error(`hcaptcha-failed: ${errorCodes}`);
|
||||
}
|
||||
}
|
||||
|
||||
type CaptchaResponse = {
|
||||
success: boolean;
|
||||
'error-codes'?: string[];
|
||||
"error-codes"?: string[];
|
||||
};
|
||||
|
||||
async function getCaptchaResponse(url: string, secret: string, response: string): Promise<CaptchaResponse> {
|
||||
async function getCaptchaResponse(
|
||||
url: string,
|
||||
secret: string,
|
||||
response: string,
|
||||
): Promise<CaptchaResponse> {
|
||||
const params = new URLSearchParams({
|
||||
secret,
|
||||
response,
|
||||
});
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
body: params,
|
||||
headers: {
|
||||
'User-Agent': config.userAgent,
|
||||
"User-Agent": config.userAgent,
|
||||
},
|
||||
// TODO
|
||||
//timeout: 10 * 1000,
|
||||
agent: getAgentByUrl,
|
||||
}).catch(e => {
|
||||
}).catch((e) => {
|
||||
throw new Error(`${e.message || e}`);
|
||||
});
|
||||
|
||||
|
@ -53,5 +69,5 @@ async function getCaptchaResponse(url: string, secret: string, response: string)
|
|||
throw new Error(`${res.status}`);
|
||||
}
|
||||
|
||||
return await res.json() as CaptchaResponse;
|
||||
return (await res.json()) as CaptchaResponse;
|
||||
}
|
||||
|
|
|
@ -1,90 +1,121 @@
|
|||
import { Antenna } from '@/models/entities/antenna.js';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { UserListJoinings, UserGroupJoinings, Blockings } from '@/models/index.js';
|
||||
import { getFullApAccount } from './convert-host.js';
|
||||
import * as Acct from '@/misc/acct.js';
|
||||
import { Packed } from './schema.js';
|
||||
import { Cache } from './cache.js';
|
||||
import type { Antenna } from "@/models/entities/antenna.js";
|
||||
import type { Note } from "@/models/entities/note.js";
|
||||
import type { User } from "@/models/entities/user.js";
|
||||
import {
|
||||
UserListJoinings,
|
||||
UserGroupJoinings,
|
||||
Blockings,
|
||||
} from "@/models/index.js";
|
||||
import { getFullApAccount } from "./convert-host.js";
|
||||
import * as Acct from "@/misc/acct.js";
|
||||
import type { Packed } from "./schema.js";
|
||||
import { Cache } from "./cache.js";
|
||||
|
||||
const blockingCache = new Cache<User['id'][]>(1000 * 60 * 5);
|
||||
const blockingCache = new Cache<User["id"][]>(1000 * 60 * 5);
|
||||
|
||||
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
|
||||
|
||||
/**
|
||||
* noteUserFollowers / antennaUserFollowing はどちらか一方が指定されていればよい
|
||||
*/
|
||||
export async function checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }, noteUserFollowers?: User['id'][], antennaUserFollowing?: User['id'][]): Promise<boolean> {
|
||||
if (note.visibility === 'specified') return false;
|
||||
export async function checkHitAntenna(
|
||||
antenna: Antenna,
|
||||
note: Note | Packed<"Note">,
|
||||
noteUser: { id: User["id"]; username: string; host: string | null },
|
||||
noteUserFollowers?: User["id"][],
|
||||
antennaUserFollowing?: User["id"][],
|
||||
): Promise<boolean> {
|
||||
if (note.visibility === "specified") return false;
|
||||
|
||||
// アンテナ作成者がノート作成者にブロックされていたらスキップ
|
||||
const blockings = await blockingCache.fetch(noteUser.id, () => Blockings.findBy({ blockerId: noteUser.id }).then(res => res.map(x => x.blockeeId)));
|
||||
if (blockings.some(blocking => blocking === antenna.userId)) return false;
|
||||
const blockings = await blockingCache.fetch(noteUser.id, () =>
|
||||
Blockings.findBy({ blockerId: noteUser.id }).then((res) =>
|
||||
res.map((x) => x.blockeeId),
|
||||
),
|
||||
);
|
||||
if (blockings.some((blocking) => blocking === antenna.userId)) return false;
|
||||
|
||||
if (note.visibility === 'followers') {
|
||||
if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId)) return false;
|
||||
if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false;
|
||||
if (note.visibility === "followers") {
|
||||
if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId))
|
||||
return false;
|
||||
if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!antenna.withReplies && note.replyId != null) return false;
|
||||
|
||||
if (antenna.src === 'home') {
|
||||
if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId)) return false;
|
||||
if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false;
|
||||
} else if (antenna.src === 'list') {
|
||||
const listUsers = (await UserListJoinings.findBy({
|
||||
if (antenna.src === "home") {
|
||||
if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId))
|
||||
return false;
|
||||
if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId))
|
||||
return false;
|
||||
} else if (antenna.src === "list") {
|
||||
const listUsers = (
|
||||
await UserListJoinings.findBy({
|
||||
userListId: antenna.userListId!,
|
||||
})).map(x => x.userId);
|
||||
})
|
||||
).map((x) => x.userId);
|
||||
|
||||
if (!listUsers.includes(note.userId)) return false;
|
||||
} else if (antenna.src === 'group') {
|
||||
const joining = await UserGroupJoinings.findOneByOrFail({ id: antenna.userGroupJoiningId! });
|
||||
} else if (antenna.src === "group") {
|
||||
const joining = await UserGroupJoinings.findOneByOrFail({
|
||||
id: antenna.userGroupJoiningId!,
|
||||
});
|
||||
|
||||
const groupUsers = (await UserGroupJoinings.findBy({
|
||||
const groupUsers = (
|
||||
await UserGroupJoinings.findBy({
|
||||
userGroupId: joining.userGroupId,
|
||||
})).map(x => x.userId);
|
||||
})
|
||||
).map((x) => x.userId);
|
||||
|
||||
if (!groupUsers.includes(note.userId)) return false;
|
||||
} else if (antenna.src === 'users') {
|
||||
const accts = antenna.users.map(x => {
|
||||
} else if (antenna.src === "users") {
|
||||
const accts = antenna.users.map((x) => {
|
||||
const { username, host } = Acct.parse(x);
|
||||
return getFullApAccount(username, host).toLowerCase();
|
||||
});
|
||||
if (!accts.includes(getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false;
|
||||
if (
|
||||
!accts.includes(
|
||||
getFullApAccount(noteUser.username, noteUser.host).toLowerCase(),
|
||||
)
|
||||
)
|
||||
return false;
|
||||
}
|
||||
|
||||
const keywords = antenna.keywords
|
||||
// Clean up
|
||||
.map(xs => xs.filter(x => x !== ''))
|
||||
.filter(xs => xs.length > 0);
|
||||
.map((xs) => xs.filter((x) => x !== ""))
|
||||
.filter((xs) => xs.length > 0);
|
||||
|
||||
if (keywords.length > 0) {
|
||||
if (note.text == null) return false;
|
||||
|
||||
const matched = keywords.some(and =>
|
||||
and.every(keyword =>
|
||||
const matched = keywords.some((and) =>
|
||||
and.every((keyword) =>
|
||||
antenna.caseSensitive
|
||||
? note.text!.includes(keyword)
|
||||
: note.text!.toLowerCase().includes(keyword.toLowerCase())
|
||||
));
|
||||
: note.text!.toLowerCase().includes(keyword.toLowerCase()),
|
||||
),
|
||||
);
|
||||
|
||||
if (!matched) return false;
|
||||
}
|
||||
|
||||
const excludeKeywords = antenna.excludeKeywords
|
||||
// Clean up
|
||||
.map(xs => xs.filter(x => x !== ''))
|
||||
.filter(xs => xs.length > 0);
|
||||
.map((xs) => xs.filter((x) => x !== ""))
|
||||
.filter((xs) => xs.length > 0);
|
||||
|
||||
if (excludeKeywords.length > 0) {
|
||||
if (note.text == null) return false;
|
||||
|
||||
const matched = excludeKeywords.some(and =>
|
||||
and.every(keyword =>
|
||||
const matched = excludeKeywords.some((and) =>
|
||||
and.every((keyword) =>
|
||||
antenna.caseSensitive
|
||||
? note.text!.includes(keyword)
|
||||
: note.text!.toLowerCase().includes(keyword.toLowerCase())
|
||||
));
|
||||
: note.text!.toLowerCase().includes(keyword.toLowerCase()),
|
||||
),
|
||||
);
|
||||
|
||||
if (matched) return false;
|
||||
}
|
||||
|
|
|
@ -1,28 +1,32 @@
|
|||
import RE2 from 're2';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import RE2 from "re2";
|
||||
import type { Note } from "@/models/entities/note.js";
|
||||
import type { User } from "@/models/entities/user.js";
|
||||
|
||||
type NoteLike = {
|
||||
userId: Note['userId'];
|
||||
text: Note['text'];
|
||||
userId: Note["userId"];
|
||||
text: Note["text"];
|
||||
};
|
||||
|
||||
type UserLike = {
|
||||
id: User['id'];
|
||||
id: User["id"];
|
||||
};
|
||||
|
||||
export async function checkWordMute(note: NoteLike, me: UserLike | null | undefined, mutedWords: Array<string | string[]>): Promise<boolean> {
|
||||
export async function checkWordMute(
|
||||
note: NoteLike,
|
||||
me: UserLike | null | undefined,
|
||||
mutedWords: Array<string | string[]>,
|
||||
): Promise<boolean> {
|
||||
// 自分自身
|
||||
if (me && (note.userId === me.id)) return false;
|
||||
if (me && note.userId === me.id) return false;
|
||||
|
||||
if (mutedWords.length > 0) {
|
||||
const text = ((note.cw ?? '') + '\n' + (note.text ?? '')).trim();
|
||||
const text = ((note.cw ?? "") + "\n" + (note.text ?? "")).trim();
|
||||
|
||||
if (text === '') return false;
|
||||
if (text === "") return false;
|
||||
|
||||
const matched = mutedWords.some(filter => {
|
||||
const matched = mutedWords.some((filter) => {
|
||||
if (Array.isArray(filter)) {
|
||||
return filter.every(keyword => text.includes(keyword));
|
||||
return filter.every((keyword) => text.includes(keyword));
|
||||
} else {
|
||||
// represents RegExp
|
||||
const regexp = filter.match(/^\/(.+)\/(.*)$/);
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
// structredCloneが遅いため
|
||||
// SEE: http://var.blog.jp/archives/86038606.html
|
||||
|
||||
type Cloneable = string | number | boolean | null | { [key: string]: Cloneable } | Cloneable[];
|
||||
type Cloneable =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| { [key: string]: Cloneable }
|
||||
| Cloneable[];
|
||||
|
||||
export function deepClone<T extends Cloneable>(x: T): T {
|
||||
if (typeof x === 'object') {
|
||||
if (typeof x === "object") {
|
||||
if (x === null) return x;
|
||||
if (Array.isArray(x)) return x.map(deepClone) as T;
|
||||
const obj = {} as Record<string, Cloneable>;
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import cd from 'content-disposition';
|
||||
import cd from "content-disposition";
|
||||
|
||||
export function contentDisposition(type: 'inline' | 'attachment', filename: string): string {
|
||||
const fallback = filename.replace(/[^\w.-]/g, '_');
|
||||
export function contentDisposition(
|
||||
type: "inline" | "attachment",
|
||||
filename: string,
|
||||
): string {
|
||||
const fallback = filename.replace(/[^\w.-]/g, "_");
|
||||
return cd(filename, { type, fallback });
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { URL } from 'node:url';
|
||||
import config from '@/config/index.js';
|
||||
import { toASCII } from 'punycode';
|
||||
import { URL } from "node:url";
|
||||
import config from "@/config/index.js";
|
||||
import { toASCII } from "punycode";
|
||||
|
||||
export function getFullApAccount(username: string, host: string | null) {
|
||||
return host ? `${username}@${toPuny(host)}` : `${username}@${toPuny(config.host)}`;
|
||||
return host
|
||||
? `${username}@${toPuny(host)}`
|
||||
: `${username}@${toPuny(config.host)}`;
|
||||
}
|
||||
|
||||
export function isSelfHost(host: string) {
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
import { Notes } from '@/models/index.js';
|
||||
import { Notes } from "@/models/index.js";
|
||||
|
||||
export async function countSameRenotes(userId: string, renoteId: string, excludeNoteId: string | undefined): Promise<number> {
|
||||
export async function countSameRenotes(
|
||||
userId: string,
|
||||
renoteId: string,
|
||||
excludeNoteId: string | undefined,
|
||||
): Promise<number> {
|
||||
// 指定したユーザーの指定したノートのリノートがいくつあるか数える
|
||||
const query = Notes.createQueryBuilder('note')
|
||||
.where('note.userId = :userId', { userId })
|
||||
.andWhere('note.renoteId = :renoteId', { renoteId });
|
||||
const query = Notes.createQueryBuilder("note")
|
||||
.where("note.userId = :userId", { userId })
|
||||
.andWhere("note.renoteId = :renoteId", { renoteId });
|
||||
|
||||
// 指定した投稿を除く
|
||||
if (excludeNoteId) {
|
||||
query.andWhere('note.id != :excludeNoteId', { excludeNoteId });
|
||||
query.andWhere("note.id != :excludeNoteId", { excludeNoteId });
|
||||
}
|
||||
|
||||
return await query.getCount();
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import * as tmp from 'tmp';
|
||||
import * as tmp from "tmp";
|
||||
|
||||
export function createTemp(): Promise<[string, () => void]> {
|
||||
return new Promise<[string, () => void]>((res, rej) => {
|
||||
|
@ -18,7 +18,7 @@ export function createTempDir(): Promise<[string, () => void]> {
|
|||
(e, path, cleanup) => {
|
||||
if (e) return rej(e);
|
||||
res([path, cleanup]);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { createTemp } from './create-temp.js';
|
||||
import { downloadUrl } from './download-url.js';
|
||||
import { detectType } from './get-file-info.js';
|
||||
import { createTemp } from "./create-temp.js";
|
||||
import { downloadUrl } from "./download-url.js";
|
||||
import { detectType } from "./get-file-info.js";
|
||||
|
||||
export async function detectUrlMime(url: string) {
|
||||
const [path, cleanup] = await createTemp();
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue