Merge branch 'develop'

This commit is contained in:
ThatOneCalculator 2023-02-01 13:10:47 -08:00
commit 5dbc93e6cd
1103 changed files with 49753 additions and 26373 deletions

View file

@ -145,6 +145,12 @@ id: 'aid'
# '127.0.0.1/32' # '127.0.0.1/32'
#] #]
# TWA
#twa:
# nameSpace: android_app
# packageName: tld.domain.twa
# sha256CertFingerprints: ['AB:CD:EF']
# Upload or download file size limits (bytes) # Upload or download file size limits (bytes)
#maxFileSize: 262144000 #maxFileSize: 262144000

View file

@ -14,9 +14,3 @@ redis/
files/ files/
misskey-assets/ misskey-assets/
.pnp.* .pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

16
.gitignore vendored
View file

@ -12,18 +12,6 @@ packages/backend/.idea/vcs.xml
node_modules node_modules
report.*.json report.*.json
# Yarn
yarn.lock
.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
cypress/screenshots cypress/screenshots
cypress/videos cypress/videos
@ -65,3 +53,7 @@ packages/backend/assets/instance.css
*.blend3 *.blend3
*.blend4 *.blend4
*.blend5 *.blend5
# old yarn
.yarn
yarn*

4
.vim/coc-settings.json Normal file
View file

@ -0,0 +1,4 @@
{
"eslint.packageManager": "pnpm",
"workspace.workspaceFolderCheckCwd": false
}

View file

@ -1,9 +1,11 @@
{ {
"recommendations": [ "recommendations": [
"editorconfig.editorconfig", "editorconfig.editorconfig",
"eg2.vscode-npm-script", "eg2.vscode-npm-script",
"dbaeumer.vscode-eslint", "rome.rome",
"Vue.volar", "Vue.volar",
"Vue.vscode-typescript-vue-plugin" "Vue.vscode-typescript-vue-plugin",
] "arcanis.vscode-zipfs",
"Orta.vscode-twoslash-queries"
]
} }

21
.woodpecker/commit.yml Normal file
View file

@ -0,0 +1,21 @@
pipeline:
testCommit:
image: node:latest
commands:
- cp .config/ci.yml .config/default.yml
- corepack enable
- corepack prepare pnpm@latest --activate
- pnpm i --frozen-lockfile
- pnpm run build
- pnpm run migrate
services:
database:
image: postgres:15
environment:
- POSTGRES_PASSWORD=test
redis:
image: redis
branches:
include: [ main, develop, feature/* ]

View file

@ -1,17 +0,0 @@
pipeline:
build:
image: node:${NODE_VERSION}
commands:
- corepack enable
- yarn install
- yarn build
environment:
- YARN_ENABLE_IMMUTABLE_INSTALLS=false
matrix:
NODE_VERSION:
- 18.12.1
- 19.2.0
branches:
include: [ main, develop, feature/* ]

View file

@ -1,28 +0,0 @@
pipeline:
migrate:
image: node:19.2.0
commands:
- cp .config/ci.yml .config/default.yml
- corepack enable
- yarn set version berry
- yarn install
- yarn build
- yarn migrate
environment:
- YARN_ENABLE_IMMUTABLE_INSTALLS=false
services:
database:
image: postgres:${DATABASE}
environment:
- POSTGRES_PASSWORD=test
redis:
image: redis
matrix:
DATABASE:
- 12
- latest
branches:
include: [ main, develop, feature/* ]

View file

@ -16,3 +16,6 @@ pipeline:
# Push new version when version tag is created # Push new version when version tag is created
event: tag event: tag
tag: v* tag: v*
depends_on:
- prSecurityCheck

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,30 +0,0 @@
httpTimeout: 600000
nmHoistingLimits: none
nodeLinker: pnpm
packageExtensions:
"@bull-board/api@*":
peerDependencies:
"@bull-board/ui": "*"
chartjs-adapter-date-fns@*:
peerDependencies:
date-fns: "*"
swiper@*:
peerDependencies:
vue: "*"
consolidate@*:
dependencies:
ejs: "*"
koa-views@*:
dependencies:
pug: "*"
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

View file

@ -41,7 +41,7 @@
## Implemented ## Implemented
- A lot of general bugfixes - A lot of general bugfixes
- Yarn 3 - pnpm instead of yarn
- Fix Dockerfile @hanna - Fix Dockerfile @hanna
- Upgrade packages with security vunrabilities - Upgrade packages with security vunrabilities
- Saner defaults - Saner defaults
@ -101,6 +101,13 @@
- Obliteration of Ai-chan - Obliteration of Ai-chan
- Switch to [Calckey.js](https://codeberg.org/calckey/calckey.js) - Switch to [Calckey.js](https://codeberg.org/calckey/calckey.js)
- Woozy mode 🥴 - Woozy mode 🥴
- 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) - MissV: [fix Misskey Forkbomb](https://code.vtopia.live/Vtopia/MissV/commit/40b23c070bd4adbb3188c73546c6c625138fb3c1)
- [Make showing ads optional](https://github.com/misskey-dev/misskey/pull/8996) - [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) - [Tapping avatar in mobile opens account modal](https://github.com/misskey-dev/misskey/pull/9056)

338
CHANGELOG.md Normal file
View file

@ -0,0 +1,338 @@
# Changelog
All changes from v13.0.0 onwards, for a full list of differences read [CALCKEY.md](./CALCKEY.md)
## [13.0.6-rc] - 2023-01-04
### Bug Fixes
- Prevent notifications if the notification contains a note that is muted
- Fix padding on normal display
- Fix: Cliff design
- Fix: user view z-fighting
- Fix: overlapping user follow button in mobile view
- Fix: Add .js to the end of two type-scripts, fixing a critical error that crashes calckey ([#9347](https://github.com/orhun/git-cliff/issues/9347))
### Features
- New post style
- Add antenna mark read functionality
- Automatic changelog generation using git cliffy
### Miscellaneous Tasks
- Update yarn
- Chore: bump version number
- Chore: upgrade packages
- Chore: up pkgs
- Chore: deprecate `deckDivider`
## [13.0.5] - 2022-12-18
### Bug Fixes
- Fix typo
- Fix-docker-env-path ([#9241](https://github.com/orhun/git-cliff/issues/9241))
- Fix: use correct color for MkMoved
- Fixed additional path to .config
### Documentation
- more badges
- weblate
- Docker-compose-port-fix ([#9251](https://github.com/orhun/git-cliff/issues/9251))
### Features
- weblate
- upgrade to vite 4
### Miscellaneous Tasks
- Update example.yml with container names specified in docker-compose, to support running either a dev or production containers off the same configs
- Chore: lint
- Chore: dockerfile cleanup
- Chore: Update patron list
- Chore: remove unicode fault in KO
- Chore: update gitignore
- Chore: fix rebuild
### Refactor
- Refactor: :busts_in_silhouette: update cleo link
- Refactor: new repo link
### Testing
- Test: 🥴
## [13.0.3] - 2022-12-16
### Bug Fixes
- Fix: 🐛 fix inconsistent theming
- Fix: css class match
- Fix: insert into correct textarea
### Documentation
- Docs: :memo: fix badge position
### Features
- Feat: Insert text at cursor for caption
### Refactor
- Refactor: rm .github folder
## [13.0.0] - 2022-12-16
## [13-rc1] - 2022-12-16
### Bug Fixes
- Fix: messaging pagination
- Fix groups button display
- Fix scroll anim bug
- Fix pinned users list
- Fix: workaround for sticky image container header
- Fix pages swiping
- Fix pages margin
- Fix user profile
- Fix fill out profile step of tutorial
- Fix: :bug: fix image size in dms
- Fix: actually set in-dm to be true if in dm
- Fix: don't do icon transform by default
- Fix problems from #9146
- Fix more icons
- Fix remote move queue
- Fix import
- Fix path
- Fix liked pages
- Fix liked pages endpoint
- Fix remote move queue
- Fix path
- Fix unicode weirdness
- Fix: call functions properly
- Fix viewing basic federaion info
- Fix: migration labels
- Fix ckjs
- Fix locale
- Fix alsoKnownAs federation
- Fix redis in ci
- Fix federation of moved to to pleroma
because it expects it to be non-existant if its null.
- Fix docker ci
### Documentation
- Docs: :memo: deps
- Docs: :memo: typo
- Docs: :memo: latest 18
- Docs: 📝 pm2
- Docs: more accessible links
- Docs: move intro to wip
- Docs: :memo: intro tutorial
- Docs: 📝 tips & tricks
- Docs: fix typo
- Docs: tips
- Docs: :memo: improve documentation, nginx
- Docs: :memo: tip
- Docs: :memo: open port tip
- Docs: 📝 alt text for calc
- Docs: 📝 typo
It's "available". Thank you luke :P
- Docs: 📝 typo
- Docs: 📝 official account
- Docs: another tip
- Docs: 📝 improve install instructions
- Docs: 📝 formatting
- Docs: 📝 optional deps
- Docs: custom locales
- Docs: a11y
- Docs: reflect last change in readme
- Docs: deps
- Docs: 📝 better links
- Docs: 📝 be more descriptive with new techs
- Docs: 📝 scylla will be optional
- Docs: 📝 better links
- Docs: 📝 be more descriptive with new techs
- Docs: 📝 scylla will be optional
- Docs: 📝 account migration
### Features
- Feat: :art: move reaction button
- Feat: :sparkles: Star button
- Feat: :art: add ripple to star react
- Feat: :art: add ripple to star react
- Feat: :sparkles: Toggle showing calckey updates as admin
- Feat: ✨ add `os.yesno` for yes/no questions
- Feat: :lipstick: add right margin to title text
- Feat: :sparkles: Allow importing follows from Pixelfed
- Feat: ✨ Append caption to textarea
- Feat: :sparkles: Managed hosting complete
- Feat: :lipstick: Phosphor icons!
- Feat: :lipstick: Phosphor icons!
- Add effects, japanese translation
- Feat: ✨ Page drafts
- Feat: Docker update script ([#9159](https://github.com/orhun/git-cliff/issues/9159))
- Feat: Docker update script ([#9159](https://github.com/orhun/git-cliff/issues/9159))
- Feat: :sparkles: Add delete all lists
- Add local move follower migration
- Feat: customizable max note length
- Add check for already moved
### Miscellaneous Tasks
- Chore: :package: Update packages
- Update example
- Update deps
- Chore: :package: package upgrades
- Chore: :arrow_up: update deps
- Chore: :arrow_up: upgrade packages
- Chore: :arrow_up: yarn 3.3.0
- Update person model
### Performance
- Perf: :zap: load icons css last
### Refactor
- Refactor: :alembic: try `active-class`
- Refactor: :recycle: Replace all `$ts` with i18n

View file

@ -1,7 +1,10 @@
# Contribution guide # Contribution guide
We're glad you're interested in contributing Calckey! In this document you will find the information you need to contribute to the project. We're glad you're interested in contributing Calckey! In this document you will find the information you need to contribute to the project.
# Translations ## Localization (l10n)
Calckey uses [Weblate](hhttps://hosted.weblate.org/engage/calckey/) for localization management.
If your language is not listed in Weblate, please open an issue.
You can contribute without knowing how to code by helping translate here: You can contribute without knowing how to code by helping translate here:
@ -10,14 +13,14 @@ You can contribute without knowing how to code by helping translate here:
[![Translation bars](https://hosted.weblate.org/widgets/calckey/-/multi-auto.svg)](https://hosted.weblate.org/engage/calckey/) [![Translation bars](https://hosted.weblate.org/widgets/calckey/-/multi-auto.svg)](https://hosted.weblate.org/engage/calckey/)
## Roadmap ## Roadmap
See [ROADMAP.md](./ROADMAP.md) See [CALCKEY.md](./CALCKEY.md)
## Issues ## Issues
Before creating an issue, please check the following: Before creating an issue, please check the following:
- To avoid duplication, please search for similar issues before creating a new issue. - To avoid duplication, please search for similar issues before creating a new issue.
- Do not use Issues to ask questions or troubleshooting. - Do not use Issues to ask questions or troubleshooting.
- Issues should only be used to feature requests, suggestions, and bug tracking. - Issues should only be used to feature requests, suggestions, and bug tracking.
- Please ask questions or troubleshooting in the [Misskey Forum](https://forum.misskey.io/) or [Discord](https://discord.gg/Wp8gVStHW3). - Please ask questions or troubleshooting in the [Matrix room](https://matrix.to/#/#calckey:matrix.fedibird.com).
> **Warning** > **Warning**
> Do not close issues that are about to be resolved. It should remain open until a commit that actually resolves it is merged. > Do not close issues that are about to be resolved. It should remain open until a commit that actually resolves it is merged.
@ -31,22 +34,22 @@ PRs that do not have a clear set of do's and don'ts tend to be bloated and diffi
Also, when you start implementation, assign yourself to the Issue (if you cannot do it yourself, ask another member to assign you). By expressing your intention to work the Issue, you can prevent conflicts in the work. Also, when you start implementation, assign yourself to the Issue (if you cannot do it yourself, ask another member to assign you). By expressing your intention to work the Issue, you can prevent conflicts in the work.
## Well-known branches ## Well-known branches
- **`master`** branch is tracking the latest release and used for production purposes. - The **`main`** branch is tracking the latest release and used for production purposes.
- **`develop`** branch is where we work for the next release. - The **`develop`** branch is where we work for the next release.
- When you create a PR, basically target it to this branch. - When you create a PR, basically target it to this branch. **But create a different branch**
- **`l10n_develop`** branch is reserved for localization management. - The **`l10n_develop`** branch is reserved for localization management.
- **`feature/*`** branches are reserved for the development of a specific feature
## Creating a PR ## Creating a PR
Thank you for your PR! Before creating a PR, please check the following: Thank you for your PR! Before creating a PR, please check the following:
- If possible, prefix the title with a keyword that identifies the type of this PR, as shown below. - If possible, prefix the title with a keyword that identifies the type of this PR, as shown below.
- `fix` / `refactor` / `feat` / `enhance` / `perf` / `chore` etc - `fix` / `refactor` / `feat` / `enhance` / `perf` / `chore` etc. You are also welcome to use gitmoji. This is important as we use these to A) easier read the git history and B) generate our changelog. Without propper prefixing it is possible that your PR is rejected.
- Also, make sure that the granularity of this PR is appropriate. Please do not include more than one type of change or interest in a single PR. - Also, make sure that the granularity of this PR is appropriate. Please do not include more than one type of change or interest in a single PR.
- If there is an Issue which will be resolved by this PR, please include a reference to the Issue in the text. - If there is an Issue which will be resolved by this PR, please include a reference to the Issue in the text. Good examples include `Closing: #21` or `Resolves: #21`
- Please add the summary of the changes to [`CHANGELOG.md`](/CHANGELOG.md). However, this is not necessary for changes that do not affect the users, such as refactoring.
- Check if there are any documents that need to be created or updated due to this change. - 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. - 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. - 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. - If this PR includes UI changes, please attach a screenshot in the text.
Thanks for your cooperation 🤗 Thanks for your cooperation 🤗
@ -68,7 +71,7 @@ Be willing to comment on the good points and not just the things you want fixed
- Are there any omissions or gaps? - Are there any omissions or gaps?
- Does it check for anomalies? - Does it check for anomalies?
## Deploy ## Deploy (SOON)
The `/deploy` command by issue comment can be used to deploy the contents of a PR to the preview environment. The `/deploy` command by issue comment can be used to deploy the contents of a PR to the preview environment.
``` ```
/deploy sha=<commit hash> /deploy sha=<commit hash>
@ -90,21 +93,14 @@ An actual domain will be assigned so you can test the federation.
- The target branch must be `master` - The target branch must be `master`
- The tag name must be the version - The tag name must be the version
## Localization (l10n)
Misskey uses [Crowdin](https://crowdin.com/project/misskey) for localization management.
You can improve our translations with your Crowdin account.
Your changes in Crowdin are automatically submitted as a PR (with the title "New Crowdin translations") to the repository.
The owner [@syuilo](https://github.com/syuilo) merges the PR into the develop branch before the next release.
If your language is not listed in Crowdin, please open an issue.
![Crowdin](https://d322cqt584bo4o.cloudfront.net/misskey/localized.svg)
## Development ## Development
During development, it is useful to use the `yarn dev` command. During development, it is useful to use the `yarn dev` command.
This command monitors the server-side and client-side source files and automatically builds them if they are modified. This command monitors the server-side and client-side source files and automatically builds them if they are modified.
In addition, it will also automatically start the Misskey server process. In addition, it will also automatically start the Misskey server process.
# THE FOLLOWING IS OUTDATED:
## Testing ## Testing
- Test codes are located in [`/test`](/test). - Test codes are located in [`/test`](/test).
@ -259,7 +255,7 @@ MongoDBは`null`で返してきてたので、その感覚で`if (x === null)`
### Migration作成方法 ### Migration作成方法
packages/backendで: packages/backendで:
```sh ```sh
yarn dlx typeorm migration:generate -d ormconfig.js -o <migration name> pnpm dlx typeorm migration:generate -d ormconfig.js -o <migration name>
``` ```
- 生成後、ファイルをmigration下に移してください - 生成後、ファイルをmigration下に移してください

View file

@ -1,5 +1,4 @@
FROM node:18-alpine FROM node:19-alpine
ENV YARN_CHECKSUM_BEHAVIOR=update
ARG NODE_ENV=production ARG NODE_ENV=production
WORKDIR /calckey WORKDIR /calckey
@ -10,17 +9,17 @@ COPY . ./
RUN apk update RUN apk update
RUN apk add git ffmpeg tini alpine-sdk python3 RUN apk add git ffmpeg tini alpine-sdk python3
# Configure corepack and yarn # Configure corepack and pnpm
RUN corepack enable RUN corepack enable
RUN yarn set version berry RUN corepack prepare pnpm@latest --activate
RUN yarn plugin import workspace-tools RUN pnpm i --frozen-lockfile
ARG NODE_ENV=production
# Install Dependencies # Build project (pnp dependencies are installed)
RUN yarn install RUN pnpm run build
RUN yarn run build
# Remove git files # Remove git files
RUN rm -rf .git RUN rm -rf .git
ENTRYPOINT [ "/sbin/tini", "--" ] ENTRYPOINT [ "/sbin/tini", "--" ]
CMD [ "yarn", "run", "migrateandstart" ] CMD [ "pnpm", "run", "migrateandstart" ]

View file

@ -5,11 +5,13 @@
**🌎 **[Calckey](https://i.calckey.cloud/)** is an open source, decentralized social media platform that's free forever! 🚀** **🌎 **[Calckey](https://i.calckey.cloud/)** is an open source, decentralized social media platform that's free forever! 🚀**
[![status-badge](https://ci.codeberg.org/api/badges/calckey/calckey/status.svg)](https://ci.codeberg.org/calckey/calckey) [![no github badge](https://nogithub.codeberg.page/badge.svg)](https://nogithub.codeberg.page/)
[![liberapay-badge](https://img.shields.io/liberapay/receives/ThatOneCalculator?logo=liberapay)](https://liberapay.com/ThatOneCalculator) [![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)
[![translate-badge](https://hosted.weblate.org/widgets/calckey/-/svg-badge.svg)](https://hosted.weblate.org/engage/calckey/) [![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)
[![codeberg-badge](https://custom-icon-badges.demolab.com/badge/hosted%20on-codeberg-blue.svg?logo=codeberg&logoColor=white)](https://codeberg.org/calckey/calckey/) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](./CODE_OF_CONDUCT.md)
[![lavaforge badge](https://custom-icon-badges.demolab.com/badge/hosted%20on-lavaforge-FF8066.svg?logo=lavaforge&logoColor=white)](https://codeberg.org/calckey/calckey/)
</div> </div>
@ -50,12 +52,23 @@
- 📖 JoinFediverse Wiki: <https://joinfediverse.wiki/What_is_Calckey%3F> - 📖 JoinFediverse Wiki: <https://joinfediverse.wiki/What_is_Calckey%3F>
- 🐋 Docker Hub: <https://hub.docker.com/r/thatonecalculator/calckey> - 🐋 Docker Hub: <https://hub.docker.com/r/thatonecalculator/calckey>
- ✍️ Weblate: <https://hosted.weblate.org/engage/calckey/> - ✍️ Weblate: <https://hosted.weblate.org/engage/calckey/>
- 📦 Yunohost: <https://github.com/YunoHost-Apps/calckey_ynh>
# 🌠 Getting started # 🌠 Getting started
This guide will work for both **starting from scratch** and **migrating from Misskey**. This guide will work for both **starting from scratch** and **migrating from Misskey**.
## 📦 Dependencies ## 🔰 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)
### 🐋 Docker
[How to run Calckey with Docker](./docker-README.md).
## 🧑‍💻 Dependencies
- 🐢 At least [NodeJS](https://nodejs.org/en/) v18.12.1 (v19 recommended) - 🐢 At least [NodeJS](https://nodejs.org/en/) v18.12.1 (v19 recommended)
- Install with [nvm](https://github.com/nvm-sh/nvm) - Install with [nvm](https://github.com/nvm-sh/nvm)
@ -82,7 +95,7 @@ This guide will work for both **starting from scratch** and **migrating from Mis
## 👀 Get folder ready ## 👀 Get folder ready
```sh ```sh
git clone https://codeberg.org/calckey/calckey.git git clone --depth 1 https://codeberg.org/calckey/calckey.git
cd calckey/ cd calckey/
# git checkout main # if you want only stable versions # git checkout main # if you want only stable versions
``` ```
@ -90,8 +103,11 @@ cd calckey/
## 📩 Install dependencies ## 📩 Install dependencies
```sh ```sh
# nvm install 18 && nvm alias default 18 && nvm use 18 # nvm install 19 && nvm use 19
corepack enable corepack enable
corepack prepare pnpm@latest --activate
# To build without TensorFlow, append --no-optional
pnpm i # --no-optional
``` ```
## 🐘 Create database ## 🐘 Create database
@ -107,7 +123,7 @@ psql postgres -c "create database calckey with encoding = 'UTF8';"
- To add custom CSS for all users, edit `./custom/assets/instance.css`. - 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 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 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 ## 🧑‍🔬 Configuring a new instance
@ -121,7 +137,7 @@ psql postgres -c "create database calckey with encoding = 'UTF8';"
```sh ```sh
cp ../misskey/.config/default.yml ./.config/default.yml # replace `../misskey/` with misskey path, add `docker.env` if you use Docker 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 ## 🍀 NGINX
@ -141,22 +157,17 @@ cp -r ../misskey/files . # if you don't use object storage
```sh ```sh
# git pull # git pull
yarn install NODE_ENV=production pnpm install && pnpm run build && pnpm run migrate
NODE_ENV=production yarn run rebuild && yarn run migrate pm2 start "NODE_ENV=production pnpm run start" --name Calckey
pm2 start "NODE_ENV=production yarn start" --name Calckey
``` ```
### 🐋 Docker
[How to run Calckey with Docker](./docker-README.md).
## 😉 Tips & Tricks ## 😉 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. - 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 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. - 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. - 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: - To add another admin account:
- Go to the user's page > 3 Dots > About > Moderation > turn on "Moderator" - Go to the user's page > 3 Dots > About > Moderation > turn on "Moderator"

103
cliff.toml Normal file
View file

@ -0,0 +1,103 @@
# configuration file for git-cliff (0.1.0)
[changelog]
# changelog header
header = """
# Changelog\n
All changes from v13.0.0 onwards, for a full list of differences read CALCKEY.md\n
"""
# template for the changelog body
# https://tera.netlify.app/docs/#introduction
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
- {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\
{% endfor %}
{% endfor %}\n
"""
# remove the leading and trailing whitespace from the template
trim = true
# changelog footer
footer = """
<!-- generated by git-cliff -->
"""
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = false
# filter out the commits that are not conventional
filter_unconventional = true
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/orhun/git-cliff/issues/${2}))"},
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "Features"},
{ message = "^add", group = "Features"},
{ message = "^fix", group = "Bug Fixes"},
{ message = "^prevent", group = "Bug Fixes"},
{ message = "^doc", group = "Documentation"},
{ message = "^perf", group = "Performance"},
{ message = "^🎨", group = "Refactor"},
{ message = "^enhance", group = "Refactor"},
{ message = "^⚡️", group = "Refactor"},
{ message = "^🔥", group = "Features"},
{ message = "^🐛", group = "Bug Fixes"},
{ message = "^🚑️", group = "Bug Fixes"},
{ message = "^block", group = "Bug Fixes"},
{ message = "^✨", group = "Features"},
{ message = "^📝", group = "Documentation"},
{ message = "^🚀", group = "Features"},
{ message = "^💄", group = "Styling"},
{ message = "^✅", group = "Testing"},
{ message = "^🔒️", group = "Security"},
{ message = "^🚨", group = "Testing"},
{ message = "^💚", group = "CI"},
{ message = "^👷", group = "CI"},
{ message = "^⬇️", group = "Miscellaneous Tasks"},
{ message = "^⬆️", group = "Miscellaneous Tasks"},
{ message = "^📌", group = "Miscellaneous Tasks"},
{ message = "^", group = "Miscellaneous Tasks"},
{ message = "^", group = "Miscellaneous Tasks"},
{ message = "^♻️", group = "Refactor"},
{ message = "^🔧", group = "CI"},
{ message = "^🔨", group = "CI"},
{ message = "^🌐", group = "Localization"},
{ message = "^✏️", group = "Localization"},
{ message = "^👽️", group = "Bug Fixes"},
{ message = "^🍱", group = "Styling"},
{ message = "^♿️", group = "Styling"},
{ message = "^🩹", group = "Bug Fixes"},
{ message = "^refactor", group = "Refactor"},
{ message = "^style", group = "Styling"},
{ message = "^test", group = "Testing"},
{ message = "^chore\\(release\\): prepare for", skip = true},
{ message = "^chore", group = "Miscellaneous Tasks"},
{ message = "^update", group = "Miscellaneous Tasks"},
{ body = ".*security", group = "Security"},
]
# protect breaking changes from being skipped due to matching a skipping commit_parser
protect_breaking_commits = false
# filter out the commits that are not matched by commit parsers
filter_commits = false
# glob pattern for matching git tags
tag_pattern = "v[0-9]*"
# regex for skipping tags
skip_tags = "v0.1.0-beta.1"
# regex for ignoring tags
ignore_tags = ""
# sort the tags chronologically
date_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"
# limit the number of commits included in the changelog.
# limit_commits = 42

View file

@ -15,7 +15,6 @@
/** /**
* @type {Cypress.PluginConfig} * @type {Cypress.PluginConfig}
*/ */
// eslint-disable-next-line no-unused-vars
module.exports = (on, config) => { module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits // `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config // `config` is the resolved Cypress config

View file

@ -30,7 +30,7 @@ services:
db: db:
restart: always restart: always
image: docker.io/postgres:13.9-alpine image: docker.io/postgres:12.2-alpine
container_name: calckey_db container_name: calckey_db
networks: networks:
- network - network

View file

@ -42,6 +42,6 @@ Once the instance is up you can use a web browser to access the web interface at
```sh ```sh
cd dev/ cd dev/
docker-compose build docker-compose build
docker-compose run --rm web yarn run init docker-compose run --rm web pnpm run init
docker-compose up -d docker-compose up -d
``` ```

View file

@ -31,7 +31,7 @@ services:
db: db:
restart: unless-stopped restart: unless-stopped
image: docker.io/postgres:13.9-alpine image: docker.io/postgres:12.2-alpine
container_name: calckey_db container_name: calckey_db
networks: networks:
- calcnet - calcnet

70
issue_template/bug.yaml Normal file
View 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://codeberg.org/calckey/calckey/src/branch/develop/CONTRIBUTING.md)
options:
- label: I agree to follow this project's Contribution Guidelines
required: true

View 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://codeberg.org/calckey/calckey/src/branch/develop/CONTRIBUTING.md)
options:
- label: I agree to follow this project's Contribution Guidelines
required: true

View file

@ -556,7 +556,6 @@ tokenRequested: "منح حق الوصول إلى الحساب"
pluginTokenRequestedDescription: "ستتمكن الإضافة من استخدام هذه الأذونات." pluginTokenRequestedDescription: "ستتمكن الإضافة من استخدام هذه الأذونات."
notificationType: "أنواع الإشعارات" notificationType: "أنواع الإشعارات"
edit: "التعديل" edit: "التعديل"
useStarForReactionFallback: "استخدم ★ كبديل إذا كان التفاعل مجهولًا"
emailServer: "خادم البريد الإلكتروني" emailServer: "خادم البريد الإلكتروني"
emailConfigInfo: "يستخدم لتأكيد عنوان بريدك الإلكتروني ولإعادة تعيين كلمة المرور إن نسيتها." emailConfigInfo: "يستخدم لتأكيد عنوان بريدك الإلكتروني ولإعادة تعيين كلمة المرور إن نسيتها."
email: "البريد الإلكتروني " email: "البريد الإلكتروني "

View file

@ -577,7 +577,6 @@ tokenRequested: "অ্যাকাউন্টে অ্যাক্সেস
pluginTokenRequestedDescription: "এই প্লাগইনটি এখানে দেওয়া অনুমুতিসমূহ ব্যাবহার করবে" pluginTokenRequestedDescription: "এই প্লাগইনটি এখানে দেওয়া অনুমুতিসমূহ ব্যাবহার করবে"
notificationType: "বিজ্ঞপ্তির ধরন" notificationType: "বিজ্ঞপ্তির ধরন"
edit: "সম্পাদনা" edit: "সম্পাদনা"
useStarForReactionFallback: "রিঅ্যাকশনের ইমোজি না জানলে ★ ব্যবহার করুন"
emailServer: "ইমেইল সার্ভার" emailServer: "ইমেইল সার্ভার"
enableEmail: "ইমেইল বিতরণ চালু করুন" enableEmail: "ইমেইল বিতরণ চালু করুন"
emailConfigInfo: "আপনার ইমেল ঠিকানা নিশ্চিত করতে এবং আপনার পাসওয়ার্ড পুনরায় সেট করতে ব্যবহৃত হয়" emailConfigInfo: "আপনার ইমেল ঠিকানা নিশ্চিত করতে এবং আপনার পাসওয়ার্ড পুনরায় সেট করতে ব্যবহৃত হয়"

View file

@ -581,7 +581,6 @@ tokenRequested: "Zugriff zum Benutzerkonto gewähren"
pluginTokenRequestedDescription: "Dieses Plugin wird die hier konfigurierten Berechtigungen verwenden können." pluginTokenRequestedDescription: "Dieses Plugin wird die hier konfigurierten Berechtigungen verwenden können."
notificationType: "Art der Benachrichtigung" notificationType: "Art der Benachrichtigung"
edit: "Bearbeiten" edit: "Bearbeiten"
useStarForReactionFallback: "Verwende ★ falls das Reaktions-Emoji unbekannt ist"
emailServer: "Email-Server" emailServer: "Email-Server"
enableEmail: "Email-Versand aktivieren" enableEmail: "Email-Versand aktivieren"
emailConfigInfo: "Zur Email-Bestätigung bei Registrierung oder zum Zurücksetzen des Passworts verwendet" emailConfigInfo: "Zur Email-Bestätigung bei Registrierung oder zum Zurücksetzen des Passworts verwendet"

408
locales/el-GR.yml Normal file
View 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: "Επισημάνσεις"

View file

@ -13,8 +13,8 @@ ok: "OK"
gotIt: "Got it!" gotIt: "Got it!"
cancel: "Cancel" cancel: "Cancel"
enterUsername: "Enter username" enterUsername: "Enter username"
renotedBy: "Renoted by {user}" renotedBy: "Boosted by {user}"
noNotes: "No notes" noNotes: "No posts"
noNotifications: "No notifications" noNotifications: "No notifications"
instance: "Instance" instance: "Instance"
settings: "Settings" settings: "Settings"
@ -44,7 +44,7 @@ copyContent: "Copy contents"
copyLink: "Copy link" copyLink: "Copy link"
delete: "Delete" delete: "Delete"
deleteAndEdit: "Delete and edit" deleteAndEdit: "Delete and edit"
deleteAndEditConfirm: "Are you sure you want to delete this note and edit it? You will lose all reactions, renotes and replies to it." deleteAndEditConfirm: "Are you sure you want to delete this post and edit it? You will lose all reactions, boosts and replies to it."
addToList: "Add to list" addToList: "Add to list"
sendMessage: "Send a message" sendMessage: "Send a message"
copyUsername: "Copy username" copyUsername: "Copy username"
@ -58,20 +58,20 @@ receiveFollowRequest: "Follow request received"
followRequestAccepted: "Follow request accepted" followRequestAccepted: "Follow request accepted"
mention: "Mention" mention: "Mention"
mentions: "Mentions" mentions: "Mentions"
directNotes: "Direct notes" directNotes: "Direct messages"
importAndExport: "Import/Export Data" importAndExport: "Import/Export Data"
import: "Import" import: "Import"
export: "Export" export: "Export"
files: "Files" files: "Files"
download: "Download" download: "Download"
driveFileDeleteConfirm: "Are you sure you want to delete the file \"{name}\"? Notes with this file attached will also be deleted." driveFileDeleteConfirm: "Are you sure you want to delete the file \"{name}\"? Posts with this file attached will also be deleted."
unfollowConfirm: "Are you sure that you want to unfollow {name}?" unfollowConfirm: "Are you sure that you want to unfollow {name}?"
exportRequested: "You've requested an export. This may take a while. It will be added to your Drive once completed." exportRequested: "You've requested an export. This may take a while. It will be added to your Drive once completed."
importRequested: "You've requested an import. This may take a while." importRequested: "You've requested an import. This may take a while."
lists: "Lists" lists: "Lists"
noLists: "You don't have any lists" noLists: "You don't have any lists"
note: "Note" note: "Post"
notes: "Notes" notes: "Posts"
following: "Following" following: "Following"
followers: "Followers" followers: "Followers"
followsYou: "Follows you" followsYou: "Follows you"
@ -94,13 +94,13 @@ followRequests: "Follow requests"
unfollow: "Unfollow" unfollow: "Unfollow"
followRequestPending: "Follow request pending" followRequestPending: "Follow request pending"
enterEmoji: "Enter an emoji" enterEmoji: "Enter an emoji"
renote: "Renote" renote: "Boost"
unrenote: "Take back renote" unrenote: "Take back boost"
renoted: "Renoted." renoted: "Boosted."
cantRenote: "This post can't be renoted." cantRenote: "This post can't be boosted."
cantReRenote: "A renote can't be renoted." cantReRenote: "A boost can't be boosted."
quote: "Quote" quote: "Quote"
pinnedNote: "Pinned note" pinnedNote: "Pinned post"
pinned: "Pin to profile" pinned: "Pin to profile"
you: "You" you: "You"
clickToShow: "Click to show" clickToShow: "Click to show"
@ -109,7 +109,7 @@ add: "Add"
reaction: "Reactions" reaction: "Reactions"
reactionSetting: "Reactions to show in the reaction picker" reactionSetting: "Reactions to show in the reaction picker"
reactionSettingDescription2: "Drag to reorder, click to delete, press \"+\" to add." reactionSettingDescription2: "Drag to reorder, click to delete, press \"+\" to add."
rememberNoteVisibility: "Remember note visibility settings" rememberNoteVisibility: "Remember post visibility settings"
attachCancel: "Remove attachment" attachCancel: "Remove attachment"
markAsSensitive: "Mark as NSFW" markAsSensitive: "Mark as NSFW"
unmarkAsSensitive: "Unmark as NSFW" unmarkAsSensitive: "Unmark as NSFW"
@ -143,7 +143,7 @@ flagAsBotDescription: "Enable this option if this account is controlled by a pro
flagAsCat: "Are you a cat? 😺" flagAsCat: "Are you a cat? 😺"
flagAsCatDescription: "You'll get cat ears and speak like a cat!" flagAsCatDescription: "You'll get cat ears and speak like a cat!"
flagShowTimelineReplies: "Show replies in timeline" flagShowTimelineReplies: "Show replies in timeline"
flagShowTimelineRepliesDescription: "Shows replies of users to notes of other users in the timeline if turned on." flagShowTimelineRepliesDescription: "Shows replies of users to posts of other users in the timeline if turned on."
autoAcceptFollowed: "Automatically approve follow requests from users you're following" autoAcceptFollowed: "Automatically approve follow requests from users you're following"
addAccount: "Add account" addAccount: "Add account"
loginFailed: "Failed to sign in" loginFailed: "Failed to sign in"
@ -188,7 +188,7 @@ instanceInfo: "Instance Information"
statistics: "Statistics" statistics: "Statistics"
clearQueue: "Clear queue" clearQueue: "Clear queue"
clearQueueConfirmTitle: "Are you sure that you want to clear the queue?" clearQueueConfirmTitle: "Are you sure that you want to clear the queue?"
clearQueueConfirmText: "Any undelivered notes remaining in the queue will not be federated. Usually this operation is not needed." clearQueueConfirmText: "Any undelivered posts remaining in the queue will not be federated. Usually this operation is not needed."
clearCachedFiles: "Clear cache" clearCachedFiles: "Clear cache"
clearCachedFilesConfirm: "Are you sure that you want to delete all cached remote files?" clearCachedFilesConfirm: "Are you sure that you want to delete all cached remote files?"
blockedInstances: "Blocked Instances" blockedInstances: "Blocked Instances"
@ -198,8 +198,8 @@ mutedUsers: "Muted users"
blockedUsers: "Blocked users" blockedUsers: "Blocked users"
noUsers: "There are no users" noUsers: "There are no users"
editProfile: "Edit profile" editProfile: "Edit profile"
noteDeleteConfirm: "Are you sure you want to delete this note?" noteDeleteConfirm: "Are you sure you want to delete this post?"
pinLimitExceeded: "You cannot pin any more notes" pinLimitExceeded: "You cannot pin any more posts"
intro: "Installation of Calckey has been finished! Please create an admin user." intro: "Installation of Calckey has been finished! Please create an admin user."
done: "Done" done: "Done"
processing: "Processing..." processing: "Processing..."
@ -342,7 +342,7 @@ pinnedUsersDescription: "List usernames separated by line breaks to be pinned in
pinnedPages: "Pinned Pages" pinnedPages: "Pinned Pages"
pinnedPagesDescription: "Enter the paths of the Pages you want to pin to the top page of this instance, separated by line breaks." pinnedPagesDescription: "Enter the paths of the Pages you want to pin to the top page of this instance, separated by line breaks."
pinnedClipId: "ID of the clip to pin" pinnedClipId: "ID of the clip to pin"
pinnedNotes: "Pinned notes" pinnedNotes: "Pinned posts"
hcaptcha: "hCaptcha" hcaptcha: "hCaptcha"
enableHcaptcha: "Enable hCaptcha" enableHcaptcha: "Enable hCaptcha"
hcaptchaSiteKey: "Site key" hcaptchaSiteKey: "Site key"
@ -359,14 +359,14 @@ antennaSource: "Antenna source"
antennaKeywords: "Keywords to listen to" antennaKeywords: "Keywords to listen to"
antennaExcludeKeywords: "Keywords to exclude" antennaExcludeKeywords: "Keywords to exclude"
antennaKeywordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition." antennaKeywordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition."
notifyAntenna: "Notify about new notes" notifyAntenna: "Notify about new posts"
withFileAntenna: "Only notes with files" withFileAntenna: "Only posts with files"
enableServiceworker: "Enable Push-Notifications for your Browser" enableServiceworker: "Enable Push-Notifications for your Browser"
antennaUsersDescription: "List one username per line" antennaUsersDescription: "List one username per line"
caseSensitive: "Case sensitive" caseSensitive: "Case sensitive"
withReplies: "Include replies" withReplies: "Include replies"
connectedTo: "Following account(s) are connected" connectedTo: "Following account(s) are connected"
notesAndReplies: "Notes and replies" notesAndReplies: "Posts and replies"
withFiles: "Including files" withFiles: "Including files"
silence: "Silence" silence: "Silence"
silenceConfirm: "Are you sure that you want to silence this user?" silenceConfirm: "Are you sure that you want to silence this user?"
@ -403,7 +403,7 @@ notFoundDescription: "No page corresponding to this URL could be found."
uploadFolder: "Default folder for uploads" uploadFolder: "Default folder for uploads"
cacheClear: "Clear cache" cacheClear: "Clear cache"
markAsReadAllNotifications: "Mark all notifications as read" markAsReadAllNotifications: "Mark all notifications as read"
markAsReadAllUnreadNotes: "Mark all notes as read" markAsReadAllUnreadNotes: "Mark all posts as read"
markAsReadAllTalkMessages: "Mark all messages as read" markAsReadAllTalkMessages: "Mark all messages as read"
help: "Help" help: "Help"
inputMessageHere: "Enter message here" inputMessageHere: "Enter message here"
@ -424,7 +424,7 @@ text: "Text"
enable: "Enable" enable: "Enable"
next: "Next" next: "Next"
retype: "Enter again" retype: "Enter again"
noteOf: "Note by {user}" noteOf: "Post by {user}"
inviteToGroup: "Invite to group" inviteToGroup: "Invite to group"
quoteAttached: "Quote" quoteAttached: "Quote"
quoteQuestion: "Append as quote?" quoteQuestion: "Append as quote?"
@ -482,8 +482,8 @@ accountSettings: "Account Settings"
promotion: "Promoted" promotion: "Promoted"
promote: "Promote" promote: "Promote"
numberOfDays: "Number of days" numberOfDays: "Number of days"
hideThisNote: "Hide this note" hideThisNote: "Hide this post"
showFeaturedNotesInTimeline: "Show featured notes in timelines" showFeaturedNotesInTimeline: "Show featured posts in timelines"
objectStorage: "Object Storage" objectStorage: "Object Storage"
useObjectStorage: "Use object storage" useObjectStorage: "Use object storage"
objectStorageBaseUrl: "Base URL" objectStorageBaseUrl: "Base URL"
@ -504,7 +504,7 @@ objectStorageSetPublicRead: "Set \"public-read\" on upload"
serverLogs: "Server logs" serverLogs: "Server logs"
deleteAll: "Delete all" deleteAll: "Delete all"
showFixedPostForm: "Display the posting form at the top of the timeline" showFixedPostForm: "Display the posting form at the top of the timeline"
newNoteRecived: "There are new notes" newNoteRecived: "There are new posts"
sounds: "Sounds" sounds: "Sounds"
listen: "Listen" listen: "Listen"
none: "None" none: "None"
@ -548,8 +548,8 @@ addRelay: "Add Relay"
inboxUrl: "Inbox URL" inboxUrl: "Inbox URL"
addedRelays: "Added Relays" addedRelays: "Added Relays"
serviceworkerInfo: "Must be enabled for push notifications." serviceworkerInfo: "Must be enabled for push notifications."
deletedNote: "Deleted note" deletedNote: "Deleted post"
invisibleNote: "Invisible note" invisibleNote: "Invisible post"
enableInfiniteScroll: "Automatically load more" enableInfiniteScroll: "Automatically load more"
visibility: "Visiblility" visibility: "Visiblility"
poll: "Poll" poll: "Poll"
@ -583,7 +583,6 @@ tokenRequested: "Grant access to account"
pluginTokenRequestedDescription: "This plugin will be able to use the permissions set here." pluginTokenRequestedDescription: "This plugin will be able to use the permissions set here."
notificationType: "Notification type" notificationType: "Notification type"
edit: "Edit" edit: "Edit"
useStarForReactionFallback: "Use ★ as fallback if the reaction emoji is unknown"
emailServer: "Email server" emailServer: "Email server"
enableEmail: "Enable email distribution" enableEmail: "Enable email distribution"
emailConfigInfo: "Used to confirm your email during sign-up or if you forget your password" emailConfigInfo: "Used to confirm your email during sign-up or if you forget your password"
@ -627,7 +626,7 @@ sample: "Sample"
abuseReports: "Reports" abuseReports: "Reports"
reportAbuse: "Report" reportAbuse: "Report"
reportAbuseOf: "Report {name}" reportAbuseOf: "Report {name}"
fillAbuseReportDescription: "Please fill in details regarding this report. If it is about a specific note, please include its URL." fillAbuseReportDescription: "Please fill in details regarding this report. If it is about a specific post, please include its URL."
abuseReported: "Your report has been sent. Thank you very much." abuseReported: "Your report has been sent. Thank you very much."
reporter: "Reporter" reporter: "Reporter"
reporteeOrigin: "Reportee Origin" reporteeOrigin: "Reportee Origin"
@ -640,27 +639,27 @@ openInNewTab: "Open in new tab"
openInSideView: "Open in side view" openInSideView: "Open in side view"
defaultNavigationBehaviour: "Default navigation behavior" defaultNavigationBehaviour: "Default navigation behavior"
editTheseSettingsMayBreakAccount: "Editing these settings may damage your account." editTheseSettingsMayBreakAccount: "Editing these settings may damage your account."
instanceTicker: "Instance information of notes" instanceTicker: "Instance information of posts"
waitingFor: "Waiting for {x}" waitingFor: "Waiting for {x}"
random: "Random" random: "Random"
system: "System" system: "System"
switchUi: "Switch UI" switchUi: "Layout"
desktop: "Desktop" desktop: "Desktop"
clip: "Clip" clip: "Clip"
createNew: "Create new" createNew: "Create new"
optional: "Optional" optional: "Optional"
createNewClip: "Create new clip" createNewClip: "Create new clip"
unclip: "Unclip" unclip: "Unclip"
confirmToUnclipAlreadyClippedNote: "This note is already part of the \"{name}\" clip. Do you want to remove it from this clip instead?" confirmToUnclipAlreadyClippedNote: "This post is already part of the \"{name}\" clip. Do you want to remove it from this clip instead?"
public: "Public" public: "Public"
i18nInfo: "Calckey is being translated into various languages by volunteers. You can help at {link}." i18nInfo: "Calckey is being translated into various languages by volunteers. You can help at {link}."
manageAccessTokens: "Manage access tokens" manageAccessTokens: "Manage access tokens"
accountInfo: "Account Info" accountInfo: "Account Info"
notesCount: "Number of notes" notesCount: "Number of posts"
repliesCount: "Number of replies sent" repliesCount: "Number of replies sent"
renotesCount: "Number of renotes sent" renotesCount: "Number of boosts sent"
repliedCount: "Number of replies received" repliedCount: "Number of replies received"
renotedCount: "Number of renotes received" renotedCount: "Number of boosts received"
followingCount: "Number of followed accounts" followingCount: "Number of followed accounts"
followersCount: "Number of followers" followersCount: "Number of followers"
sentReactionsCount: "Number of sent reactions" sentReactionsCount: "Number of sent reactions"
@ -672,15 +671,15 @@ no: "No"
driveFilesCount: "Number of Drive files" driveFilesCount: "Number of Drive files"
driveUsage: "Drive space usage" driveUsage: "Drive space usage"
noCrawle: "Reject crawler indexing" noCrawle: "Reject crawler indexing"
noCrawleDescription: "Ask search engines to not index your profile page, notes, Pages, etc." noCrawleDescription: "Ask search engines to not index your profile page, posts, Pages, etc."
lockedAccountInfo: "Unless you set your note visiblity to \"Followers only\", your notes will be visible to anyone, even if you require followers to be manually approved." lockedAccountInfo: "Unless you set your post visiblity to \"Followers only\", your posts will be visible to anyone, even if you require followers to be manually approved."
alwaysMarkSensitive: "Mark as NSFW by default" alwaysMarkSensitive: "Mark as NSFW by default"
loadRawImages: "Load original images instead of showing thumbnails" loadRawImages: "Load original images instead of showing thumbnails"
disableShowingAnimatedImages: "Don't play animated images" disableShowingAnimatedImages: "Don't play animated images"
verificationEmailSent: "A verification email has been sent. Please follow the included link to complete verification." verificationEmailSent: "A verification email has been sent. Please follow the included link to complete verification."
notSet: "Not set" notSet: "Not set"
emailVerified: "Email has been verified" emailVerified: "Email has been verified"
noteFavoritesCount: "Number of bookmarked notes" noteFavoritesCount: "Number of bookmarked posts"
pageLikesCount: "Number of liked Pages" pageLikesCount: "Number of liked Pages"
pageLikedCount: "Number of received Page likes" pageLikedCount: "Number of received Page likes"
contact: "Contact" contact: "Contact"
@ -702,7 +701,7 @@ showTitlebar: "Show title bar"
clearCache: "Clear cache" clearCache: "Clear cache"
onlineUsersCount: "{n} users are online" onlineUsersCount: "{n} users are online"
nUsers: "{n} Users" nUsers: "{n} Users"
nNotes: "{n} Notes" nNotes: "{n} Posts"
sendErrorReports: "Send error reports" sendErrorReports: "Send error reports"
sendErrorReportsDescription: "When turned on, detailed error information will be shared with Calckey when a problem occurs, helping to improve the quality of Misskey.\nThis will include information such the version of your OS, what browser you're using, your activity in Calckey, etc." sendErrorReportsDescription: "When turned on, detailed error information will be shared with Calckey when a problem occurs, helping to improve the quality of Misskey.\nThis will include information such the version of your OS, what browser you're using, your activity in Calckey, etc."
myTheme: "My theme" myTheme: "My theme"
@ -743,8 +742,8 @@ unlikeConfirm: "Really remove your like?"
fullView: "Full view" fullView: "Full view"
quitFullView: "Exit full view" quitFullView: "Exit full view"
addDescription: "Add description" addDescription: "Add description"
userPagePinTip: "You can display notes here by selecting \"Pin to profile\" from the menu of individual notes." userPagePinTip: "You can display posts here by selecting \"Pin to profile\" from the menu of individual posts."
notSpecifiedMentionWarning: "This note contains mentions of users not included as recipients" notSpecifiedMentionWarning: "This post contains mentions of users not included as recipients"
info: "About" info: "About"
userInfo: "User information" userInfo: "User information"
unknown: "Unknown" unknown: "Unknown"
@ -773,7 +772,7 @@ postToGallery: "Create new gallery post"
gallery: "Gallery" gallery: "Gallery"
recentPosts: "Recent pages" recentPosts: "Recent pages"
popularPosts: "Popular pages" popularPosts: "Popular pages"
shareWithNote: "Share with note" shareWithNote: "Share with post"
ads: "Advertisements" ads: "Advertisements"
expiration: "Deadline" expiration: "Deadline"
memo: "Memo" memo: "Memo"
@ -787,7 +786,7 @@ secureMode: "Secure Mode (Authorized Fetch)"
instanceSecurity: "Instance Security" instanceSecurity: "Instance Security"
secureModeInfo: "When requesting from other instances, do not send back without proof." secureModeInfo: "When requesting from other instances, do not send back without proof."
privateMode: "Private Mode" privateMode: "Private Mode"
privateModeInfo: "When enabled, only whitelisted instances can federate with your instances. All notes will be hidden from the public." privateModeInfo: "When enabled, only whitelisted instances can federate with your instances. All posts will be hidden from the public."
allowedInstances: "Whitelisted Instances" allowedInstances: "Whitelisted Instances"
allowedInstancesDescription: "Hosts of instances to be whitelisted for federation, each seperated by a new line (only applies in private mode)." allowedInstancesDescription: "Hosts of instances to be whitelisted for federation, each seperated by a new line (only applies in private mode)."
previewNoteText: "Show preview" previewNoteText: "Show preview"
@ -796,7 +795,7 @@ customCssWarn: "This setting should only be used if you know what it does. Enter
global: "Global" global: "Global"
recommended: "Recommended" recommended: "Recommended"
squareAvatars: "Display squared avatars" squareAvatars: "Display squared avatars"
seperateRenoteQuote: "Seperate renote and quote buttons" seperateRenoteQuote: "Seperate boost and quote buttons"
sent: "Sent" sent: "Sent"
received: "Received" received: "Received"
searchResult: "Search results" searchResult: "Search results"
@ -930,6 +929,7 @@ moveFrom: "Move to this account from an older account"
moveFromLabel: "Account you're moving from:" 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" 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." 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: _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." description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server."
@ -1102,7 +1102,7 @@ _channel:
owned: "Owned" owned: "Owned"
following: "Followed" following: "Followed"
usersCount: "{n} Participants" usersCount: "{n} Participants"
notesCount: "{n} Notes" notesCount: "{n} Posts"
_messaging: _messaging:
dms: "Private" dms: "Private"
groups: "Groups" groups: "Groups"
@ -1115,15 +1115,15 @@ _wordMute:
muteWords: "Muted words" muteWords: "Muted words"
muteWordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition." muteWordsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition."
muteWordsDescription2: "Surround keywords with slashes to use regular expressions." muteWordsDescription2: "Surround keywords with slashes to use regular expressions."
softDescription: "Hide notes that fulfil the set conditions from the timeline." softDescription: "Hide posts that fulfil the set conditions from the timeline."
hardDescription: "Prevents notes fulfilling the set conditions from being added to the timeline. In addition, these notes will not be added to the timeline even if the conditions are changed." hardDescription: "Prevents posts fulfilling the set conditions from being added to the timeline. In addition, these posts will not be added to the timeline even if the conditions are changed."
soft: "Soft" soft: "Soft"
hard: "Hard" hard: "Hard"
mutedNotes: "Muted notes" mutedNotes: "Muted posts"
_instanceMute: _instanceMute:
instanceMuteDescription: "This will mute any notes/renotes from the listed instances, including those of users replying to a user from a muted instance." instanceMuteDescription: "This will mute any posts/boosts from the listed instances, including those of users replying to a user from a muted instance."
instanceMuteDescription2: "Separate with newlines" instanceMuteDescription2: "Separate with newlines"
title: "Hides notes from listed instances." title: "Hides posts from listed instances."
heading: "List of instances to be muted" heading: "List of instances to be muted"
_theme: _theme:
explore: "Explore Themes" explore: "Explore Themes"
@ -1173,7 +1173,7 @@ _theme:
hashtag: "Hashtag" hashtag: "Hashtag"
mention: "Mention" mention: "Mention"
mentionMe: "Mentions (Me)" mentionMe: "Mentions (Me)"
renote: "Renote" renote: "Boost"
modalBg: "Modal background" modalBg: "Modal background"
divider: "Divider" divider: "Divider"
scrollbarHandle: "Scrollbar handle" scrollbarHandle: "Scrollbar handle"
@ -1200,8 +1200,8 @@ _theme:
accentLighten: "Accent (Lightened)" accentLighten: "Accent (Lightened)"
fgHighlighted: "Highlighted Text" fgHighlighted: "Highlighted Text"
_sfx: _sfx:
note: "New note" note: "New post"
noteMy: "Own note" noteMy: "Own post"
notification: "Notifications" notification: "Notifications"
chat: "Chat" chat: "Chat"
chatBg: "Chat (Background)" chatBg: "Chat (Background)"
@ -1227,7 +1227,7 @@ _tutorial:
step1_1: "Welcome!" step1_1: "Welcome!"
step1_2: "Let's get you set up. You'll be up and running in no time!" step1_2: "Let's get you set up. You'll be up and running in no time!"
step2_1: "First, please fill out your profile." step2_1: "First, please fill out your profile."
step2_2: "Providing some information about who you are will make it easier for others to tell if they want to see your notes or follow you." step2_2: "Providing some information about who you are will make it easier for others to tell if they want to see your posts or follow you."
step3_1: "Now time to follow some people!" step3_1: "Now time to follow some people!"
step3_2: "Your home and social timelines are based off of who you follow, so try following a couple accounts to get started.\nClick the plus circle on the top right of a profile to follow them." step3_2: "Your home and social timelines are based off of who you follow, so try following a couple accounts to get started.\nClick the plus circle on the top right of a profile to follow them."
step4_1: "Let's get you out there." step4_1: "Let's get you out there."
@ -1268,7 +1268,7 @@ _permissions:
"write:messaging": "Compose or delete chat messages" "write:messaging": "Compose or delete chat messages"
"read:mutes": "View your list of muted users" "read:mutes": "View your list of muted users"
"write:mutes": "Edit your list of muted users" "write:mutes": "Edit your list of muted users"
"write:notes": "Compose or delete notes" "write:notes": "Compose or delete posts"
"read:notifications": "View your notifications" "read:notifications": "View your notifications"
"write:notifications": "Manage your notifications" "write:notifications": "Manage your notifications"
"read:reactions": "View your reactions" "read:reactions": "View your reactions"
@ -1294,11 +1294,11 @@ _auth:
callback: "Returning to the application" callback: "Returning to the application"
denied: "Access denied" denied: "Access denied"
_antennaSources: _antennaSources:
all: "All notes" all: "All posts"
homeTimeline: "Notes from followed users" homeTimeline: "Posts from followed users"
users: "Notes from specific users" users: "Posts from specific users"
userList: "Notes from a specified list of users" userList: "Posts from a specified list of users"
userGroup: "Notes from users in a specified group" userGroup: "Posts from users in a specified group"
_weekday: _weekday:
sunday: "Sunday" sunday: "Sunday"
monday: "Monday" monday: "Monday"
@ -1329,7 +1329,9 @@ _widgets:
jobQueue: "Job Queue" jobQueue: "Job Queue"
serverMetric: "Server metrics" serverMetric: "Server metrics"
aiscript: "AiScript console" aiscript: "AiScript console"
aichan: "Ai" userList: "User list"
_userList:
chooseList: "Select a list"
_cw: _cw:
hide: "Hide" hide: "Hide"
show: "Show content" show: "Show content"
@ -1359,7 +1361,7 @@ _poll:
remainingSeconds: "{s} second(s) remaining" remainingSeconds: "{s} second(s) remaining"
_visibility: _visibility:
public: "Public" public: "Public"
publicDescription: "Your note will be visible for all users" publicDescription: "Your post will be visible for all users"
home: "Home" home: "Home"
homeDescription: "Post to home timeline only" homeDescription: "Post to home timeline only"
followers: "Followers" followers: "Followers"
@ -1369,8 +1371,8 @@ _visibility:
localOnly: "Local only" localOnly: "Local only"
localOnlyDescription: "Not visible to remote users" localOnlyDescription: "Not visible to remote users"
_postForm: _postForm:
replyPlaceholder: "Reply to this note..." replyPlaceholder: "Reply to this post..."
quotePlaceholder: "Quote this note..." quotePlaceholder: "Quote this post..."
channelPlaceholder: "Post to a channel..." channelPlaceholder: "Post to a channel..."
_placeholders: _placeholders:
a: "What are you up to?" a: "What are you up to?"
@ -1392,7 +1394,7 @@ _profile:
changeAvatar: "Change avatar" changeAvatar: "Change avatar"
changeBanner: "Change banner" changeBanner: "Change banner"
_exportOrImport: _exportOrImport:
allNotes: "All notes" allNotes: "All posts"
followingList: "Followed users" followingList: "Followed users"
muteList: "Muted users" muteList: "Muted users"
blockingList: "Blocked users" blockingList: "Blocked users"
@ -1405,10 +1407,10 @@ _charts:
usersIncDec: "Difference in the number of users" usersIncDec: "Difference in the number of users"
usersTotal: "Total number of users" usersTotal: "Total number of users"
activeUsers: "Active users" activeUsers: "Active users"
notesIncDec: "Difference in the number of notes" notesIncDec: "Difference in the number of posts"
localNotesIncDec: "Difference in the number of local notes" localNotesIncDec: "Difference in the number of local posts"
remoteNotesIncDec: "Difference in the number of remote notes" remoteNotesIncDec: "Difference in the number of remote posts"
notesTotal: "Total number of notes" notesTotal: "Total number of posts"
filesIncDec: "Difference in the number of files" filesIncDec: "Difference in the number of files"
filesTotal: "Total number of files" filesTotal: "Total number of files"
storageUsageIncDec: "Difference in storage usage" storageUsageIncDec: "Difference in storage usage"
@ -1417,8 +1419,8 @@ _instanceCharts:
requests: "Requests" requests: "Requests"
users: "Difference in the number of users" users: "Difference in the number of users"
usersTotal: "Cumulative number of users" usersTotal: "Cumulative number of users"
notes: "Difference in the number of notes" notes: "Difference in the number of posts"
notesTotal: "Cumulative number of notes" notesTotal: "Cumulative number of posts"
ff: "Difference in the number of followed users / followers " ff: "Difference in the number of followed users / followers "
ffTotal: "Cumulative number of followed users / followers" ffTotal: "Cumulative number of followed users / followers"
cacheSize: "Difference in cache size" cacheSize: "Difference in cache size"
@ -1505,10 +1507,10 @@ _pages:
id: "Canvas ID" id: "Canvas ID"
width: "Width" width: "Width"
height: "Height" height: "Height"
note: "Embedded note" note: "Embedded post"
_note: _note:
id: "Note ID" id: "Post ID"
idDescription: "You can alternatively paste the note URL here." idDescription: "You can alternatively paste the post URL here."
detailed: "Detailed view" detailed: "Detailed view"
switch: "Switch" switch: "Switch"
_switch: _switch:
@ -1729,7 +1731,7 @@ _notification:
youGotMention: "{name} mentioned you" youGotMention: "{name} mentioned you"
youGotReply: "{name} replied to you" youGotReply: "{name} replied to you"
youGotQuote: "{name} quoted you" youGotQuote: "{name} quoted you"
youRenoted: "Renote from {name}" youRenoted: "Boost from {name}"
youGotPoll: "{name} voted on your poll" youGotPoll: "{name} voted on your poll"
youGotMessagingMessageFromUser: "{name} sent you a chat message" youGotMessagingMessageFromUser: "{name} sent you a chat message"
youGotMessagingMessageFromGroup: "A chat message was sent to the {name} group" youGotMessagingMessageFromGroup: "A chat message was sent to the {name} group"
@ -1744,7 +1746,7 @@ _notification:
follow: "New followers" follow: "New followers"
mention: "Mentions" mention: "Mentions"
reply: "Replies" reply: "Replies"
renote: "Renotes" renote: "Boosts"
quote: "Quotes" quote: "Quotes"
reaction: "Reactions" reaction: "Reactions"
pollVote: "Votes on polls" pollVote: "Votes on polls"
@ -1756,7 +1758,7 @@ _notification:
_actions: _actions:
followBack: "followed you back" followBack: "followed you back"
reply: "Reply" reply: "Reply"
renote: "Renote" renote: "Boosts"
_deck: _deck:
alwaysShowMainColumn: "Always show main column" alwaysShowMainColumn: "Always show main column"
columnAlign: "Align columns" columnAlign: "Align columns"
@ -1782,4 +1784,4 @@ _deck:
antenna: "Antennas" antenna: "Antennas"
list: "List" list: "List"
mentions: "Mentions" mentions: "Mentions"
direct: "Direct notes" direct: "Direct messages"

View file

@ -580,7 +580,6 @@ tokenRequested: "Permiso de acceso a la cuenta"
pluginTokenRequestedDescription: "Este plugin podrá usar los permisos descritos aquí" pluginTokenRequestedDescription: "Este plugin podrá usar los permisos descritos aquí"
notificationType: "Tipo de notificación" notificationType: "Tipo de notificación"
edit: "Editar" edit: "Editar"
useStarForReactionFallback: "En caso de que los emojis de reacciones no sean claros, usar en su lugar una estrella"
emailServer: "Servidor de correo" emailServer: "Servidor de correo"
enableEmail: "Activar el envío de correos electrónicos" 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" emailConfigInfo: "Usar en caso de validación de correo electrónico y pedido de contraseña"

View file

@ -567,14 +567,13 @@ large: "Grand"
medium: "Moyen" medium: "Moyen"
small: "Petit" small: "Petit"
generateAccessToken: "Générer un jeton d'accès" generateAccessToken: "Générer un jeton d'accès"
permission: "Autorisations " permission: "Autorisations"
enableAll: "Tout activer" enableAll: "Tout activer"
disableAll: "Tout désactiver" disableAll: "Tout désactiver"
tokenRequested: "Autoriser l'accès au compte" tokenRequested: "Autoriser l'accès au compte"
pluginTokenRequestedDescription: "Ce plugin pourra utiliser les autorisations définies ici." pluginTokenRequestedDescription: "Ce plugin pourra utiliser les autorisations définies ici."
notificationType: "Type de notifications" notificationType: "Type de notifications"
edit: "Editer" edit: "Editer"
useStarForReactionFallback: "Utiliser ★ comme alternative si lémoji de réaction est inconnu"
emailServer: "Serveur mail" emailServer: "Serveur mail"
enableEmail: "Activer la distribution de courriel" 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 doubli." emailConfigInfo: "Utilisé pour confirmer votre adresse de courriel et la réinitialisation de votre mot de passe en cas doubli."

View file

@ -577,7 +577,6 @@ tokenRequested: "Berikan ijin akses ke akun"
pluginTokenRequestedDescription: "Plugin ini dapat menggunakan setelan ijin disini." pluginTokenRequestedDescription: "Plugin ini dapat menggunakan setelan ijin disini."
notificationType: "Jenis pemberitahuan" notificationType: "Jenis pemberitahuan"
edit: "Sunting" edit: "Sunting"
useStarForReactionFallback: "Gunakan ★ sebagai fallback jika reaksi emoji tidak diketahui"
emailServer: "Peladen surel" emailServer: "Peladen surel"
enableEmail: "Nyalakan distribusi surel" enableEmail: "Nyalakan distribusi surel"
emailConfigInfo: "Digunakan untuk mengonfirmasi surel kamu disaat mendaftar dan lupa kata sandi" emailConfigInfo: "Digunakan untuk mengonfirmasi surel kamu disaat mendaftar dan lupa kata sandi"

View file

@ -573,7 +573,6 @@ tokenRequested: "Autorizza accesso all'account"
pluginTokenRequestedDescription: "Il plugin potrà utilizzare le autorizzazioni impostate qui." pluginTokenRequestedDescription: "Il plugin potrà utilizzare le autorizzazioni impostate qui."
notificationType: "Tipo di notifiche" notificationType: "Tipo di notifiche"
edit: "Modifica" edit: "Modifica"
useStarForReactionFallback: "Se è sconosciuto l'emoji di reazione, usare la ★ come alternativa."
emailServer: "Server email" emailServer: "Server email"
enableEmail: "Abilita consegna email" enableEmail: "Abilita consegna email"
emailConfigInfo: "Utilizzato per verificare il tuo indirizzo di posta elettronica e per reimpostare la tua password" emailConfigInfo: "Utilizzato per verificare il tuo indirizzo di posta elettronica e per reimpostare la tua password"

View file

@ -583,7 +583,6 @@ tokenRequested: "アカウントへのアクセス許可"
pluginTokenRequestedDescription: "このプラグインはここで設定した権限を行使できるようになります。" pluginTokenRequestedDescription: "このプラグインはここで設定した権限を行使できるようになります。"
notificationType: "通知の種類" notificationType: "通知の種類"
edit: "編集" edit: "編集"
useStarForReactionFallback: "リアクション絵文字が不明な場合、代わりに★を使う"
emailServer: "メールサーバー" emailServer: "メールサーバー"
enableEmail: "メール配信機能を有効化する" enableEmail: "メール配信機能を有効化する"
emailConfigInfo: "メールアドレスの確認やパスワードリセットの際に使います" emailConfigInfo: "メールアドレスの確認やパスワードリセットの際に使います"

View file

@ -579,7 +579,6 @@ tokenRequested: "アカウントへのアクセス許可"
pluginTokenRequestedDescription: "このプラグインはここで設定した権限を使えるようになるで。" pluginTokenRequestedDescription: "このプラグインはここで設定した権限を使えるようになるで。"
notificationType: "通知の種類" notificationType: "通知の種類"
edit: "編集" edit: "編集"
useStarForReactionFallback: "リアクションがようわからん場合、★を使う"
emailServer: "メールサーバー" emailServer: "メールサーバー"
enableEmail: "メール配信を受け取る" enableEmail: "メール配信を受け取る"
emailConfigInfo: "メールアドレスの確認とかパスワードリセットの時に使うで" emailConfigInfo: "メールアドレスの確認とかパスワードリセットの時に使うで"

View file

@ -580,7 +580,6 @@ tokenRequested: "계정 접근 허용"
pluginTokenRequestedDescription: "이 플러그인은 여기서 설정한 권한을 사용할 수 있게 됩니다." pluginTokenRequestedDescription: "이 플러그인은 여기서 설정한 권한을 사용할 수 있게 됩니다."
notificationType: "알림 유형" notificationType: "알림 유형"
edit: "편집" edit: "편집"
useStarForReactionFallback: "알 수 없는 리액션 이모지 대신 ★ 사용"
emailServer: "메일 서버" emailServer: "메일 서버"
enableEmail: "이메일 송신 기능 활성화" enableEmail: "이메일 송신 기능 활성화"
emailConfigInfo: "가입 시 메일 주소 확인이나 비밀번호 초기화 시에 사용합니다." emailConfigInfo: "가입 시 메일 주소 확인이나 비밀번호 초기화 시에 사용합니다."

View file

@ -572,7 +572,6 @@ tokenRequested: "Przydziel dostęp do konta"
pluginTokenRequestedDescription: "Ta wtyczka będzie mogła korzystać z ustawionych tu uprawnień." pluginTokenRequestedDescription: "Ta wtyczka będzie mogła korzystać z ustawionych tu uprawnień."
notificationType: "Rodzaj powiadomień" notificationType: "Rodzaj powiadomień"
edit: "Edytuj" edit: "Edytuj"
useStarForReactionFallback: "Użyj ★ jako zapasowego emoji, gdy emoji reakcji jest nieznane"
emailServer: "Serwer poczty e-mail" emailServer: "Serwer poczty e-mail"
enableEmail: "Włącz dostarczanie wiadomości 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" emailConfigInfo: "Wykorzystywany do potwierdzenia adresu e-mail w trakcie rejestracji, lub gdy zapomnisz hasła"

View file

@ -576,7 +576,6 @@ tokenRequested: "Acordă acces la cont"
pluginTokenRequestedDescription: "Acest plugin va putea să folosească permisiunile setate aici." pluginTokenRequestedDescription: "Acest plugin va putea să folosească permisiunile setate aici."
notificationType: "Tipul notificării" notificationType: "Tipul notificării"
edit: "Editează" edit: "Editează"
useStarForReactionFallback: "Folosește ★ ca fallback dacă emoji-ul este necunoscut"
emailServer: "Server email" emailServer: "Server email"
enableEmail: "Activează distribuția de emailuri" enableEmail: "Activează distribuția de emailuri"
emailConfigInfo: "Folosit pentru a confirma emailul tău în timpul logări dacă îți uiți parola" emailConfigInfo: "Folosit pentru a confirma emailul tău în timpul logări dacă îți uiți parola"

View file

@ -580,7 +580,6 @@ tokenRequested: "Открыть доступ к учётной записи"
pluginTokenRequestedDescription: "Это расширение сможет пользоваться разрешениями, установленными здесь." pluginTokenRequestedDescription: "Это расширение сможет пользоваться разрешениями, установленными здесь."
notificationType: "Тип уведомления" notificationType: "Тип уведомления"
edit: "Изменить" edit: "Изменить"
useStarForReactionFallback: "Ставить ★ в качестве реакции вместо неизвестного эмодзи"
emailServer: "Сервер электронной почты" emailServer: "Сервер электронной почты"
enableEmail: "Включить обмен электронной почтой" enableEmail: "Включить обмен электронной почтой"
emailConfigInfo: "Используется для подтверждения адреса электронной почты и сброса пароля." emailConfigInfo: "Используется для подтверждения адреса электронной почты и сброса пароля."

View file

@ -579,7 +579,6 @@ tokenRequested: "Povoliť prístup k účtu"
pluginTokenRequestedDescription: "Tento plugin bude môcť používať oprávnenia nastavené tu." pluginTokenRequestedDescription: "Tento plugin bude môcť používať oprávnenia nastavené tu."
notificationType: "Typ oznámenia" notificationType: "Typ oznámenia"
edit: "Upraviť" edit: "Upraviť"
useStarForReactionFallback: "Použiť ★ keď emoji reakcie nie je známe"
emailServer: "Email server" emailServer: "Email server"
enableEmail: "Zapnúť email" enableEmail: "Zapnúť email"
emailConfigInfo: "Používa sa na overenie emaily pri registrácii alebo pri zabudnutí hesla" emailConfigInfo: "Používa sa na overenie emaily pri registrácii alebo pri zabudnutí hesla"

View file

@ -580,7 +580,6 @@ tokenRequested: "ให้สิทธิ์การเข้าถึงบั
pluginTokenRequestedDescription: "ปลั๊กอินนี้จะสามารถใช้การอนุญาตที่ตั้งค่าไว้ที่นี่นะ" pluginTokenRequestedDescription: "ปลั๊กอินนี้จะสามารถใช้การอนุญาตที่ตั้งค่าไว้ที่นี่นะ"
notificationType: "ประเภทการแจ้งเตือน" notificationType: "ประเภทการแจ้งเตือน"
edit: "แก้ไข" edit: "แก้ไข"
useStarForReactionFallback: "ใช้ ★ เป็นทางเลือกแทนถ้าหากไม่ทราบอิโมจิ"
emailServer: "อีเมล์เซิร์ฟเวอร์" emailServer: "อีเมล์เซิร์ฟเวอร์"
enableEmail: "เปิดใช้งานการกระจายอีเมล" enableEmail: "เปิดใช้งานการกระจายอีเมล"
emailConfigInfo: "ใช้เพื่อยืนยันอีเมลของคุณระหว่างการสมัครหรือถ้าหากคุณลืมรหัสผ่าน" emailConfigInfo: "ใช้เพื่อยืนยันอีเมลของคุณระหว่างการสมัครหรือถ้าหากคุณลืมรหัสผ่าน"

View file

@ -577,7 +577,6 @@ tokenRequested: "Надати доступ до акаунту"
pluginTokenRequestedDescription: "Цей плагін зможе використовувати дозволи які тут вказані." pluginTokenRequestedDescription: "Цей плагін зможе використовувати дозволи які тут вказані."
notificationType: "Тип сповіщення" notificationType: "Тип сповіщення"
edit: "Редагувати" edit: "Редагувати"
useStarForReactionFallback: "Використовувати ★ як запасний варіант, якщо емодзі реакції невідомий"
emailServer: "Сервер електронної пошти" emailServer: "Сервер електронної пошти"
enableEmail: "Увімкнути функцію доставки пошти" enableEmail: "Увімкнути функцію доставки пошти"
emailConfigInfo: "Використовується для підтвердження електронної пошти підчас реєстрації, а також для відновлення паролю." emailConfigInfo: "Використовується для підтвердження електронної пошти підчас реєстрації, а також для відновлення паролю."

View file

@ -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." pluginTokenRequestedDescription: "Plugin này sẽ có thể sử dụng các quyền được đặt ở đây."
notificationType: "Loại thông báo" notificationType: "Loại thông báo"
edit: "Sửa" edit: "Sửa"
useStarForReactionFallback: "Dùng ★ nếu emoji biểu cảm không có"
emailServer: "Email máy chủ" emailServer: "Email máy chủ"
enableEmail: "Bật phân phối email" 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" 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"

View file

@ -580,7 +580,6 @@ tokenRequested: "允许访问账户"
pluginTokenRequestedDescription: "此插件将能够拥有此处设置的权限" pluginTokenRequestedDescription: "此插件将能够拥有此处设置的权限"
notificationType: "通知类型" notificationType: "通知类型"
edit: "编辑" edit: "编辑"
useStarForReactionFallback: "如果回应的是未知表情符号,则使用★作为代替"
emailServer: "邮件服务器" emailServer: "邮件服务器"
enableEmail: "启用发送邮件功能" enableEmail: "启用发送邮件功能"
emailConfigInfo: "用于确认电子邮件和密码重置" emailConfigInfo: "用于确认电子邮件和密码重置"

View file

@ -580,7 +580,6 @@ tokenRequested: "允許存取帳戶"
pluginTokenRequestedDescription: "此外掛將擁有在此設定的權限。" pluginTokenRequestedDescription: "此外掛將擁有在此設定的權限。"
notificationType: "通知形式" notificationType: "通知形式"
edit: "編輯" edit: "編輯"
useStarForReactionFallback: "以★代替未知的表情符號"
emailServer: "電郵伺服器" emailServer: "電郵伺服器"
enableEmail: "啟用發送電郵功能" enableEmail: "啟用發送電郵功能"
emailConfigInfo: "用於確認電郵地址及密碼重置" emailConfigInfo: "用於確認電郵地址及密碼重置"

View file

@ -1,51 +1,45 @@
{ {
"name": "calckey", "name": "calckey",
"version": "13.0.5", "version": "13.1.0",
"codename": "aqua", "codename": "aqua",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://codeberg.org/calckey/calckey.git" "url": "https://codeberg.org/calckey/calckey.git"
}, },
"packageManager": "yarn@3.3.0", "packageManager": "pnpm@7.26.3",
"workspaces": [
"packages/client",
"packages/backend",
"packages/sw"
],
"private": true, "private": true,
"scripts": { "scripts": {
"rebuild": "yarn clean && yarn workspaces foreach run build && yarn run gulp", "rebuild": "pnpm run clean && pnpm -r run build && pnpm run gulp",
"build": "yarn workspaces foreach run build && yarn run gulp", "build": "pnpm -r run build && pnpm run gulp",
"start": "yarn workspace backend run start", "start": "pnpm --filter backend run start",
"start:test": "yarn workspace backend run start:test", "start:test": "pnpm --filter backend run start:test",
"init": "yarn migrate", "init": "pnpm run migrate",
"migrate": "yarn workspace backend run migrate", "migrate": "pnpm --filter backend run migrate",
"revertmigration": "yarn workspace backend run revertmigration", "revertmigration": "pnpm --filter backend run revertmigration",
"migrateandstart": "yarn migrate && yarn start", "migrateandstart": "pnpm run migrate && pnpm run start",
"gulp": "gulp build", "gulp": "gulp build",
"watch": "yarn dev", "watch": "pnpm run dev",
"dev": "node ./scripts/dev.js", "dev": "pnpm node ./scripts/dev.js",
"lint": "yarn workspaces foreach run lint", "lint": "pnpm -r run lint",
"cy:open": "cypress open --browser --e2e --config-file=cypress.config.ts", "cy:open": "cypress open --browser --e2e --config-file=cypress.config.ts",
"cy:run": "cypress run", "cy:run": "cypress run",
"e2e": "start-server-and-test start:test http://localhost:61812 cy:run", "e2e": "start-server-and-test start:test http://localhost:61812 cy:run",
"mocha": "yarn workspace backend run mocha", "mocha": "pnpm --filter backend run mocha",
"test": "yarn mocha", "test": "pnpm run mocha",
"format": "gulp format", "format": "gulp format",
"clean": "node ./scripts/clean.js", "clean": "pnpm node ./scripts/clean.js",
"clean-all": "node ./scripts/clean-all.js", "clean-all": "pnpm node ./scripts/clean-all.js",
"cleanall": "yarn clean-all" "cleanall": "pnpm run clean-all"
}, },
"resolutions": { "resolutions": {
"chokidar": "^3.3.1", "chokidar": "^3.3.1",
"lodash": "^4.17.21" "lodash": "^4.17.21"
}, },
"dependencies": { "dependencies": {
"@bull-board/api": "^4.6.4", "@bull-board/api": "^4.10.2",
"@bull-board/ui": "^4.6.4", "@bull-board/ui": "^4.10.2",
"@tensorflow/tfjs": "^3.21.0", "@tensorflow/tfjs": "^3.21.0",
"calckey-js": "^0.0.17", "calckey-js": "^0.0.20",
"eslint": "^8.30.0",
"execa": "5.1.1", "execa": "5.1.1",
"gulp": "4.0.2", "gulp": "4.0.2",
"gulp-cssnano": "2.1.3", "gulp-cssnano": "2.1.3",
@ -60,11 +54,11 @@
"devDependencies": { "devDependencies": {
"@types/gulp": "4.0.10", "@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1", "@types/gulp-rename": "2.0.1",
"@typescript-eslint/parser": "5.46.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "10.11.0", "cypress": "10.11.0",
"install-peers": "^1.0.4",
"rome": "^11.0.0",
"start-server-and-test": "1.15.2", "start-server-and-test": "1.15.2",
"typescript": "4.9.4", "typescript": "4.9.4"
"vue-eslint-parser": "^9.1.0"
} }
} }

View file

@ -1,4 +0,0 @@
node_modules
/built
/.eslintrc.js
/@types/**/*

View file

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

BIN
packages/backend/assets/icons/maskable.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
packages/backend/assets/icons/monochrome.png (Stored with Git LFS) Normal file

Binary file not shown.

View 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"`);
}
}

View 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`);
}
}

View file

@ -4,21 +4,22 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "node ./built/index.js", "start": "pnpm node ./built/index.js",
"start:test": "NODE_ENV=test node ./built/index.js", "start:test": "NODE_ENV=test pnpm node ./built/index.js",
"migrate": "typeorm migration:run -d ormconfig.js", "migrate": "typeorm migration:run -d ormconfig.js",
"revertmigration": "typeorm migration:revert -d ormconfig.js", "revertmigration": "typeorm migration:revert -d ormconfig.js",
"build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json", "build": "pnpm swc src -d built -D",
"watch": "node watch.mjs", "watch": "pnpm swc src -d built -D -w",
"lint": "eslint --quiet \"src/**/*.ts\"", "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", "mocha": "cross-env NODE_ENV=test TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha",
"test": "npm run mocha" "test": "pnpm run mocha"
}, },
"resolutions": { "resolutions": {
"chokidar": "^3.3.1", "chokidar": "^3.3.1",
"lodash": "^4.17.21" "lodash": "^4.17.21"
}, },
"optionalDependencies": { "optionalDependencies": {
"@swc/core-android-arm64": "1.3.11",
"@tensorflow/tfjs-node": "3.21.1" "@tensorflow/tfjs-node": "3.21.1"
}, },
"dependencies": { "dependencies": {
@ -29,10 +30,14 @@
"@elastic/elasticsearch": "7.17.0", "@elastic/elasticsearch": "7.17.0",
"@koa/cors": "3.4.3", "@koa/cors": "3.4.3",
"@koa/multer": "3.0.0", "@koa/multer": "3.0.0",
"@koa/router": "9.4.0", "@koa/router": "9.0.1",
"@peertube/http-signature": "1.7.0", "@peertube/http-signature": "1.7.0",
"@redocly/openapi-core": "1.0.0-beta.120",
"@sinonjs/fake-timers": "9.1.2", "@sinonjs/fake-timers": "9.1.2",
"@swc/cli": "^0.1.59",
"@swc/core": "^1.3.26",
"@syuilo/aiscript": "0.11.1", "@syuilo/aiscript": "0.11.1",
"@tensorflow/tfjs": "^4.2.0",
"ajv": "8.11.2", "ajv": "8.11.2",
"archiver": "5.3.1", "archiver": "5.3.1",
"autobind-decorator": "2.4.0", "autobind-decorator": "2.4.0",
@ -42,7 +47,7 @@
"blurhash": "1.1.5", "blurhash": "1.1.5",
"bull": "4.10.2", "bull": "4.10.2",
"cacheable-lookup": "7.0.0", "cacheable-lookup": "7.0.0",
"calckey-js": "^0.0.17", "calckey-js": "^0.0.20",
"cbor": "8.1.0", "cbor": "8.1.0",
"chalk": "5.2.0", "chalk": "5.2.0",
"chalk-template": "0.4.0", "chalk-template": "0.4.0",
@ -58,12 +63,12 @@
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.2",
"got": "12.5.3", "got": "12.5.3",
"hpagent": "0.1.2", "hpagent": "0.1.2",
"ioredis": "4.28.5", "ioredis": "5.2.4",
"ip-cidr": "3.0.11", "ip-cidr": "3.0.11",
"is-svg": "4.3.2", "is-svg": "4.3.2",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsdom": "20.0.3", "jsdom": "20.0.3",
"json5": "2.2.2", "json5": "2.2.3",
"json5-loader": "4.0.1", "json5-loader": "4.0.1",
"jsonld": "6.0.0", "jsonld": "6.0.0",
"jsrsasign": "10.6.1", "jsrsasign": "10.6.1",
@ -76,7 +81,7 @@
"koa-send": "5.0.1", "koa-send": "5.0.1",
"koa-slow": "2.1.0", "koa-slow": "2.1.0",
"koa-views": "7.0.2", "koa-views": "7.0.2",
"mfm-js": "0.23.0", "mfm-js": "0.23.2",
"mime-types": "2.1.35", "mime-types": "2.1.35",
"mocha": "10.2.0", "mocha": "10.2.0",
"multer": "1.4.4-lts.1", "multer": "1.4.4-lts.1",
@ -93,7 +98,7 @@
"promise-limit": "2.7.0", "promise-limit": "2.7.0",
"pug": "3.0.2", "pug": "3.0.2",
"punycode": "2.1.1", "punycode": "2.1.1",
"pureimage": "0.3.14", "pureimage": "0.3.15",
"qrcode": "1.5.1", "qrcode": "1.5.1",
"random-seed": "0.3.0", "random-seed": "0.3.0",
"ratelimiter": "3.4.1", "ratelimiter": "3.4.1",
@ -104,22 +109,22 @@
"rndstr": "1.0.0", "rndstr": "1.0.0",
"rss-parser": "3.12.0", "rss-parser": "3.12.0",
"s-age": "1.1.2", "s-age": "1.1.2",
"sanitize-html": "2.8.0", "sanitize-html": "2.8.1",
"seedrandom": "^3.0.5",
"semver": "7.3.8", "semver": "7.3.8",
"sharp": "0.31.2", "sharp": "0.31.3",
"speakeasy": "2.0.0", "speakeasy": "2.0.0",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0", "stringz": "2.1.0",
"summaly": "2.7.0", "summaly": "2.7.0",
"syslog-pro": "1.0.0", "syslog-pro": "1.0.0",
"systeminformation": "5.16.6", "systeminformation": "5.16.9",
"tesseract.js": "^3.0.3", "tesseract.js": "^3.0.3",
"tinycolor2": "1.4.2", "tinycolor2": "1.5.2",
"tmp": "0.2.1", "tmp": "0.2.1",
"ts-loader": "9.4.2", "ts-loader": "9.4.2",
"ts-node": "10.9.1", "ts-node": "10.9.1",
"tsc-alias": "1.8.2", "tsconfig-paths": "4.1.2",
"tsconfig-paths": "4.1.1",
"twemoji-parser": "14.0.0", "twemoji-parser": "14.0.0",
"typeorm": "0.3.11", "typeorm": "0.3.11",
"ulid": "2.3.0", "ulid": "2.3.0",
@ -131,7 +136,6 @@
"xev": "3.0.2" "xev": "3.0.2"
}, },
"devDependencies": { "devDependencies": {
"@redocly/openapi-core": "1.0.0-beta.114",
"@types/bcryptjs": "2.4.2", "@types/bcryptjs": "2.4.2",
"@types/bull": "3.15.9", "@types/bull": "3.15.9",
"@types/cbor": "6.0.0", "@types/cbor": "6.0.0",
@ -153,7 +157,7 @@
"@types/koa__multer": "2.0.4", "@types/koa__multer": "2.0.4",
"@types/koa__router": "8.0.11", "@types/koa__router": "8.0.11",
"@types/mocha": "9.1.1", "@types/mocha": "9.1.1",
"@types/node": "18.11.17", "@types/node": "18.11.18",
"@types/node-fetch": "3.0.3", "@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.7", "@types/nodemailer": "6.4.7",
"@types/oauth": "0.9.1", "@types/oauth": "0.9.1",
@ -166,7 +170,7 @@
"@types/rename": "1.0.4", "@types/rename": "1.0.4",
"@types/sanitize-html": "2.8.0", "@types/sanitize-html": "2.8.0",
"@types/semver": "7.3.13", "@types/semver": "7.3.13",
"@types/sharp": "0.31.0", "@types/sharp": "0.31.1",
"@types/sinonjs__fake-timers": "8.1.2", "@types/sinonjs__fake-timers": "8.1.2",
"@types/speakeasy": "2.0.7", "@types/speakeasy": "2.0.7",
"@types/tinycolor2": "1.4.3", "@types/tinycolor2": "1.4.3",
@ -175,12 +179,11 @@
"@types/web-push": "3.3.2", "@types/web-push": "3.3.2",
"@types/websocket": "1.0.5", "@types/websocket": "1.0.5",
"@types/ws": "8.5.3", "@types/ws": "8.5.3",
"@typescript-eslint/eslint-plugin": "5.46.1",
"@typescript-eslint/parser": "5.46.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "8.30.0", "eslint": "^8.31.0",
"eslint-plugin-import": "2.26.0",
"execa": "6.1.0", "execa": "6.1.0",
"typescript": "4.9.4" "swc-loader": "^0.2.3",
"typescript": "4.9.4",
"webpack": "^5.75.0"
} }
} }

View file

@ -1,11 +1,14 @@
declare module 'hcaptcha' { declare module "hcaptcha" {
interface IVerifyResponse { interface IVerifyResponse {
success: boolean; success: boolean;
challenge_ts: string; challenge_ts: string;
hostname: string; hostname: string;
credit?: boolean; 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>;
} }

View file

@ -1,5 +1,5 @@
declare module '@peertube/http-signature' { declare module "@peertube/http-signature" {
import { IncomingMessage, ClientRequest } from 'node:http'; import type { IncomingMessage, ClientRequest } from "node:http";
interface ISignature { interface ISignature {
keyId: string; keyId: string;
@ -28,8 +28,8 @@ declare module '@peertube/http-signature' {
} }
type RequestSignerConstructorOptions = type RequestSignerConstructorOptions =
IRequestSignerConstructorOptionsFromProperties | | IRequestSignerConstructorOptionsFromProperties
IRequestSignerConstructorOptionsFromFunction; | IRequestSignerConstructorOptionsFromFunction;
interface IRequestSignerConstructorOptionsFromProperties { interface IRequestSignerConstructorOptionsFromProperties {
keyId: string; keyId: string;
@ -59,11 +59,23 @@ declare module '@peertube/http-signature' {
httpVersion?: string; httpVersion?: string;
} }
export function parse(request: IncomingMessage, options?: IParseRequestOptions): IParsedSignature; export function parse(
export function parseRequest(request: IncomingMessage, options?: IParseRequestOptions): IParsedSignature; request: IncomingMessage,
options?: IParseRequestOptions,
): IParsedSignature;
export function parseRequest(
request: IncomingMessage,
options?: IParseRequestOptions,
): IParsedSignature;
export function sign(request: ClientRequest, options: ISignRequestOptions): boolean; export function sign(
export function signRequest(request: ClientRequest, options: ISignRequestOptions): boolean; request: ClientRequest,
options: ISignRequestOptions,
): boolean;
export function signRequest(
request: ClientRequest,
options: ISignRequestOptions,
): boolean;
export function createSigner(): RequestSigner; export function createSigner(): RequestSigner;
export function isSigner(obj: any): obj is 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 sshKeyFingerprint(key: string): string;
export function pemToRsaSSHKey(pem: string, comment: string): string; export function pemToRsaSSHKey(pem: string, comment: string): string;
export function verify(parsedSignature: IParsedSignature, pubkey: string | Buffer): boolean; export function verify(
export function verifySignature(parsedSignature: IParsedSignature, pubkey: string | Buffer): boolean; parsedSignature: IParsedSignature,
export function verifyHMAC(parsedSignature: IParsedSignature, secret: string): boolean; pubkey: string | Buffer,
): boolean;
export function verifySignature(
parsedSignature: IParsedSignature,
pubkey: string | Buffer,
): boolean;
export function verifyHMAC(
parsedSignature: IParsedSignature,
secret: string,
): boolean;
} }

View file

@ -1,5 +1,5 @@
declare module 'koa-json-body' { declare module "koa-json-body" {
import { Middleware } from 'koa'; import type { Middleware } from "koa";
interface IKoaJsonBodyOptions { interface IKoaJsonBodyOptions {
strict: boolean; strict: boolean;

View file

@ -1,5 +1,5 @@
declare module 'koa-slow' { declare module "koa-slow" {
import { Middleware } from 'koa'; import type { Middleware } from "koa";
interface ISlowOptions { interface ISlowOptions {
url?: RegExp; url?: RegExp;

View file

@ -1,4 +1,4 @@
declare module 'os-utils' { declare module "os-utils" {
type FreeCommandCallback = (usedmem: number) => void; type FreeCommandCallback = (usedmem: number) => void;
type HarddriveCallback = (total: number, free: number, used: 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 harddrive(callback: HarddriveCallback): void;
export function getProcesses(callback: GetProcessesCallback): 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 allLoadavg(): string;
export function loadavg(_time?: number): number; export function loadavg(_time?: number): number;

View file

@ -1,4 +1,4 @@
declare module '*/package.json' { declare module "*/package.json" {
interface IRepository { interface IRepository {
type: string; type: string;
url: string; url: string;

View file

@ -1,5 +1,5 @@
declare module 'probe-image-size' { declare module "probe-image-size" {
import { ReadStream } from 'node:fs'; import type { ReadStream } from "node:fs";
type ProbeOptions = { type ProbeOptions = {
retries: 1; retries: 1;
@ -12,14 +12,24 @@ declare module 'probe-image-size' {
length?: number; length?: number;
type: string; type: string;
mime: string; mime: string;
wUnits: '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'; hUnits: "in" | "mm" | "cm" | "pt" | "pc" | "px" | "em" | "ex";
url?: string; url?: string;
}; };
function probeImageSize(src: string | ReadStream, options?: ProbeOptions): Promise<ProbeResult>; function probeImageSize(
function probeImageSize(src: string | ReadStream, callback: (err: Error | null, result?: ProbeResult) => void): void; src: string | ReadStream,
function probeImageSize(src: string | ReadStream, options: ProbeOptions, callback: (err: Error | null, result?: ProbeResult) => void): void; 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 namespace probeImageSize {} // Hack

View file

@ -1,28 +1,27 @@
import cluster from 'node:cluster'; import cluster from "node:cluster";
import chalk from 'chalk'; import chalk from "chalk";
import Xev from 'xev'; import Xev from "xev";
import Logger from '@/services/logger.js'; import Logger from "@/services/logger.js";
import { envOption } from '../env.js'; import { envOption } from "../env.js";
// for typeorm // for typeorm
import 'reflect-metadata'; import "reflect-metadata";
import { masterMain } from './master.js'; import { masterMain } from "./master.js";
import { workerMain } from './worker.js'; import { workerMain } from "./worker.js";
const logger = new Logger('core', 'cyan'); const logger = new Logger("core", "cyan");
const clusterLogger = logger.createSubLogger('cluster', 'orange', false); const clusterLogger = logger.createSubLogger("cluster", "orange", false);
const ev = new Xev(); const ev = new Xev();
/** /**
* Init process * Init process
*/ */
export default async function() { export default async function () {
process.title = `Calckey (${cluster.isPrimary ? 'master' : 'worker'})`; process.title = `Calckey (${cluster.isPrimary ? "master" : "worker"})`;
if (cluster.isPrimary || envOption.disableClustering) { if (cluster.isPrimary || envOption.disableClustering) {
await masterMain(); await masterMain();
if (cluster.isPrimary) { if (cluster.isPrimary) {
ev.mount(); ev.mount();
} }
@ -32,27 +31,27 @@ export default async function() {
await workerMain(); await workerMain();
} }
// ユニットテスト時にMisskeyが子プロセスで起動された時のため // For when Calckey is started in a child process during unit testing.
// それ以外のときは process.send は使えないので弾く // Otherwise, process.send cannot be used, so start it.
if (process.send) { if (process.send) {
process.send('ok'); process.send("ok");
} }
} }
//#region Events //#region Events
// Listen new workers // Listen new workers
cluster.on('fork', worker => { cluster.on("fork", (worker) => {
clusterLogger.debug(`Process forked: [${worker.id}]`); clusterLogger.debug(`Process forked: [${worker.id}]`);
}); });
// Listen online workers // Listen online workers
cluster.on('online', worker => { cluster.on("online", (worker) => {
clusterLogger.debug(`Process is now online: [${worker.id}]`); clusterLogger.debug(`Process is now online: [${worker.id}]`);
}); });
// Listen for dying workers // Listen for dying workers
cluster.on('exit', worker => { cluster.on("exit", (worker) => {
// Replace the dead worker, // Replace the dead worker,
// we're not sentimental // we're not sentimental
clusterLogger.error(chalk.red(`[${worker.id}] died :(`)); clusterLogger.error(chalk.red(`[${worker.id}] died :(`));
@ -61,18 +60,18 @@ cluster.on('exit', worker => {
// Display detail of unhandled promise rejection // Display detail of unhandled promise rejection
if (!envOption.quiet) { if (!envOption.quiet) {
process.on('unhandledRejection', console.dir); process.on("unhandledRejection", console.dir);
} }
// Display detail of uncaught exception // Display detail of uncaught exception
process.on('uncaughtException', err => { process.on("uncaughtException", (err) => {
try { try {
logger.error(err); logger.error(err);
} catch { } } catch {}
}); });
// Dying away... // Dying away...
process.on('exit', code => { process.on("exit", (code) => {
logger.info(`The process is going to exit with code ${code}`); logger.info(`The process is going to exit with code ${code}`);
}); });

View file

@ -1,50 +1,64 @@
import * as fs from 'node:fs'; import * as fs from "node:fs";
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from "node:url";
import { dirname } from 'node:path'; import { dirname } from "node:path";
import * as os from 'node:os'; import * as os from "node:os";
import cluster from 'node:cluster'; import cluster from "node:cluster";
import chalk from 'chalk'; import chalk from "chalk";
import chalkTemplate from 'chalk-template'; import chalkTemplate from "chalk-template";
import semver from 'semver'; import semver from "semver";
import Logger from '@/services/logger.js'; import Logger from "@/services/logger.js";
import loadConfig from '@/config/load.js'; import loadConfig from "@/config/load.js";
import { Config } from '@/config/types.js'; import type { Config } from "@/config/types.js";
import { lessThan } from '@/prelude/array.js'; import { lessThan } from "@/prelude/array.js";
import { envOption } from '../env.js'; import { envOption } from "../env.js";
import { showMachineInfo } from '@/misc/show-machine-info.js'; import { showMachineInfo } from "@/misc/show-machine-info.js";
import { db, initDb } from '../db/postgre.js'; import { db, initDb } from "../db/postgre.js";
const _filename = fileURLToPath(import.meta.url); const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename); 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 logger = new Logger("core", "cyan");
const bootLogger = logger.createSubLogger('boot', 'magenta', false); const bootLogger = logger.createSubLogger("boot", "magenta", false);
const themeColor = chalk.hex('#31748f'); const themeColor = chalk.hex("#31748f");
function greet() { function greet() {
if (!envOption.quiet) { if (!envOption.quiet) {
//#region Calckey logo //#region Calckey logo
const v = `v${meta.version}`; 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 //#endregion
console.log(' Calckey is an open-source decentralized microblogging platform.'); console.log(
console.log(chalk.rgb(255, 136, 0)(' If you like Calckey, please consider starring or contributing to the repo. https://codeberg.org/calckey/calckey')); " 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(''); console.log("");
console.log(chalkTemplate`--- ${os.hostname()} {gray (PID: ${process.pid.toString()})} ---`); 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); bootLogger.info(`Calckey v${meta.version}`, null, true);
} }
@ -63,42 +77,50 @@ export async function masterMain() {
config = loadConfigBoot(); config = loadConfigBoot();
await connectDb(); await connectDb();
} catch (e) { } catch (e) {
bootLogger.error('Fatal error occurred during initialization', null, true); bootLogger.error("Fatal error occurred during initialization", null, true);
process.exit(1); process.exit(1);
} }
bootLogger.succ('Calckey initialized'); bootLogger.succ("Calckey initialized");
if (!envOption.disableClustering) { if (!envOption.disableClustering) {
await spawnWorkers(config.clusterLimit); 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) { if (!envOption.noDaemons) {
import('../daemons/server-stats.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/queue-stats.js").then((x) => x.default());
import('../daemons/janitor.js').then(x => x.default()); import("../daemons/janitor.js").then((x) => x.default());
} }
} }
function showEnvironment(): void { function showEnvironment(): void {
const env = process.env.NODE_ENV; const env = process.env.NODE_ENV;
const logger = bootLogger.createSubLogger('env'); const logger = bootLogger.createSubLogger("env");
logger.info(typeof env === 'undefined' ? 'NODE_ENV is not set' : `NODE_ENV: ${env}`); logger.info(
typeof env === "undefined" ? "NODE_ENV is not set" : `NODE_ENV: ${env}`,
);
if (env !== 'production') { if (env !== "production") {
logger.warn('The environment is not in production mode.'); logger.warn("The environment is not in production mode.");
logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', null, true); logger.warn("DO NOT USE FOR PRODUCTION PURPOSE!", null, true);
} }
} }
function showNodejsVersion(): void { function showNodejsVersion(): void {
const nodejsLogger = bootLogger.createSubLogger('nodejs'); const nodejsLogger = bootLogger.createSubLogger("nodejs");
nodejsLogger.info(`Version ${process.version} detected.`); 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)) { if (semver.lt(process.version, minVersion)) {
nodejsLogger.error(`At least Node.js ${minVersion} required!`); nodejsLogger.error(`At least Node.js ${minVersion} required!`);
process.exit(1); process.exit(1);
@ -106,14 +128,14 @@ function showNodejsVersion(): void {
} }
function loadConfigBoot(): Config { function loadConfigBoot(): Config {
const configLogger = bootLogger.createSubLogger('config'); const configLogger = bootLogger.createSubLogger("config");
let config; let config;
try { try {
config = loadConfig(); config = loadConfig();
} catch (exception) { } catch (exception) {
if (exception.code === 'ENOENT') { if (exception.code === "ENOENT") {
configLogger.error('Configuration file not found', null, true); configLogger.error("Configuration file not found", null, true);
process.exit(1); process.exit(1);
} else if (e instanceof Error) { } else if (e instanceof Error) {
configLogger.error(e.message); configLogger.error(e.message);
@ -122,22 +144,24 @@ function loadConfigBoot(): Config {
throw exception; throw exception;
} }
configLogger.succ('Loaded'); configLogger.succ("Loaded");
return config; return config;
} }
async function connectDb(): Promise<void> { async function connectDb(): Promise<void> {
const dbLogger = bootLogger.createSubLogger('db'); const dbLogger = bootLogger.createSubLogger("db");
// Try to connect to DB // Try to connect to DB
try { try {
dbLogger.info('Connecting...'); dbLogger.info("Connecting...");
await initDb(); 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}`); dbLogger.succ(`Connected: v${v}`);
} catch (e) { } catch (e) {
dbLogger.error('Cannot connect', null, true); dbLogger.error("Cannot connect", null, true);
dbLogger.error(e); dbLogger.error(e);
process.exit(1); process.exit(1);
} }
@ -145,20 +169,20 @@ async function connectDb(): Promise<void> {
async function spawnWorkers(limit: number = 1) { async function spawnWorkers(limit: number = 1) {
const workers = Math.min(limit, os.cpus().length); 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)); await Promise.all([...Array(workers)].map(spawnWorker));
bootLogger.succ('All workers started'); bootLogger.succ("All workers started");
} }
function spawnWorker(): Promise<void> { function spawnWorker(): Promise<void> {
return new Promise(res => { return new Promise((res) => {
const worker = cluster.fork(); const worker = cluster.fork();
worker.on('message', message => { worker.on("message", (message) => {
if (message === 'listenFailed') { if (message === "listenFailed") {
bootLogger.error(`The server Listen failed due to the previous error.`); bootLogger.error("The server Listen failed due to the previous error.");
process.exit(1); process.exit(1);
} }
if (message !== 'ready') return; if (message !== "ready") return;
res(); res();
}); });
}); });

View file

@ -1,5 +1,5 @@
import cluster from 'node:cluster'; import cluster from "node:cluster";
import { initDb } from '../db/postgre.js'; import { initDb } from "../db/postgre.js";
/** /**
* Init worker process * Init worker process
@ -8,13 +8,13 @@ export async function workerMain() {
await initDb(); await initDb();
// start server // start server
await import('../server/index.js').then(x => x.default()); await import("../server/index.js").then((x) => x.default());
// start job queue // start job queue
import('../queue/index.js').then(x => x.default()); import("../queue/index.js").then((x) => x.default());
if (cluster.isWorker) { if (cluster.isWorker) {
// Send a 'ready' message to parent process // Send a 'ready' message to parent process
process.send!('ready'); process.send!("ready");
} }
} }

View file

@ -1,3 +1,3 @@
import load from './load.js'; import load from "./load.js";
export default load(); export default load();

View file

@ -2,11 +2,11 @@
* Config loader * Config loader
*/ */
import * as fs from 'node:fs'; import * as fs from "node:fs";
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from "node:url";
import { dirname } from 'node:path'; import { dirname } from "node:path";
import * as yaml from 'js-yaml'; import * as yaml from "js-yaml";
import type { Source, Mixin } from './types.js'; import type { Source, Mixin } from "./types.js";
const _filename = fileURLToPath(import.meta.url); const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename); const _dirname = dirname(_filename);
@ -19,14 +19,20 @@ const dir = `${_dirname}/../../../../.config`;
/** /**
* Path of configuration file * Path of configuration file
*/ */
const path = process.env.NODE_ENV === 'test' const path =
? `${dir}/test.yml` process.env.NODE_ENV === "test" ? `${dir}/test.yml` : `${dir}/default.yml`;
: `${dir}/default.yml`;
export default function load() { export default function load() {
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8')); const meta = JSON.parse(
const clientManifest = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/_client_dist_/manifest.json`, 'utf-8')); fs.readFileSync(`${_dirname}/../../../../built/meta.json`, "utf-8"),
const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source; );
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; const mixin = {} as Mixin;
@ -34,19 +40,19 @@ export default function load() {
config.url = url.origin; 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.version = meta.version;
mixin.host = url.host; mixin.host = url.host;
mixin.hostname = url.hostname; mixin.hostname = url.hostname;
mixin.scheme = url.protocol.replace(/:$/, ''); mixin.scheme = url.protocol.replace(/:$/, "");
mixin.wsScheme = mixin.scheme.replace('http', 'ws'); mixin.wsScheme = mixin.scheme.replace("http", "ws");
mixin.wsUrl = `${mixin.wsScheme}://${mixin.host}`; mixin.wsUrl = `${mixin.wsScheme}://${mixin.host}`;
mixin.apiUrl = `${mixin.scheme}://${mixin.host}/api`; mixin.apiUrl = `${mixin.scheme}://${mixin.host}/api`;
mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`; mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`;
mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`; mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`;
mixin.userAgent = `Calckey/${meta.version} (${config.url})`; 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; if (!config.redis.prefix) config.redis.prefix = mixin.host;

View file

@ -47,7 +47,7 @@ export type Source = {
id: string; id: string;
outgoingAddressFamily?: 'ipv4' | 'ipv6' | 'dual'; outgoingAddressFamily?: "ipv4" | "ipv6" | "dual";
deliverJobConcurrency?: number; deliverJobConcurrency?: number;
inboxJobConcurrency?: number; inboxJobConcurrency?: number;
@ -64,6 +64,12 @@ export type Source = {
mediaProxy?: string; mediaProxy?: string;
proxyRemoteFiles?: boolean; proxyRemoteFiles?: boolean;
twa: {
nameSpace?: string;
packageName?: string;
sha256CertFingerprints?: string[];
};
// Managed hosting stuff // Managed hosting stuff
maxUserSignups?: number; maxUserSignups?: number;
isManagedHosting?: boolean; isManagedHosting?: boolean;
@ -81,7 +87,6 @@ export type Source = {
user?: string; user?: string;
pass?: string; pass?: string;
useImplicitSslTls?: boolean; useImplicitSslTls?: boolean;
}; };
objectStorage: { objectStorage: {
managed?: boolean; managed?: boolean;

View file

@ -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 SECOND = 1000;
export const SEC = 1000; export const SEC = 1000; // why do we need this duplicate here?
export const MINUTE = 60 * SEC; 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 HOUR = 60 * MIN;
export const DAY = 24 * HOUR; export const DAY = 24 * HOUR;
export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min export const USER_ONLINE_THRESHOLD = 10 * MINUTE;
export const USER_ACTIVE_THRESHOLD = 1000 * 60 * 60 * 24 * 3; // 3days export const USER_ACTIVE_THRESHOLD = 3 * DAY;
// ブラウザで直接表示することを許可するファイルの種類のリスト // List of file types allowed to be viewed directly in the browser
// ここに含まれないものは application/octet-stream としてレスポンスされる // Anything not included here will be responded as application/octet-stream
// SVGはXSSを生むので許可しない // 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 = [ export const FILE_TYPE_BROWSERSAFE = [
// Images // Images
'image/png', "image/png",
'image/gif', "image/gif", // TODO: deprecated, but still used by old notes, new gifs should be converted to webp in the future
'image/jpeg', "image/jpeg",
'image/webp', "image/webp", // TODO: make this the default image format
'image/apng', "image/apng",
'image/bmp', "image/bmp",
'image/tiff', "image/tiff",
'image/x-icon', "image/x-icon",
"image/avif", // not as good supported now, but its good to introduce initial support for the future
// OggS // OggS
'audio/opus', "audio/opus",
'video/ogg', "video/ogg",
'audio/ogg', "audio/ogg",
'application/ogg', "application/ogg",
// ISO/IEC base media file format // ISO/IEC base media file format
'video/quicktime', "video/quicktime",
'video/mp4', "video/mp4", // TODO: we need to check for av1 later
'audio/mp4', "video/vnd.avi", // also av1
'video/x-m4v', "audio/mp4",
'audio/x-m4a', "video/x-m4v",
'video/3gpp', "audio/x-m4a",
'video/3gpp2', "video/3gpp",
"video/3gpp2",
"video/3gp2",
"audio/3gpp",
"audio/3gpp2",
"audio/3gp2",
'video/mpeg', "video/mpeg",
'audio/mpeg', "audio/mpeg",
'video/webm', "video/webm",
'audio/webm', "audio/webm",
'audio/aac', "audio/aac",
'audio/x-flac', "audio/x-flac",
'audio/vnd.wave', "audio/flac",
"audio/vnd.wave",
]; ];
/* /*
https://github.com/sindresorhus/file-type/blob/main/supported.js https://github.com/sindresorhus/file-type/blob/main/supported.js

View file

@ -1,13 +1,13 @@
// TODO: 消したい // TODO: 消したい
const interval = 30 * 60 * 1000; const interval = 30 * 60 * 1000;
import { AttestationChallenges } from '@/models/index.js'; import { AttestationChallenges } from "@/models/index.js";
import { LessThan } from 'typeorm'; import { LessThan } from "typeorm";
/** /**
* Clean up database occasionally * Clean up database occasionally
*/ */
export default function() { export default function () {
async function tick() { async function tick() {
await AttestationChallenges.delete({ await AttestationChallenges.delete({
createdAt: LessThan(new Date(new Date().getTime() - 5 * 60 * 1000)), createdAt: LessThan(new Date(new Date().getTime() - 5 * 60 * 1000)),

View file

@ -1,5 +1,5 @@
import Xev from 'xev'; import Xev from "xev";
import { deliverQueue, inboxQueue } from '../queue/queues.js'; import { deliverQueue, inboxQueue } from "../queue/queues.js";
const ev = new Xev(); const ev = new Xev();
@ -8,21 +8,21 @@ const interval = 10000;
/** /**
* Report queue stats regularly * Report queue stats regularly
*/ */
export default function() { export default function () {
const log = [] as any[]; const log = [] as any[];
ev.on('requestQueueStatsLog', x => { ev.on("requestQueueStatsLog", (x) => {
ev.emit(`queueStatsLog:${x.id}`, log.slice(0, x.length || 50)); ev.emit(`queueStatsLog:${x.id}`, log.slice(0, x.length || 50));
}); });
let activeDeliverJobs = 0; let activeDeliverJobs = 0;
let activeInboxJobs = 0; let activeInboxJobs = 0;
deliverQueue.on('global:active', () => { deliverQueue.on("global:active", () => {
activeDeliverJobs++; activeDeliverJobs++;
}); });
inboxQueue.on('global:active', () => { inboxQueue.on("global:active", () => {
activeInboxJobs++; activeInboxJobs++;
}); });
@ -45,7 +45,7 @@ export default function() {
}, },
}; };
ev.emit('queueStats', stats); ev.emit("queueStats", stats);
log.unshift(stats); log.unshift(stats);
if (log.length > 200) log.pop(); if (log.length > 200) log.pop();

View file

@ -1,6 +1,6 @@
import si from 'systeminformation'; import si from "systeminformation";
import Xev from 'xev'; import Xev from "xev";
import * as osUtils from 'os-utils'; import * as osUtils from "os-utils";
const ev = new Xev(); const ev = new Xev();
@ -12,10 +12,10 @@ const round = (num: number) => Math.round(num * 10) / 10;
/** /**
* Report server stats regularly * Report server stats regularly
*/ */
export default function() { export default function () {
const log = [] as any[]; const log = [] as any[];
ev.on('requestServerStatsLog', x => { ev.on("requestServerStatsLog", (x) => {
ev.emit(`serverStatsLog:${x.id}`, log.slice(0, x.length || 50)); 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)), w: round(Math.max(0, fsStats.wIO_sec ?? 0)),
}, },
}; };
ev.emit('serverStats', stats); ev.emit("serverStats", stats);
log.unshift(stats); log.unshift(stats);
if (log.length > 200) log.pop(); if (log.length > 200) log.pop();
} }

View file

@ -1,12 +1,12 @@
import * as elasticsearch from '@elastic/elasticsearch'; import * as elasticsearch from "@elastic/elasticsearch";
import config from '@/config/index.js'; import config from "@/config/index.js";
const index = { const index = {
settings: { settings: {
analysis: { analysis: {
analyzer: { analyzer: {
ngram: { ngram: {
tokenizer: 'ngram', tokenizer: "ngram",
}, },
}, },
}, },
@ -14,16 +14,16 @@ const index = {
mappings: { mappings: {
properties: { properties: {
text: { text: {
type: 'text', type: "text",
index: true, index: true,
analyzer: 'ngram', analyzer: "ngram",
}, },
userId: { userId: {
type: 'keyword', type: "keyword",
index: true, index: true,
}, },
userHost: { userHost: {
type: 'keyword', type: "keyword",
index: true, index: true,
}, },
}, },
@ -31,26 +31,35 @@ const index = {
}; };
// Init ElasticSearch connection // Init ElasticSearch connection
const client = config.elasticsearch ? new elasticsearch.Client({ const client = config.elasticsearch
node: `${config.elasticsearch.ssl ? 'https://' : 'http://'}${config.elasticsearch.host}:${config.elasticsearch.port}`, ? new elasticsearch.Client({
auth: (config.elasticsearch.user && config.elasticsearch.pass) ? { node: `${config.elasticsearch.ssl ? "https://" : "http://"}${
username: config.elasticsearch.user, config.elasticsearch.host
password: config.elasticsearch.pass, }:${config.elasticsearch.port}`,
} : undefined, auth:
pingTimeout: 30000, config.elasticsearch.user && config.elasticsearch.pass
}) : null; ? {
username: config.elasticsearch.user,
password: config.elasticsearch.pass,
}
: undefined,
pingTimeout: 30000,
})
: null;
if (client) { if (client) {
client.indices.exists({ client.indices
index: config.elasticsearch.index || 'misskey_note', .exists({
}).then(exist => { index: config.elasticsearch.index || "misskey_note",
if (!exist.body) { })
client.indices.create({ .then((exist) => {
index: config.elasticsearch.index || 'misskey_note', if (!exist.body) {
body: index, client.indices.create({
}); index: config.elasticsearch.index || "misskey_note",
} body: index,
}); });
}
});
} }
export default client; export default client;

View file

@ -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");

View file

@ -1,87 +1,89 @@
// https://github.com/typeorm/typeorm/issues/2400 // https://github.com/typeorm/typeorm/issues/2400
import pg from 'pg'; import pg from "pg";
pg.types.setTypeParser(20, Number); pg.types.setTypeParser(20, Number);
import { Logger, DataSource } from 'typeorm'; import type { Logger } from "typeorm";
import * as highlight from 'cli-highlight'; import { DataSource } from "typeorm";
import config from '@/config/index.js'; import * as highlight from "cli-highlight";
import config from "@/config/index.js";
import { User } from '@/models/entities/user.js'; import { User } from "@/models/entities/user.js";
import { DriveFile } from '@/models/entities/drive-file.js'; import { DriveFile } from "@/models/entities/drive-file.js";
import { DriveFolder } from '@/models/entities/drive-folder.js'; import { DriveFolder } from "@/models/entities/drive-folder.js";
import { AccessToken } from '@/models/entities/access-token.js'; import { AccessToken } from "@/models/entities/access-token.js";
import { App } from '@/models/entities/app.js'; import { App } from "@/models/entities/app.js";
import { PollVote } from '@/models/entities/poll-vote.js'; import { PollVote } from "@/models/entities/poll-vote.js";
import { Note } from '@/models/entities/note.js'; import { Note } from "@/models/entities/note.js";
import { NoteReaction } from '@/models/entities/note-reaction.js'; import { NoteReaction } from "@/models/entities/note-reaction.js";
import { NoteWatching } from '@/models/entities/note-watching.js'; import { NoteWatching } from "@/models/entities/note-watching.js";
import { NoteThreadMuting } from '@/models/entities/note-thread-muting.js'; import { NoteThreadMuting } from "@/models/entities/note-thread-muting.js";
import { NoteUnread } from '@/models/entities/note-unread.js'; import { NoteUnread } from "@/models/entities/note-unread.js";
import { Notification } from '@/models/entities/notification.js'; import { Notification } from "@/models/entities/notification.js";
import { Meta } from '@/models/entities/meta.js'; import { Meta } from "@/models/entities/meta.js";
import { Following } from '@/models/entities/following.js'; import { Following } from "@/models/entities/following.js";
import { Instance } from '@/models/entities/instance.js'; import { Instance } from "@/models/entities/instance.js";
import { Muting } from '@/models/entities/muting.js'; import { Muting } from "@/models/entities/muting.js";
import { SwSubscription } from '@/models/entities/sw-subscription.js'; import { SwSubscription } from "@/models/entities/sw-subscription.js";
import { Blocking } from '@/models/entities/blocking.js'; import { Blocking } from "@/models/entities/blocking.js";
import { UserList } from '@/models/entities/user-list.js'; import { UserList } from "@/models/entities/user-list.js";
import { UserListJoining } from '@/models/entities/user-list-joining.js'; import { UserListJoining } from "@/models/entities/user-list-joining.js";
import { UserGroup } from '@/models/entities/user-group.js'; import { UserGroup } from "@/models/entities/user-group.js";
import { UserGroupJoining } from '@/models/entities/user-group-joining.js'; import { UserGroupJoining } from "@/models/entities/user-group-joining.js";
import { UserGroupInvitation } from '@/models/entities/user-group-invitation.js'; import { UserGroupInvitation } from "@/models/entities/user-group-invitation.js";
import { Hashtag } from '@/models/entities/hashtag.js'; import { Hashtag } from "@/models/entities/hashtag.js";
import { NoteFavorite } from '@/models/entities/note-favorite.js'; import { NoteFavorite } from "@/models/entities/note-favorite.js";
import { AbuseUserReport } from '@/models/entities/abuse-user-report.js'; import { AbuseUserReport } from "@/models/entities/abuse-user-report.js";
import { RegistrationTicket } from '@/models/entities/registration-tickets.js'; import { RegistrationTicket } from "@/models/entities/registration-tickets.js";
import { MessagingMessage } from '@/models/entities/messaging-message.js'; import { MessagingMessage } from "@/models/entities/messaging-message.js";
import { Signin } from '@/models/entities/signin.js'; import { Signin } from "@/models/entities/signin.js";
import { AuthSession } from '@/models/entities/auth-session.js'; import { AuthSession } from "@/models/entities/auth-session.js";
import { FollowRequest } from '@/models/entities/follow-request.js'; import { FollowRequest } from "@/models/entities/follow-request.js";
import { Emoji } from '@/models/entities/emoji.js'; import { Emoji } from "@/models/entities/emoji.js";
import { UserNotePining } from '@/models/entities/user-note-pining.js'; import { UserNotePining } from "@/models/entities/user-note-pining.js";
import { Poll } from '@/models/entities/poll.js'; import { Poll } from "@/models/entities/poll.js";
import { UserKeypair } from '@/models/entities/user-keypair.js'; import { UserKeypair } from "@/models/entities/user-keypair.js";
import { UserPublickey } from '@/models/entities/user-publickey.js'; import { UserPublickey } from "@/models/entities/user-publickey.js";
import { UserProfile } from '@/models/entities/user-profile.js'; import { UserProfile } from "@/models/entities/user-profile.js";
import { UserSecurityKey } from '@/models/entities/user-security-key.js'; import { UserSecurityKey } from "@/models/entities/user-security-key.js";
import { AttestationChallenge } from '@/models/entities/attestation-challenge.js'; import { AttestationChallenge } from "@/models/entities/attestation-challenge.js";
import { Page } from '@/models/entities/page.js'; import { Page } from "@/models/entities/page.js";
import { PageLike } from '@/models/entities/page-like.js'; import { PageLike } from "@/models/entities/page-like.js";
import { GalleryPost } from '@/models/entities/gallery-post.js'; import { GalleryPost } from "@/models/entities/gallery-post.js";
import { GalleryLike } from '@/models/entities/gallery-like.js'; import { GalleryLike } from "@/models/entities/gallery-like.js";
import { ModerationLog } from '@/models/entities/moderation-log.js'; import { ModerationLog } from "@/models/entities/moderation-log.js";
import { UsedUsername } from '@/models/entities/used-username.js'; import { UsedUsername } from "@/models/entities/used-username.js";
import { Announcement } from '@/models/entities/announcement.js'; import { Announcement } from "@/models/entities/announcement.js";
import { AnnouncementRead } from '@/models/entities/announcement-read.js'; import { AnnouncementRead } from "@/models/entities/announcement-read.js";
import { Clip } from '@/models/entities/clip.js'; import { Clip } from "@/models/entities/clip.js";
import { ClipNote } from '@/models/entities/clip-note.js'; import { ClipNote } from "@/models/entities/clip-note.js";
import { Antenna } from '@/models/entities/antenna.js'; import { Antenna } from "@/models/entities/antenna.js";
import { AntennaNote } from '@/models/entities/antenna-note.js'; import { AntennaNote } from "@/models/entities/antenna-note.js";
import { PromoNote } from '@/models/entities/promo-note.js'; import { PromoNote } from "@/models/entities/promo-note.js";
import { PromoRead } from '@/models/entities/promo-read.js'; import { PromoRead } from "@/models/entities/promo-read.js";
import { Relay } from '@/models/entities/relay.js'; import { Relay } from "@/models/entities/relay.js";
import { MutedNote } from '@/models/entities/muted-note.js'; import { MutedNote } from "@/models/entities/muted-note.js";
import { Channel } from '@/models/entities/channel.js'; import { Channel } from "@/models/entities/channel.js";
import { ChannelFollowing } from '@/models/entities/channel-following.js'; import { ChannelFollowing } from "@/models/entities/channel-following.js";
import { ChannelNotePining } from '@/models/entities/channel-note-pining.js'; import { ChannelNotePining } from "@/models/entities/channel-note-pining.js";
import { RegistryItem } from '@/models/entities/registry-item.js'; import { RegistryItem } from "@/models/entities/registry-item.js";
import { Ad } from '@/models/entities/ad.js'; import { Ad } from "@/models/entities/ad.js";
import { PasswordResetRequest } from '@/models/entities/password-reset-request.js'; import { PasswordResetRequest } from "@/models/entities/password-reset-request.js";
import { UserPending } from '@/models/entities/user-pending.js'; import { UserPending } from "@/models/entities/user-pending.js";
import { Webhook } from '@/models/entities/webhook.js'; import { Webhook } from "@/models/entities/webhook.js";
import { UserIp } from '@/models/entities/user-ip.js'; import { UserIp } from "@/models/entities/user-ip.js";
import { entities as charts } from '@/services/chart/entities.js'; import { entities as charts } from "@/services/chart/entities.js";
import { envOption } from '../env.js'; import { envOption } from "../env.js";
import { dbLogger } from './logger.js'; import { dbLogger } from "./logger.js";
import { redisClient } from './redis.js'; import { redisClient } from "./redis.js";
const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false); const sqlLogger = dbLogger.createSubLogger("sql", "gray", false);
class MyCustomLogger implements Logger { class MyCustomLogger implements Logger {
private highlight(sql: string) { private highlight(sql: string) {
return highlight.highlight(sql, { return highlight.highlight(sql, {
language: 'sql', ignoreIllegals: true, language: "sql",
ignoreIllegals: true,
}); });
} }
@ -178,10 +180,10 @@ export const entities = [
...charts, ...charts,
]; ];
const log = process.env.NODE_ENV !== 'production'; const log = process.env.NODE_ENV !== "production";
export const db = new DataSource({ export const db = new DataSource({
type: 'postgres', type: "postgres",
host: config.db.host, host: config.db.host,
port: config.db.port, port: config.db.port,
username: config.db.user, username: config.db.user,
@ -191,24 +193,26 @@ export const db = new DataSource({
statement_timeout: 1000 * 10, statement_timeout: 1000 * 10,
...config.db.extra, ...config.db.extra,
}, },
synchronize: process.env.NODE_ENV === 'test', synchronize: process.env.NODE_ENV === "test",
dropSchema: process.env.NODE_ENV === 'test', dropSchema: process.env.NODE_ENV === "test",
cache: !config.db.disableCache ? { cache: !config.db.disableCache
type: 'ioredis', ? {
options: { type: "ioredis",
host: config.redis.host, options: {
port: config.redis.port, host: config.redis.host,
family: config.redis.family == null ? 0 : config.redis.family, port: config.redis.port,
password: config.redis.pass, family: config.redis.family == null ? 0 : config.redis.family,
keyPrefix: `${config.redis.prefix}:query:`, password: config.redis.pass,
db: config.redis.db || 0, keyPrefix: `${config.redis.prefix}:query:`,
}, db: config.redis.db || 0,
} : false, },
}
: false,
logging: log, logging: log,
logger: log ? new MyCustomLogger() : undefined, logger: log ? new MyCustomLogger() : undefined,
maxQueryExecutionTime: 300, maxQueryExecutionTime: 300,
entities: entities, entities: entities,
migrations: ['../../migration/*.js'], migrations: ["../../migration/*.js"],
}); });
export async function initDb(force = false) { export async function initDb(force = false) {
@ -247,7 +251,7 @@ export async function resetDb() {
if (i === 3) { if (i === 3) {
throw e; throw e;
} else { } else {
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise((resolve) => setTimeout(resolve, 1000));
continue; continue;
} }
} }

View file

@ -1,5 +1,5 @@
import Redis from 'ioredis'; import Redis from "ioredis";
import config from '@/config/index.js'; import config from "@/config/index.js";
export function createConnection() { export function createConnection() {
return new Redis({ return new Redis({

View file

@ -10,11 +10,16 @@ const envOption = {
}; };
for (const key of Object.keys(envOption) as (keyof typeof 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.disableClustering = true;
if (process.env.NODE_ENV === 'test') envOption.quiet = 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.noDaemons = true;
export { envOption }; export { envOption };

View file

@ -1 +1,2 @@
// rome-ignore lint/suspicious/noExplicitAny: i have no idea
type FIXME = any; type FIXME = any;

View file

@ -2,12 +2,12 @@
* Misskey Entry Point! * Misskey Entry Point!
*/ */
import { EventEmitter } from 'node:events'; import { EventEmitter } from "node:events";
import boot from './boot/index.js'; import boot from "./boot/index.js";
Error.stackTraceLimit = Infinity; Error.stackTraceLimit = Infinity;
EventEmitter.defaultMaxListeners = 128; EventEmitter.defaultMaxListeners = 128;
boot().catch(err => { boot().catch((err) => {
console.error(err); console.error(err);
}); });

View file

@ -1,6 +1,6 @@
import { URL } from 'node:url'; import { URL } from "node:url";
import * as parse5 from 'parse5'; import * as parse5 from "parse5";
import * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js'; import * as TreeAdapter from "../../node_modules/parse5/dist/tree-adapters/default.js";
const treeAdapter = TreeAdapter.defaultTreeAdapter; const treeAdapter = TreeAdapter.defaultTreeAdapter;
@ -9,11 +9,11 @@ const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
export function fromHtml(html: string, hashtagNames?: string[]): string { export function fromHtml(html: string, hashtagNames?: string[]): string {
// some AP servers like Pixelfed use br tags as well as newlines // 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); const dom = parse5.parseFragment(html);
let text = ''; let text = "";
for (const n of dom.childNodes) { for (const n of dom.childNodes) {
analyze(n); analyze(n);
@ -23,14 +23,14 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
function getText(node: TreeAdapter.Node): string { function getText(node: TreeAdapter.Node): string {
if (treeAdapter.isTextNode(node)) return node.value; if (treeAdapter.isTextNode(node)) return node.value;
if (!treeAdapter.isElementNode(node)) return ''; if (!treeAdapter.isElementNode(node)) return "";
if (node.nodeName === 'br') return '\n'; if (node.nodeName === "br") return "\n";
if (node.childNodes) { 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 { function appendChildren(childNodes: TreeAdapter.ChildNode[]): void {
@ -51,42 +51,46 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
if (!treeAdapter.isElementNode(node)) return; if (!treeAdapter.isElementNode(node)) return;
switch (node.nodeName) { switch (node.nodeName) {
case 'br': { case "br": {
text += '\n'; text += "\n";
break; break;
} }
case 'a': case "a": {
{
const txt = getText(node); const txt = getText(node);
const rel = node.attrs.find(x => x.name === 'rel'); const rel = node.attrs.find((x) => x.name === "rel");
const href = node.attrs.find(x => x.name === 'href'); 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; text += txt;
// メンション // メンション
} else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) { } else if (txt.startsWith("@") && !rel?.value.match(/^me /)) {
const part = txt.split('@'); const part = txt.split("@");
if (part.length === 2 && href) { if (part.length === 2 && href) {
//#region ホスト名部分が省略されているので復元する //#region ホスト名部分が省略されているので復元する
const acct = `${txt}@${(new URL(href.value)).hostname}`; const acct = `${txt}@${new URL(href.value).hostname}`;
text += acct; text += acct;
//#endregion //#endregion
} else if (part.length === 3) { } else if (part.length === 3) {
text += txt; text += txt;
} }
// その他 // その他
} else { } else {
const generateLink = () => { const generateLink = () => {
if (!href && !txt) { if (!(href || txt)) {
return ''; return "";
} }
if (!href) { if (!href) {
return txt; 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)) { if (href.value.match(urlRegexFull)) {
return href.value; return href.value;
} else { } else {
@ -94,7 +98,7 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
} }
} }
if (href.value.match(urlRegex) && !href.value.match(urlRegexFull)) { if (href.value.match(urlRegex) && !href.value.match(urlRegexFull)) {
return `[${txt}](<${href.value}>)`; // #6846 return `[${txt}](<${href.value}>)`; // #6846
} else { } else {
return `[${txt}](${href.value})`; return `[${txt}](${href.value})`;
} }
@ -105,55 +109,53 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
break; break;
} }
case 'h1': case "h1": {
{ text += "【";
text += '【';
appendChildren(node.childNodes); appendChildren(node.childNodes);
text += '】\n'; text += "】\n";
break; break;
} }
case 'b': case "b":
case 'strong': case "strong": {
{ text += "**";
text += '**';
appendChildren(node.childNodes); appendChildren(node.childNodes);
text += '**'; text += "**";
break; break;
} }
case 'small': case "small": {
{ text += "<small>";
text += '<small>';
appendChildren(node.childNodes); appendChildren(node.childNodes);
text += '</small>'; text += "</small>";
break; break;
} }
case 's': case "s":
case 'del': case "del": {
{ text += "~~";
text += '~~';
appendChildren(node.childNodes); appendChildren(node.childNodes);
text += '~~'; text += "~~";
break; break;
} }
case 'i': case "i":
case 'em': case "em": {
{ text += "<i>";
text += '<i>';
appendChildren(node.childNodes); appendChildren(node.childNodes);
text += '</i>'; text += "</i>";
break; break;
} }
// block code (<pre><code>) // block code (<pre><code>)
case 'pre': { case "pre": {
if (node.childNodes.length === 1 && node.childNodes[0].nodeName === 'code') { if (
text += '\n```\n'; node.childNodes.length === 1 &&
node.childNodes[0].nodeName === "code"
) {
text += "\n```\n";
text += getText(node.childNodes[0]); text += getText(node.childNodes[0]);
text += '\n```\n'; text += "\n```\n";
} else { } else {
appendChildren(node.childNodes); appendChildren(node.childNodes);
} }
@ -161,50 +163,48 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
} }
// inline code (<code>) // inline code (<code>)
case 'code': { case "code": {
text += '`'; text += "`";
appendChildren(node.childNodes); appendChildren(node.childNodes);
text += '`'; text += "`";
break; break;
} }
case 'blockquote': { case "blockquote": {
const t = getText(node); const t = getText(node);
if (t) { if (t) {
text += '\n> '; text += "\n> ";
text += t.split('\n').join('\n> '); text += t.split("\n").join("\n> ");
} }
break; break;
} }
case 'p': case "p":
case 'h2': case "h2":
case 'h3': case "h3":
case 'h4': case "h4":
case 'h5': case "h5":
case 'h6': case "h6": {
{ text += "\n\n";
text += '\n\n';
appendChildren(node.childNodes); appendChildren(node.childNodes);
break; break;
} }
// other block elements // other block elements
case 'div': case "div":
case 'header': case "header":
case 'footer': case "footer":
case 'article': case "article":
case 'li': case "li":
case 'dt': case "dt":
case 'dd': case "dd": {
{ text += "\n";
text += '\n';
appendChildren(node.childNodes); appendChildren(node.childNodes);
break; break;
} }
default: // includes inline elements default: {
{ // includes inline elements
appendChildren(node.childNodes); appendChildren(node.childNodes);
break; break;
} }

View file

@ -1,65 +1,71 @@
import { JSDOM } from 'jsdom'; import { JSDOM } from "jsdom";
import * as mfm from 'mfm-js'; import type * as mfm from "mfm-js";
import config from '@/config/index.js'; import config from "@/config/index.js";
import { intersperse } from '@/prelude/array.js'; import { intersperse } from "@/prelude/array.js";
import { IMentionedRemoteUsers } from '@/models/entities/note.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) { if (nodes == null) {
return null; return null;
} }
const { window } = new JSDOM(''); const { window } = new JSDOM("");
const doc = window.document; const doc = window.document;
function appendChildren(children: mfm.MfmNode[], targetElement: any): void { function appendChildren(children: mfm.MfmNode[], targetElement: any): void {
if (children) { 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) { bold(node) {
const el = doc.createElement('b'); const el = doc.createElement("b");
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
small(node) { small(node) {
const el = doc.createElement('small'); const el = doc.createElement("small");
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
strike(node) { strike(node) {
const el = doc.createElement('del'); const el = doc.createElement("del");
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
italic(node) { italic(node) {
const el = doc.createElement('i'); const el = doc.createElement("i");
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
fn(node) { fn(node) {
const el = doc.createElement('i'); const el = doc.createElement("i");
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
blockCode(node) { blockCode(node) {
const pre = doc.createElement('pre'); const pre = doc.createElement("pre");
const inner = doc.createElement('code'); const inner = doc.createElement("code");
inner.textContent = node.props.code; inner.textContent = node.props.code;
pre.appendChild(inner); pre.appendChild(inner);
return pre; return pre;
}, },
center(node) { center(node) {
const el = doc.createElement('div'); const el = doc.createElement("div");
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
@ -73,81 +79,90 @@ export function toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMenti
}, },
hashtag(node) { hashtag(node) {
const a = doc.createElement('a'); const a = doc.createElement("a");
a.href = `${config.url}/tags/${node.props.hashtag}`; a.href = `${config.url}/tags/${node.props.hashtag}`;
a.textContent = `#${node.props.hashtag}`; a.textContent = `#${node.props.hashtag}`;
a.setAttribute('rel', 'tag'); a.setAttribute("rel", "tag");
return a; return a;
}, },
inlineCode(node) { inlineCode(node) {
const el = doc.createElement('code'); const el = doc.createElement("code");
el.textContent = node.props.code; el.textContent = node.props.code;
return el; return el;
}, },
mathInline(node) { mathInline(node) {
const el = doc.createElement('code'); const el = doc.createElement("code");
el.textContent = node.props.formula; el.textContent = node.props.formula;
return el; return el;
}, },
mathBlock(node) { mathBlock(node) {
const el = doc.createElement('code'); const el = doc.createElement("code");
el.textContent = node.props.formula; el.textContent = node.props.formula;
return el; return el;
}, },
link(node) { link(node) {
const a = doc.createElement('a'); const a = doc.createElement("a");
a.href = node.props.url; a.href = node.props.url;
appendChildren(node.children, a); appendChildren(node.children, a);
return a; return a;
}, },
mention(node) { mention(node) {
const a = doc.createElement('a'); const a = doc.createElement("a");
const { username, host, acct } = node.props; const { username, host, acct } = node.props;
const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host); const remoteUserInfo = mentionedRemoteUsers.find(
a.href = remoteUserInfo ? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri) : `${config.url}/${acct}`; (remoteUser) =>
a.className = 'u-url mention'; 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; a.textContent = acct;
return a; return a;
}, },
quote(node) { quote(node) {
const el = doc.createElement('blockquote'); const el = doc.createElement("blockquote");
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },
text(node) { text(node) {
const el = doc.createElement('span'); const el = doc.createElement("span");
const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x)); const nodes = node.props.text
.split(/\r\n|\r|\n/)
.map((x) => doc.createTextNode(x));
for (const x of intersperse<FIXME | 'br'>('br', nodes)) { for (const x of intersperse<FIXME | "br">("br", nodes)) {
el.appendChild(x === 'br' ? doc.createElement('br') : x); el.appendChild(x === "br" ? doc.createElement("br") : x);
} }
return el; return el;
}, },
url(node) { url(node) {
const a = doc.createElement('a'); const a = doc.createElement("a");
a.href = node.props.url; a.href = node.props.url;
a.textContent = node.props.url; a.textContent = node.props.url;
return a; return a;
}, },
search(node) { search(node) {
const a = doc.createElement('a'); const a = doc.createElement("a");
a.href = `https://search.annoyingorange.xyz/search?q=${node.props.query}`; a.href = `https://search.annoyingorange.xyz/search?q=${node.props.query}`;
a.textContent = node.props.content; a.textContent = node.props.content;
return a; return a;
}, },
plain(node) { plain(node) {
const el = doc.createElement('span'); const el = doc.createElement("span");
appendChildren(node.children, el); appendChildren(node.children, el);
return el; return el;
}, },

View file

@ -4,8 +4,8 @@ export type Acct = {
}; };
export function parse(acct: string): Acct { export function parse(acct: string): Acct {
if (acct.startsWith('@')) acct = acct.substr(1); if (acct.startsWith("@")) acct = acct.substr(1);
const split = acct.split('@', 2); const split = acct.split("@", 2);
return { username: split[0], host: split[1] || null }; return { username: split[0], host: split[1] || null };
} }

View file

@ -1,6 +1,6 @@
import { Antennas } from '@/models/index.js'; import { Antennas } from "@/models/index.js";
import { Antenna } from '@/models/entities/antenna.js'; import type { Antenna } from "@/models/entities/antenna.js";
import { subscriber } from '@/db/redis.js'; import { subscriber } from "@/db/redis.js";
let antennasFetched = false; let antennasFetched = false;
let antennas: Antenna[] = []; let antennas: Antenna[] = [];
@ -14,20 +14,20 @@ export async function getAntennas() {
return antennas; return antennas;
} }
subscriber.on('message', async (_, data) => { subscriber.on("message", async (_, data) => {
const obj = JSON.parse(data); const obj = JSON.parse(data);
if (obj.channel === 'internal') { if (obj.channel === "internal") {
const { type, body } = obj.message; const { type, body } = obj.message;
switch (type) { switch (type) {
case 'antennaCreated': case "antennaCreated":
antennas.push(body); antennas.push(body);
break; break;
case 'antennaUpdated': case "antennaUpdated":
antennas[antennas.findIndex(a => a.id === body.id)] = body; antennas[antennas.findIndex((a) => a.id === body.id)] = body;
break; break;
case 'antennaDeleted': case "antennaDeleted":
antennas = antennas.filter(a => a.id !== body.id); antennas = antennas.filter((a) => a.id !== body.id);
break; break;
default: default:
break; break;

View file

@ -1,35 +1,35 @@
export const kinds = [ export const kinds = [
'read:account', "read:account",
'write:account', "write:account",
'read:blocks', "read:blocks",
'write:blocks', "write:blocks",
'read:drive', "read:drive",
'write:drive', "write:drive",
'read:favorites', "read:favorites",
'write:favorites', "write:favorites",
'read:following', "read:following",
'write:following', "write:following",
'read:messaging', "read:messaging",
'write:messaging', "write:messaging",
'read:mutes', "read:mutes",
'write:mutes', "write:mutes",
'write:notes', "write:notes",
'read:notifications', "read:notifications",
'write:notifications', "write:notifications",
'read:reactions', "read:reactions",
'write:reactions', "write:reactions",
'write:votes', "write:votes",
'read:pages', "read:pages",
'write:pages', "write:pages",
'write:page-likes', "write:page-likes",
'read:page-likes', "read:page-likes",
'read:user-groups', "read:user-groups",
'write:user-groups', "write:user-groups",
'read:channels', "read:channels",
'write:channels', "write:channels",
'read:gallery', "read:gallery",
'write:gallery', "write:gallery",
'read:gallery-likes', "read:gallery-likes",
'write:gallery-likes', "write:gallery-likes",
]; ];
// IF YOU ADD KINDS(PERMISSIONS), YOU MUST ADD TRANSLATIONS (under _permissions). // IF YOU ADD KINDS(PERMISSIONS), YOU MUST ADD TRANSLATIONS (under _permissions).

View file

@ -1,16 +1,15 @@
import { redisClient } from '../db/redis.js'; import { redisClient } from "../db/redis.js";
import { promisify } from 'node:util'; import { promisify } from "node:util";
import redisLock from 'redis-lock'; import redisLock from "redis-lock";
/** /**
* Retry delay (ms) for lock acquisition * Retry delay (ms) for lock acquisition
*/ */
const retryDelay = 100; const retryDelay = 100;
const lock: (key: string, timeout?: number) => Promise<() => void> const lock: (key: string, timeout?: number) => Promise<() => void> = redisClient
= redisClient
? promisify(redisLock(redisClient, retryDelay)) ? promisify(redisLock(redisClient, retryDelay))
: async () => () => { }; : async () => () => {};
/** /**
* Get AP Object lock * Get AP Object lock
@ -22,7 +21,10 @@ export function getApLock(uri: string, timeout = 30 * 1000) {
return lock(`ap-object:${uri}`, timeout); 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); return lock(`instance:${host}`, timeout);
} }

View file

@ -1,6 +1,6 @@
// https://gist.github.com/nfantone/1eaa803772025df69d07f4dbf5df7e58 // https://gist.github.com/nfantone/1eaa803772025df69d07f4dbf5df7e58
'use strict'; "use strict";
/** /**
* @callback BeforeShutdownListener * @callback BeforeShutdownListener
@ -11,7 +11,7 @@
* System signals the app will listen to initiate shutdown. * System signals the app will listen to initiate shutdown.
* @const {string[]} * @const {string[]}
*/ */
const SHUTDOWN_SIGNALS = ['SIGINT', 'SIGTERM']; const SHUTDOWN_SIGNALS = ["SIGINT", "SIGTERM"];
/** /**
* Time in milliseconds to wait before forcing shutdown. * 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 {string[]} signals System signals to listen to.
* @param {function(string)} fn Function to execute on shutdown. * @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) { for (const sig of signals) {
process.once(sig, fn); process.once(sig, fn);
} }
@ -44,7 +47,9 @@ const processOnce = (signals: string[], fn: (signalOrEvent: string) => void) =>
const forceExitAfter = (timeout: number) => () => { const forceExitAfter = (timeout: number) => () => {
setTimeout(() => { setTimeout(() => {
// Force shutdown after timeout // 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); return process.exit(1);
}, timeout).unref(); }, timeout).unref();
}; };
@ -56,7 +61,7 @@ const forceExitAfter = (timeout: number) => () => {
* @param {string} signalOrEvent The exit signal or event name received on the process. * @param {string} signalOrEvent The exit signal or event name received on the process.
*/ */
async function shutdownHandler(signalOrEvent: string) { 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`); console.warn(`Shutting down: received [${signalOrEvent}] signal`);
@ -65,7 +70,11 @@ async function shutdownHandler(signalOrEvent: string) {
await listener(signalOrEvent); await listener(signalOrEvent);
} catch (err) { } catch (err) {
if (err instanceof Error) { 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
}`,
);
} }
} }
} }

View file

@ -1,8 +1,8 @@
export class Cache<T> { 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; private lifetime: number;
constructor(lifetime: Cache<never>['lifetime']) { constructor(lifetime: Cache<never>["lifetime"]) {
this.cache = new Map(); this.cache = new Map();
this.lifetime = lifetime; this.lifetime = lifetime;
} }
@ -17,7 +17,7 @@ export class Cache<T> {
public get(key: string | null): T | undefined { public get(key: string | null): T | undefined {
const cached = this.cache.get(key); const cached = this.cache.get(key);
if (cached == null) return undefined; if (cached == null) return undefined;
if ((Date.now() - cached.date) > this.lifetime) { if (Date.now() - cached.date > this.lifetime) {
this.cache.delete(key); this.cache.delete(key);
return undefined; return undefined;
} }
@ -32,7 +32,11 @@ export class Cache<T> {
* fetcherを呼び出して結果をキャッシュ& * fetcherを呼び出して結果をキャッシュ&
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします * 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); const cachedValue = this.get(key);
if (cachedValue !== undefined) { if (cachedValue !== undefined) {
if (validator) { if (validator) {
@ -56,7 +60,11 @@ export class Cache<T> {
* fetcherを呼び出して結果をキャッシュ& * fetcherを呼び出して結果をキャッシュ&
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします * 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); const cachedValue = this.get(key);
if (cachedValue !== undefined) { if (cachedValue !== undefined) {
if (validator) { if (validator) {

View file

@ -1,51 +1,67 @@
import fetch from 'node-fetch'; import fetch from "node-fetch";
import { URLSearchParams } from 'node:url'; import { URLSearchParams } from "node:url";
import { getAgentByUrl } from './fetch.js'; import { getAgentByUrl } from "./fetch.js";
import config from '@/config/index.js'; import config from "@/config/index.js";
export async function verifyRecaptcha(secret: string, response: string) { 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}`); throw new Error(`recaptcha-request-failed: ${e.message}`);
}); });
if (result.success !== true) { 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}`); throw new Error(`recaptcha-failed: ${errorCodes}`);
} }
} }
export async function verifyHcaptcha(secret: string, response: string) { 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}`); throw new Error(`hcaptcha-request-failed: ${e.message}`);
}); });
if (result.success !== true) { 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}`); throw new Error(`hcaptcha-failed: ${errorCodes}`);
} }
} }
type CaptchaResponse = { type CaptchaResponse = {
success: boolean; 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({ const params = new URLSearchParams({
secret, secret,
response, response,
}); });
const res = await fetch(url, { const res = await fetch(url, {
method: 'POST', method: "POST",
body: params, body: params,
headers: { headers: {
'User-Agent': config.userAgent, "User-Agent": config.userAgent,
}, },
// TODO // TODO
//timeout: 10 * 1000, //timeout: 10 * 1000,
agent: getAgentByUrl, agent: getAgentByUrl,
}).catch(e => { }).catch((e) => {
throw new Error(`${e.message || 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}`); throw new Error(`${res.status}`);
} }
return await res.json() as CaptchaResponse; return (await res.json()) as CaptchaResponse;
} }

View file

@ -1,90 +1,121 @@
import { Antenna } from '@/models/entities/antenna.js'; import type { Antenna } from "@/models/entities/antenna.js";
import { Note } from '@/models/entities/note.js'; import type { Note } from "@/models/entities/note.js";
import { User } from '@/models/entities/user.js'; import type { User } from "@/models/entities/user.js";
import { UserListJoinings, UserGroupJoinings, Blockings } from '@/models/index.js'; import {
import { getFullApAccount } from './convert-host.js'; UserListJoinings,
import * as Acct from '@/misc/acct.js'; UserGroupJoinings,
import { Packed } from './schema.js'; Blockings,
import { Cache } from './cache.js'; } 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: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている // NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
/** /**
* noteUserFollowers / antennaUserFollowing * 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> { export async function checkHitAntenna(
if (note.visibility === 'specified') return false; 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))); const blockings = await blockingCache.fetch(noteUser.id, () =>
if (blockings.some(blocking => blocking === antenna.userId)) return false; 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 (note.visibility === "followers") {
if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId)) return false; if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId))
if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false; return false;
if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId))
return false;
} }
if (!antenna.withReplies && note.replyId != null) return false; if (!antenna.withReplies && note.replyId != null) return false;
if (antenna.src === 'home') { if (antenna.src === "home") {
if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId)) return false; if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId))
if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false; return false;
} else if (antenna.src === 'list') { if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId))
const listUsers = (await UserListJoinings.findBy({ return false;
userListId: antenna.userListId!, } else if (antenna.src === "list") {
})).map(x => x.userId); const listUsers = (
await UserListJoinings.findBy({
userListId: antenna.userListId!,
})
).map((x) => x.userId);
if (!listUsers.includes(note.userId)) return false; if (!listUsers.includes(note.userId)) return false;
} else if (antenna.src === 'group') { } else if (antenna.src === "group") {
const joining = await UserGroupJoinings.findOneByOrFail({ id: antenna.userGroupJoiningId! }); const joining = await UserGroupJoinings.findOneByOrFail({
id: antenna.userGroupJoiningId!,
});
const groupUsers = (await UserGroupJoinings.findBy({ const groupUsers = (
userGroupId: joining.userGroupId, await UserGroupJoinings.findBy({
})).map(x => x.userId); userGroupId: joining.userGroupId,
})
).map((x) => x.userId);
if (!groupUsers.includes(note.userId)) return false; if (!groupUsers.includes(note.userId)) return false;
} else if (antenna.src === 'users') { } else if (antenna.src === "users") {
const accts = antenna.users.map(x => { const accts = antenna.users.map((x) => {
const { username, host } = Acct.parse(x); const { username, host } = Acct.parse(x);
return getFullApAccount(username, host).toLowerCase(); 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 const keywords = antenna.keywords
// Clean up // Clean up
.map(xs => xs.filter(x => x !== '')) .map((xs) => xs.filter((x) => x !== ""))
.filter(xs => xs.length > 0); .filter((xs) => xs.length > 0);
if (keywords.length > 0) { if (keywords.length > 0) {
if (note.text == null) return false; if (note.text == null) return false;
const matched = keywords.some(and => const matched = keywords.some((and) =>
and.every(keyword => and.every((keyword) =>
antenna.caseSensitive antenna.caseSensitive
? note.text!.includes(keyword) ? note.text!.includes(keyword)
: note.text!.toLowerCase().includes(keyword.toLowerCase()) : note.text!.toLowerCase().includes(keyword.toLowerCase()),
)); ),
);
if (!matched) return false; if (!matched) return false;
} }
const excludeKeywords = antenna.excludeKeywords const excludeKeywords = antenna.excludeKeywords
// Clean up // Clean up
.map(xs => xs.filter(x => x !== '')) .map((xs) => xs.filter((x) => x !== ""))
.filter(xs => xs.length > 0); .filter((xs) => xs.length > 0);
if (excludeKeywords.length > 0) { if (excludeKeywords.length > 0) {
if (note.text == null) return false; if (note.text == null) return false;
const matched = excludeKeywords.some(and => const matched = excludeKeywords.some((and) =>
and.every(keyword => and.every((keyword) =>
antenna.caseSensitive antenna.caseSensitive
? note.text!.includes(keyword) ? note.text!.includes(keyword)
: note.text!.toLowerCase().includes(keyword.toLowerCase()) : note.text!.toLowerCase().includes(keyword.toLowerCase()),
)); ),
);
if (matched) return false; if (matched) return false;
} }

View file

@ -1,28 +1,32 @@
import RE2 from 're2'; import RE2 from "re2";
import { Note } from '@/models/entities/note.js'; import type { Note } from "@/models/entities/note.js";
import { User } from '@/models/entities/user.js'; import type { User } from "@/models/entities/user.js";
type NoteLike = { type NoteLike = {
userId: Note['userId']; userId: Note["userId"];
text: Note['text']; text: Note["text"];
}; };
type UserLike = { 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) { 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)) { if (Array.isArray(filter)) {
return filter.every(keyword => text.includes(keyword)); return filter.every((keyword) => text.includes(keyword));
} else { } else {
// represents RegExp // represents RegExp
const regexp = filter.match(/^\/(.+)\/(.*)$/); const regexp = filter.match(/^\/(.+)\/(.*)$/);

View file

@ -1,10 +1,16 @@
// structredCloneが遅いため // structredCloneが遅いため
// SEE: http://var.blog.jp/archives/86038606.html // 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 { export function deepClone<T extends Cloneable>(x: T): T {
if (typeof x === 'object') { if (typeof x === "object") {
if (x === null) return x; if (x === null) return x;
if (Array.isArray(x)) return x.map(deepClone) as T; if (Array.isArray(x)) return x.map(deepClone) as T;
const obj = {} as Record<string, Cloneable>; const obj = {} as Record<string, Cloneable>;

View file

@ -1,6 +1,9 @@
import cd from 'content-disposition'; import cd from "content-disposition";
export function contentDisposition(type: 'inline' | 'attachment', filename: string): string { export function contentDisposition(
const fallback = filename.replace(/[^\w.-]/g, '_'); type: "inline" | "attachment",
filename: string,
): string {
const fallback = filename.replace(/[^\w.-]/g, "_");
return cd(filename, { type, fallback }); return cd(filename, { type, fallback });
} }

View file

@ -1,9 +1,11 @@
import { URL } from 'node:url'; import { URL } from "node:url";
import config from '@/config/index.js'; import config from "@/config/index.js";
import { toASCII } from 'punycode'; import { toASCII } from "punycode";
export function getFullApAccount(username: string, host: string | null) { 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) { export function isSelfHost(host: string) {

View file

@ -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') const query = Notes.createQueryBuilder("note")
.where('note.userId = :userId', { userId }) .where("note.userId = :userId", { userId })
.andWhere('note.renoteId = :renoteId', { renoteId }); .andWhere("note.renoteId = :renoteId", { renoteId });
// 指定した投稿を除く // 指定した投稿を除く
if (excludeNoteId) { if (excludeNoteId) {
query.andWhere('note.id != :excludeNoteId', { excludeNoteId }); query.andWhere("note.id != :excludeNoteId", { excludeNoteId });
} }
return await query.getCount(); return await query.getCount();

View file

@ -1,4 +1,4 @@
import * as tmp from 'tmp'; import * as tmp from "tmp";
export function createTemp(): Promise<[string, () => void]> { export function createTemp(): Promise<[string, () => void]> {
return new Promise<[string, () => void]>((res, rej) => { return new Promise<[string, () => void]>((res, rej) => {
@ -18,7 +18,7 @@ export function createTempDir(): Promise<[string, () => void]> {
(e, path, cleanup) => { (e, path, cleanup) => {
if (e) return rej(e); if (e) return rej(e);
res([path, cleanup]); res([path, cleanup]);
} },
); );
}); });
} }

View file

@ -1,6 +1,6 @@
import { createTemp } from './create-temp.js'; import { createTemp } from "./create-temp.js";
import { downloadUrl } from './download-url.js'; import { downloadUrl } from "./download-url.js";
import { detectType } from './get-file-info.js'; import { detectType } from "./get-file-info.js";
export async function detectUrlMime(url: string) { export async function detectUrlMime(url: string) {
const [path, cleanup] = await createTemp(); const [path, cleanup] = await createTemp();

View file

@ -1,10 +1,10 @@
import * as fs from 'node:fs'; import * as fs from "node:fs";
import * as util from 'node:util'; import * as util from "node:util";
import Logger from '@/services/logger.js'; import Logger from "@/services/logger.js";
import { createTemp } from './create-temp.js'; import { createTemp } from "./create-temp.js";
import { downloadUrl } from './download-url.js'; import { downloadUrl } from "./download-url.js";
const logger = new Logger('download-text-file'); const logger = new Logger("download-text-file");
export async function downloadTextFile(url: string): Promise<string> { export async function downloadTextFile(url: string): Promise<string> {
// Create temp file // Create temp file
@ -16,7 +16,7 @@ export async function downloadTextFile(url: string): Promise<string> {
// write content at URL to temp file // write content at URL to temp file
await downloadUrl(url, path); await downloadUrl(url, path);
const text = await util.promisify(fs.readFile)(path, 'utf8'); const text = await util.promisify(fs.readFile)(path, "utf8");
return text; return text;
} finally { } finally {

View file

@ -1,18 +1,18 @@
import * as fs from 'node:fs'; import * as fs from "node:fs";
import * as stream from 'node:stream'; import * as stream from "node:stream";
import * as util from 'node:util'; import * as util from "node:util";
import got, * as Got from 'got'; import got, * as Got from "got";
import { httpAgent, httpsAgent, StatusError } from './fetch.js'; import { httpAgent, httpsAgent, StatusError } from "./fetch.js";
import config from '@/config/index.js'; import config from "@/config/index.js";
import chalk from 'chalk'; import chalk from "chalk";
import Logger from '@/services/logger.js'; import Logger from "@/services/logger.js";
import IPCIDR from 'ip-cidr'; import IPCIDR from "ip-cidr";
import PrivateIp from 'private-ip'; import PrivateIp from "private-ip";
const pipeline = util.promisify(stream.pipeline); const pipeline = util.promisify(stream.pipeline);
export async function downloadUrl(url: string, path: string): Promise<void> { export async function downloadUrl(url: string, path: string): Promise<void> {
const logger = new Logger('download'); const logger = new Logger("download");
logger.info(`Downloading ${chalk.cyan(url)} ...`); logger.info(`Downloading ${chalk.cyan(url)} ...`);
@ -20,55 +20,69 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
const operationTimeout = 60 * 1000; const operationTimeout = 60 * 1000;
const maxSize = config.maxFileSize || 262144000; const maxSize = config.maxFileSize || 262144000;
const req = got.stream(url, { const req = got
headers: { .stream(url, {
'User-Agent': config.userAgent, headers: {
}, "User-Agent": config.userAgent,
timeout: { },
lookup: timeout, timeout: {
connect: timeout, lookup: timeout,
secureConnect: timeout, connect: timeout,
socket: timeout, // read timeout secureConnect: timeout,
response: timeout, socket: timeout, // read timeout
send: timeout, response: timeout,
request: operationTimeout, // whole operation timeout send: timeout,
}, request: operationTimeout, // whole operation timeout
agent: { },
http: httpAgent, agent: {
https: httpsAgent, http: httpAgent,
}, https: httpsAgent,
http2: false, // default },
retry: { http2: false, // default
limit: 0, retry: {
}, limit: 0,
}).on('response', (res: Got.Response) => { },
if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !config.proxy && res.ip) { })
if (isPrivateIp(res.ip)) { .on("response", (res: Got.Response) => {
logger.warn(`Blocked address: ${res.ip}`); if (
req.destroy(); (process.env.NODE_ENV === "production" ||
process.env.NODE_ENV === "test") &&
!config.proxy &&
res.ip
) {
if (isPrivateIp(res.ip)) {
logger.warn(`Blocked address: ${res.ip}`);
req.destroy();
}
} }
}
const contentLength = res.headers['content-length']; const contentLength = res.headers["content-length"];
if (contentLength != null) { if (contentLength != null) {
const size = Number(contentLength); const size = Number(contentLength);
if (size > maxSize) { if (size > maxSize) {
logger.warn(`maxSize exceeded (${size} > ${maxSize}) on response`); logger.warn(`maxSize exceeded (${size} > ${maxSize}) on response`);
req.destroy();
}
}
})
.on("downloadProgress", (progress: Got.Progress) => {
if (progress.transferred > maxSize) {
logger.warn(
`maxSize exceeded (${progress.transferred} > ${maxSize}) on downloadProgress`,
);
req.destroy(); req.destroy();
} }
} });
}).on('downloadProgress', (progress: Got.Progress) => {
if (progress.transferred > maxSize) {
logger.warn(`maxSize exceeded (${progress.transferred} > ${maxSize}) on downloadProgress`);
req.destroy();
}
});
try { try {
await pipeline(req, fs.createWriteStream(path)); await pipeline(req, fs.createWriteStream(path));
} catch (e) { } catch (e) {
if (e instanceof Got.HTTPError) { if (e instanceof Got.HTTPError) {
throw new StatusError(`${e.response.statusCode} ${e.response.statusMessage}`, e.response.statusCode, e.response.statusMessage); throw new StatusError(
`${e.response.statusCode} ${e.response.statusMessage}`,
e.response.statusCode,
e.response.statusMessage,
);
} else { } else {
throw e; throw e;
} }

View file

@ -1,4 +1,4 @@
import twemoji from 'twemoji-parser/dist/lib/regex.js'; import twemoji from "twemoji-parser/dist/lib/regex.js";
const twemojiRegex = twemoji.default; const twemojiRegex = twemoji.default;
export const emojiRegex = new RegExp(`(${twemojiRegex.source})`); export const emojiRegex = new RegExp(`(${twemojiRegex.source})`);

View file

@ -1,10 +1,10 @@
import * as mfm from 'mfm-js'; import * as mfm from "mfm-js";
import { unique } from '@/prelude/array.js'; import { unique } from "@/prelude/array.js";
export function extractCustomEmojisFromMfm(nodes: mfm.MfmNode[]): string[] { export function extractCustomEmojisFromMfm(nodes: mfm.MfmNode[]): string[] {
const emojiNodes = mfm.extract(nodes, (node) => { const emojiNodes = mfm.extract(nodes, (node) => {
return (node.type === 'emojiCode' && node.props.name.length <= 100); return node.type === "emojiCode" && node.props.name.length <= 100;
}); });
return unique(emojiNodes.map(x => x.props.name)); return unique(emojiNodes.map((x) => x.props.name));
} }

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