dev: remove files that are not needed for this repository
|
@ -1,10 +0,0 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = tab
|
||||
indent_size = 2
|
||||
charset = utf-8
|
||||
insert_final_newline = true
|
||||
|
||||
[*.yml]
|
||||
indent_style = space
|
4
.envrc
|
@ -1,4 +0,0 @@
|
|||
if ! has nix_direnv_version || ! nix_direnv_version 2.3.0; then
|
||||
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.3.0/direnvrc" "sha256-Dmd+j63L84wuzgyjITIfSxSD57Tx7v51DMxVZOsiUD8="
|
||||
fi
|
||||
use flake . --impure
|
7
.gitattributes
vendored
|
@ -1,7 +0,0 @@
|
|||
*.svg -diff -text
|
||||
*.psd -diff -text
|
||||
*.ai -diff -text
|
||||
*.mqo -diff -text
|
||||
*.glb -diff -text
|
||||
*.blend -diff -text
|
||||
*.afdesign -diff -text
|
10
.vscode/extensions.json
vendored
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"editorconfig.editorconfig",
|
||||
"rome.rome",
|
||||
"Vue.volar",
|
||||
"Vue.vscode-typescript-vue-plugin",
|
||||
"arcanis.vscode-zipfs",
|
||||
"Orta.vscode-twoslash-queries"
|
||||
]
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"$schema": "http://json.schemastore.org/vsls",
|
||||
"gitignore": "exclude"
|
||||
}
|
3
.weblate
|
@ -1,3 +0,0 @@
|
|||
[weblate]
|
||||
url = https://hosted.weblate.org/api/
|
||||
translation = firefish/locales
|
16000
CHANGELOG.md
|
@ -1,136 +0,0 @@
|
|||
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
@thatonecalculator on Codeberg,
|
||||
`@kainoa@firefish.social` on the Fediverse,
|
||||
or kainoa@t1c.dev via email.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of
|
||||
actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by
|
||||
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||
[https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
|
273
CONTRIBUTING.md
|
@ -1,273 +0,0 @@
|
|||
# Contribution guide
|
||||
We're glad you're interested in contributing Firefish! In this document you will find the information you need to contribute to the project.
|
||||
|
||||
## Translation (i18n)
|
||||
Firefish uses [Weblate](https://hosted.weblate.org/engage/firefish/) for translation and internationalization 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:
|
||||
|
||||
[![Translation status](https://hosted.weblate.org/widgets/firefish/-/287x66-grey.png)](https://hosted.weblate.org/engage/firefish/)
|
||||
|
||||
[![Translation bars](https://hosted.weblate.org/widgets/firefish/-/multi-auto.svg)](https://hosted.weblate.org/engage/firefish/)
|
||||
|
||||
## Issues
|
||||
Before creating an issue, please check the following:
|
||||
- To avoid duplication, please search for similar issues before creating a new issue.
|
||||
- Do not use Issues to ask questions or troubleshooting.
|
||||
- Issues should only be used to feature requests, suggestions, and bug tracking.
|
||||
- Please ask questions or troubleshooting in the [Matrix room](https://matrix.to/#/#firefish:matrix.fedibird.com).
|
||||
|
||||
> **Warning**
|
||||
> Do not close issues that are about to be resolved. It should remain open until a commit that actually resolves it is merged.
|
||||
|
||||
## Before implementation
|
||||
When you want to add a feature or fix a bug, **first have the design and policy reviewed in an Issue** (if it is not there, please make one). Without this step, there is a high possibility that the PR will not be merged even if it is implemented.
|
||||
|
||||
At this point, you also need to clarify the goals of the PR you will create, and make sure that the other members of the team are aware of them.
|
||||
PRs that do not have a clear set of do's and don'ts tend to be bloated and difficult to review.
|
||||
|
||||
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
|
||||
- The **`main`** branch is tracking the latest release and used for production purposes.
|
||||
- The **`develop`** branch is where we work for the next release.
|
||||
- When you create a PR, basically target it to this branch. **But create a different branch**
|
||||
- The **`l10n_develop`** branch is reserved for localization management.
|
||||
- **`feature/*`** branches are reserved for the development of a specific feature
|
||||
|
||||
## Creating a PR
|
||||
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.
|
||||
- `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.
|
||||
- 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`
|
||||
- Check if there are any documents that need to be created or updated due to this change.
|
||||
- If you have added a feature or fixed a bug, please add a test case if possible.
|
||||
- Please make sure that formatting, tests and Lint are passed in advance.
|
||||
- You can run it with `pnpm run format`, `pnpm run test` and `pnpm run lint`. [See more info](#testing)
|
||||
- If this PR includes UI changes, please attach a screenshot in the text.
|
||||
|
||||
Thanks for your cooperation 🤗
|
||||
|
||||
## Reviewers guide
|
||||
Be willing to comment on the good points and not just the things you want fixed 💯
|
||||
|
||||
### Review perspective
|
||||
- Scope
|
||||
- Are the goals of the PR clear?
|
||||
- Is the granularity of the PR appropriate?
|
||||
- Security
|
||||
- Does merging this PR create a vulnerability?
|
||||
- Performance
|
||||
- Will merging this PR cause unexpected performance degradation?
|
||||
- Is there a more efficient way?
|
||||
- Testing
|
||||
- Does the test ensure the expected behavior?
|
||||
- Are there any omissions or gaps?
|
||||
- Does it check for anomalies?
|
||||
|
||||
## Deploy (SOON)
|
||||
The `/deploy` command by issue comment can be used to deploy the contents of a PR to the preview environment.
|
||||
```
|
||||
/deploy sha=<commit hash>
|
||||
```
|
||||
An actual domain will be assigned so you can test the federation.
|
||||
|
||||
## Merge
|
||||
|
||||
## Release
|
||||
### Release Instructions
|
||||
1. Commit version changes in the `develop` branch ([package.json](https://github.com/misskey-dev/misskey/blob/develop/package.json))
|
||||
2. Create a release PR.
|
||||
- Into `master` from `develop` branch.
|
||||
- The title must be in the format `Release: x.y.z`.
|
||||
- `x.y.z` is the new version you are trying to release.
|
||||
3. Deploy and perform a simple QA check. Also verify that the tests passed.
|
||||
4. Merge it.
|
||||
5. Create a [release of GitHub](https://github.com/misskey-dev/misskey/releases)
|
||||
- The target branch must be `master`
|
||||
- The tag name must be the version
|
||||
|
||||
## Development
|
||||
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.
|
||||
In addition, it will also automatically start the Misskey server process.
|
||||
|
||||
|
||||
# THE FOLLOWING IS OUTDATED:
|
||||
|
||||
## Testing
|
||||
- Test codes are located in [`/test`](/test).
|
||||
|
||||
### Run test
|
||||
Create a config file.
|
||||
```
|
||||
cp test/test.yml .config/
|
||||
```
|
||||
Prepare DB/Redis for testing.
|
||||
```
|
||||
docker-compose -f test/docker-compose.yml up
|
||||
```
|
||||
Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.
|
||||
|
||||
Run all test.
|
||||
```
|
||||
yarn test
|
||||
```
|
||||
|
||||
#### Run specify test
|
||||
```
|
||||
TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT="./test/tsconfig.json" yarn dlx mocha test/foo.ts --require ts-node/register
|
||||
```
|
||||
|
||||
### e2e tests
|
||||
TODO
|
||||
|
||||
## Continuous integration
|
||||
Misskey uses GitHub Actions for executing automated tests.
|
||||
Configuration files are located in [`/.github/workflows`](/.github/workflows).
|
||||
|
||||
## Vue
|
||||
Misskey uses Vue(v3) as its front-end framework.
|
||||
- Use TypeScript.
|
||||
- **When creating a new component, please use the Composition API (with [setup sugar](https://v3.vuejs.org/api/sfc-script-setup.html) and [ref sugar](https://github.com/vuejs/rfcs/discussions/369)) instead of the Options API.**
|
||||
- Some of the existing components are implemented in the Options API, but it is an old implementation. Refactors that migrate those components to the Composition API are also welcome.
|
||||
|
||||
## nirax
|
||||
niraxは、Misskeyで使用しているオリジナルのフロントエンドルーティングシステムです。
|
||||
**vue-routerから影響を多大に受けているので、まずはvue-routerについて学ぶことをお勧めします。**
|
||||
|
||||
### ルート定義
|
||||
ルート定義は、以下の形式のオブジェクトの配列です。
|
||||
|
||||
``` ts
|
||||
{
|
||||
name?: string;
|
||||
path: string;
|
||||
component: Component;
|
||||
query?: Record<string, string>;
|
||||
loginRequired?: boolean;
|
||||
hash?: string;
|
||||
globalCacheKey?: string;
|
||||
children?: RouteDef[];
|
||||
}
|
||||
```
|
||||
|
||||
> **Warning**
|
||||
> 現状、ルートは定義された順に評価されます。
|
||||
> たとえば、`/foo/:id`ルート定義の次に`/foo/bar`ルート定義がされていた場合、後者がマッチすることはありません。
|
||||
|
||||
### 複数のルーター
|
||||
vue-routerとの最大の違いは、niraxは複数のルーターが存在することを許可している点です。
|
||||
これにより、アプリ内ウィンドウでブラウザとは個別にルーティングすることなどが可能になります。
|
||||
|
||||
## Notes
|
||||
### How to resolve conflictions occurred at yarn.lock?
|
||||
|
||||
Just execute `yarn` to fix it.
|
||||
|
||||
### INSERTするときにはsaveではなくinsertを使用する
|
||||
#6441
|
||||
|
||||
### placeholder
|
||||
SQLをクエリビルダで組み立てる際、使用するプレースホルダは重複してはならない
|
||||
例えば
|
||||
``` ts
|
||||
query.andWhere(new Brackets(qb => {
|
||||
for (const type of ps.fileType) {
|
||||
qb.orWhere(`:type = ANY(note.attachedFileTypes)`, { type: type });
|
||||
}
|
||||
}));
|
||||
```
|
||||
と書くと、ループ中で`type`というプレースホルダが複数回使われてしまいおかしくなる
|
||||
だから次のようにする必要がある
|
||||
```ts
|
||||
query.andWhere(new Brackets(qb => {
|
||||
for (const type of ps.fileType) {
|
||||
const i = ps.fileType.indexOf(type);
|
||||
qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type });
|
||||
}
|
||||
}));
|
||||
```
|
||||
|
||||
### Not `null` in TypeORM
|
||||
```ts
|
||||
const foo = await Foos.findOne({
|
||||
bar: Not(null)
|
||||
});
|
||||
```
|
||||
のようなクエリ(`bar`が`null`ではない)は期待通りに動作しない。
|
||||
次のようにします:
|
||||
```ts
|
||||
const foo = await Foos.findOne({
|
||||
bar: Not(IsNull())
|
||||
});
|
||||
```
|
||||
|
||||
### `null` in SQL
|
||||
SQLを発行する際、パラメータが`null`になる可能性のある場合はSQL文を出し分けなければならない
|
||||
例えば
|
||||
``` ts
|
||||
query.where('file.folderId = :folderId', { folderId: ps.folderId });
|
||||
```
|
||||
という処理で、`ps.folderId`が`null`だと結果的に`file.folderId = null`のようなクエリが発行されてしまい、これは正しいSQLではないので期待した結果が得られない
|
||||
だから次のようにする必要がある
|
||||
``` ts
|
||||
if (ps.folderId) {
|
||||
query.where('file.folderId = :folderId', { folderId: ps.folderId });
|
||||
} else {
|
||||
query.where('file.folderId IS NULL');
|
||||
}
|
||||
```
|
||||
|
||||
### `[]` in SQL
|
||||
SQLを発行する際、`IN`のパラメータが`[]`(空の配列)になる可能性のある場合はSQL文を出し分けなければならない
|
||||
例えば
|
||||
``` ts
|
||||
const users = await Users.find({
|
||||
id: In(userIds)
|
||||
});
|
||||
```
|
||||
という処理で、`userIds`が`[]`だと結果的に`user.id IN ()`のようなクエリが発行されてしまい、これは正しいSQLではないので期待した結果が得られない
|
||||
だから次のようにする必要がある
|
||||
``` ts
|
||||
const users = userIds.length > 0 ? await Users.find({
|
||||
id: In(userIds)
|
||||
}) : [];
|
||||
```
|
||||
|
||||
### 配列のインデックス in SQL
|
||||
SQLでは配列のインデックスは**1始まり**。
|
||||
`[a, b, c]`の `a`にアクセスしたいなら`[0]`ではなく`[1]`と書く
|
||||
|
||||
### null IN
|
||||
nullが含まれる可能性のあるカラムにINするときは、そのままだとおかしくなるのでORなどでnullのハンドリングをしよう。
|
||||
|
||||
### `undefined`にご用心
|
||||
MongoDBの時とは違い、findOneでレコードを取得する時に対象レコードが存在しない場合 **`undefined`** が返ってくるので注意。
|
||||
MongoDBは`null`で返してきてたので、その感覚で`if (x === null)`とか書くとバグる。代わりに`if (x == null)`と書いてください
|
||||
|
||||
### Migration作成方法
|
||||
packages/backendで:
|
||||
```sh
|
||||
pnpm dlx typeorm migration:generate -d ormconfig.js -o <migration name>
|
||||
```
|
||||
|
||||
- 生成後、ファイルをmigration下に移してください
|
||||
- 作成されたスクリプトは不必要な変更を含むため除去してください
|
||||
|
||||
### コネクションには`markRaw`せよ
|
||||
**Vueのコンポーネントのdataオプションとして**misskey.jsのコネクションを設定するとき、必ず`markRaw`でラップしてください。インスタンスが不必要にリアクティブ化されることで、misskey.js内の処理で不具合が発生するとともに、パフォーマンス上の問題にも繋がる。なお、Composition APIを使う場合はこの限りではない(リアクティブ化はマニュアルなため)。
|
||||
|
||||
### JSONのimportに気を付けよう
|
||||
TypeScriptでjsonをimportすると、tscでコンパイルするときにそのjsonファイルも一緒にdistディレクトリに吐き出されてしまう。この挙動により、意図せずファイルの書き換えが発生することがあるので、jsonをimportするときは書き換えられても良いものかどうか確認すること。書き換えされて欲しくない場合は、importで読み込むのではなく、`fs.readFileSync`などの関数を使って読み込むようにすればよい。
|
||||
|
||||
### コンポーネントのスタイル定義でmarginを持たせない
|
||||
コンポーネント自身がmarginを設定するのは問題の元となることはよく知られている
|
||||
marginはそのコンポーネントを使う側が設定する
|
||||
|
||||
## その他
|
||||
### HTMLのクラス名で follow という単語は使わない
|
||||
広告ブロッカーで誤ってブロックされる
|
1
Procfile
|
@ -1 +0,0 @@
|
|||
web: NODE_ENV=production npm start
|
|
@ -1,6 +1,6 @@
|
|||
[Misskey](https://misskey-hub.net/) のフォークの [Firefish](https://joinfirefish.org/) のフォークです。
|
||||
|
||||
オリジナルの README は[こちら](./README.original.md)
|
||||
本家 Firefish のリポジトリは[こちら](https://git.joinfirefish.org/firefish/firefish)
|
||||
|
||||
# 変更点
|
||||
|
||||
|
|
|
@ -1,278 +0,0 @@
|
|||
<div align="center">
|
||||
<a href="https://joinfirefish.org/">
|
||||
<img src="./title.svg" alt="Firefish logo" style="border-radius:50%" width="400"/>
|
||||
</a>
|
||||
|
||||
**🌎 **[Firefish](https://joinfirefish.org/)** is an open source, decentralized social media platform that's free forever! 🚀**
|
||||
|
||||
[![no github badge](https://nogithub.codeberg.page/badge.svg)](https://nogithub.codeberg.page/)
|
||||
<!-- [![status badge](https://ci.codeberg.org/api/badges/firefish/firefish/status.svg)](https://ci.codeberg.org/firefish/firefish) -->
|
||||
[![opencollective badge](https://opencollective.com/firefish/tiers/badge.svg)](https://opencollective.com/Firefish)
|
||||
[![liberapay badge](https://img.shields.io/liberapay/receives/ThatOneCalculator?logo=liberapay)](https://liberapay.com/ThatOneCalculator)
|
||||
[![translate-badge](https://hosted.weblate.org/widgets/firefish/-/svg-badge.svg)](https://hosted.weblate.org/engage/firefish/)
|
||||
<!-- [![docker badge](https://img.shields.io/docker/pulls/thatonecalculator/firefish?logo=docker)](https://hub.docker.com/r/thatonecalculator/firefish) -->
|
||||
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](./CODE_OF_CONDUCT.md)
|
||||
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
<img src="./animated.svg" align="right" height="320px"/>
|
||||
|
||||
# ✨ About Firefish
|
||||
|
||||
- Firefish is based off of Misskey, a powerful microblogging server on ActivityPub with features such as emoji reactions, a customizable web UI, rich chatting, and much more!
|
||||
- Firefish adds many quality of life changes and bug fixes for users and server admins alike.
|
||||
- Read **[this document](./FIREFISH.md)** all for current and future differences.
|
||||
- Notable differences:
|
||||
- Improved UI/UX (especially on mobile)
|
||||
- Post editing
|
||||
- Content importing
|
||||
- Improved notifications
|
||||
- Improved server security
|
||||
- Improved accessibility
|
||||
- Improved threads
|
||||
- Recommended Servers timeline
|
||||
- OCR image captioning
|
||||
- New and improved Groups
|
||||
- Better intro tutorial
|
||||
- Compatibility with Mastodon clients/apps
|
||||
- Backfill user information
|
||||
- Advanced search
|
||||
- Many more user and admin settings
|
||||
- [So much more!](./FIREFISH.md)
|
||||
|
||||
</div>
|
||||
|
||||
<div style="clear: both;"></div>
|
||||
|
||||
# 🥂 Links
|
||||
|
||||
### Want to get involved? Great!
|
||||
|
||||
- If you have the means to, [donations](https://opencollective.com/Firefish) are a great way to keep us going.
|
||||
- If you know how to program in TypeScript, Vue, or Rust, read the [contributing](./CONTRIBUTING.md) document.
|
||||
- If you know a non-English language, translating Firefish on [Weblate](https://hosted.weblate.org/engage/firefish/) help bring Firefish to more people. No technical experience needed!
|
||||
- Want to write/report about us, have any professional inquiries, or just have questions to ask? Contact us [here!](https://joinfirefish.org/contact/)
|
||||
|
||||
### All links
|
||||
|
||||
- 🌐 Homepage: <https://joinfirefish.org>
|
||||
- 💸 Donations:
|
||||
- OpenCollective: <https://opencollective.com/Firefish>
|
||||
- Liberapay: <https://liberapay.com/ThatOneCalculator>
|
||||
- Donate publicly to get your name on the Patron list!
|
||||
- 🚢 Flagship server: <https://firefish.social>
|
||||
- 💁 Matrix support room: <https://matrix.to/#/#firefish:matrix.fedibird.com>
|
||||
- 📣 Official account: <https://i.firefish.cloud/@firefish>
|
||||
- 📜 Server list: <https://joinfirefish.org/join>
|
||||
- ✍️ Weblate: <https://hosted.weblate.org/engage/firefish/>
|
||||
- ️️📬 Contact: <https://joinfirefish.org/contact/>
|
||||
|
||||
# 🌠 Getting started
|
||||
|
||||
Want to just join a Firefish server? View the list here, pick one, and join:
|
||||
|
||||
### https://joinfirefish.org/join
|
||||
|
||||
---
|
||||
|
||||
Want to make your own? Keep reading!
|
||||
|
||||
This guide will work for both **starting from scratch** and **migrating from Misskey**.
|
||||
|
||||
## 🔰 Easy installers
|
||||
|
||||
If you have access to a server that supports one of the sources below, I recommend you use it! Note that these methods *won't* allow you to migrate from Misskey without manual intervention.
|
||||
|
||||
<!-- [![Install on the Arch User Repository](https://pool.jortage.com/voringme/misskey/ba2a5c07-f078-43f1-8483-2e01acca9c40.png)](https://aur.archlinux.org/packages/firefish) -->
|
||||
|
||||
[![Install on Ubuntu](https://pool.jortage.com/voringme/misskey/3b62a443-1b44-45cf-8f9e-f1c588f803ed.png)](https://gitlab.prometheus.systems/firefish/ubuntu-bash-install) [![Install Firefish with YunoHost](https://install-app.yunohost.org/install-with-yunohost.svg)](https://install-app.yunohost.org/?app=firefish)
|
||||
|
||||
## 🛳️ Containerization
|
||||
|
||||
- [🐳 How to run Firefish with Docker](https://git.joinfirefish.org/firefish/firefish/-/blob/develop/docs/docker.md)
|
||||
- [🛞 How to run Firefish with Kubernetes/Helm](https://git.joinfirefish.org/firefish/firefish/-/blob/develop/docs/kubernetes.md)
|
||||
|
||||
## 🧑💻 Dependencies
|
||||
|
||||
- 🐢 At least [NodeJS](https://nodejs.org/en/) v18.16.0 (v20 recommended)
|
||||
- 🐘 At least [PostgreSQL](https://www.postgresql.org/) v12 (v14 recommended)
|
||||
- 🍱 At least [Redis](https://redis.io/) v6 (v7 recommended)
|
||||
- Web Proxy (one of the following)
|
||||
- 🍀 Nginx (recommended)
|
||||
- 🦦 Caddy
|
||||
- 🪶 Apache
|
||||
- ⚡ [libvips](https://www.libvips.org/)
|
||||
|
||||
### 😗 Optional dependencies
|
||||
|
||||
- [FFmpeg](https://ffmpeg.org/) for video transcoding
|
||||
- Full text search (one of the following)
|
||||
- 🦔 [Sonic](https://crates.io/crates/sonic-server)
|
||||
- [MeiliSearch](https://www.meilisearch.com/)
|
||||
- [ElasticSearch](https://www.elastic.co/elasticsearch/)
|
||||
- Caching server (one of the following)
|
||||
- 🐲 [DragonflyDB](https://www.dragonflydb.io/) (recommended)
|
||||
- 👻 [KeyDB](https://keydb.dev/)
|
||||
- 🍱 Another [Redis](https://redis.io/) server
|
||||
|
||||
### 🏗️ Build dependencies
|
||||
|
||||
- 🦀 At least [Rust](https://www.rust-lang.org/) v1.68.0
|
||||
- 🦬 C/C++ compiler & build tools
|
||||
- `build-essential` on Debian/Ubuntu Linux
|
||||
- `base-devel` on Arch Linux
|
||||
- 🐍 [Python 3](https://www.python.org/)
|
||||
|
||||
## 👀 Get folder ready
|
||||
|
||||
```sh
|
||||
git clone https://git.joinfirefish.org/firefish/firefish.git
|
||||
cd firefish/
|
||||
```
|
||||
|
||||
> **Note**
|
||||
> By default, you're on the develop branch. Run `git checkout main` or `git checkout beta` to switch to the Main/Beta branches.
|
||||
|
||||
## 📩 Install dependencies
|
||||
|
||||
```sh
|
||||
# nvm install 19 && nvm use 19
|
||||
corepack enable
|
||||
corepack prepare pnpm@latest --activate
|
||||
# To build without TensorFlow, append --no-optional
|
||||
pnpm i # --no-optional
|
||||
```
|
||||
|
||||
### pm2
|
||||
|
||||
To install pm2 run:
|
||||
|
||||
```
|
||||
npm i -g pm2
|
||||
pm2 install pm2-logrotate
|
||||
```
|
||||
|
||||
> **Note**
|
||||
> [`pm2-logrotate`](https://github.com/keymetrics/pm2-logrotate/blob/master/README.md) ensures that log files don't infinitely gather size, as Firefish produces a lot of logs.
|
||||
|
||||
## 🐘 Create database
|
||||
|
||||
In PostgreSQL (`psql`), run the following command:
|
||||
|
||||
```sql
|
||||
CREATE DATABASE firefish WITH encoding = 'UTF8';
|
||||
```
|
||||
|
||||
or run the following from the command line:
|
||||
|
||||
```sh
|
||||
psql postgres -c "create database firefish with encoding = 'UTF8';"
|
||||
```
|
||||
|
||||
In Firefish's directory, fill out the `db` section of `.config/default.yml` with the correct information, where the `db` key is `firefish`.
|
||||
|
||||
## 💰 Caching server
|
||||
|
||||
If you experience a lot of traffic, it's a good idea to set up another Redis-compatible caching server. If you don't set one one up, it'll fall back to the mandatory Redis server. DragonflyDB is the recommended option due to its unrivaled performance and ease of use.
|
||||
|
||||
## 🔎 Set up search
|
||||
|
||||
### 🦔 Sonic
|
||||
|
||||
Sonic is better suited for self hosters with smaller deployments. It uses almost no resources, barely any any disk space, and is relatively fast.
|
||||
|
||||
Follow sonic's [installation guide](https://github.com/valeriansaliou/sonic#installation)
|
||||
|
||||
> **Note**
|
||||
> If you use IPv4: in Sonic's directory, edit the `config.cfg` file to change `inet` to `"0.0.0.0:1491"`.
|
||||
|
||||
In Firefish's directory, fill out the `sonic` section of `.config/default.yml` with the correct information.
|
||||
|
||||
### Meilisearch
|
||||
|
||||
Meilisearch is better suited for larger deployments. It's faster but uses far more resources and disk space.
|
||||
|
||||
Follow Meilisearch's [quick start guide](https://www.meilisearch.com/docs/learn/getting_started/quick_start)
|
||||
|
||||
In Firefish's directory, fill out the `meilisearch` section of `.config/default.yml` with the correct information.
|
||||
|
||||
### ElasticSearch
|
||||
|
||||
Please don't use ElasticSearch unless you already have an ElasticSearch setup and want to continue using it for Firefish. ElasticSearch is slow, heavy, and offers very few benefits over Sonic/Meilisearch.
|
||||
|
||||
## 💅 Customize
|
||||
|
||||
- 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://yourserver.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 error images, place them in the `./custom/assets/badges` directory, replacing the files already there.
|
||||
- To add custom sounds, place only mp3 files in the `./custom/assets/sounds` directory.
|
||||
- To update custom assets without rebuilding, just run `pnpm run gulp`.
|
||||
|
||||
## 🧑🔬 Configuring a new server
|
||||
|
||||
- Run `cp .config/example.yml .config/default.yml`
|
||||
- Edit `.config/default.yml`, making sure to fill out required fields.
|
||||
- Also copy and edit `.config/docker_example.env` to `.config/docker.env` if you're using Docker.
|
||||
|
||||
## 🚚 Migrating from Misskey/FoundKey to Firefish
|
||||
|
||||
For migrating from Misskey v13, Misskey v12, and FoundKey, read [this document](https://git.joinfirefish.org/firefish/firefish/-/blob/develop/docs/migrate.md).
|
||||
|
||||
## 🌐 Web proxy
|
||||
|
||||
### 🍀 Nginx (recommended)
|
||||
|
||||
- Run `sudo cp ./firefish.nginx.conf /etc/nginx/sites-available/ && cd /etc/nginx/sites-available/`
|
||||
- Edit `firefish.nginx.conf` to reflect your server properly
|
||||
- Run `sudo ln -s ./firefish.nginx.conf ../sites-enabled/firefish.nginx.conf`
|
||||
- Run `sudo nginx -t` to validate that the config is valid, then restart the NGINX service.
|
||||
|
||||
### 🦦 Caddy
|
||||
|
||||
- Add the following block to your `Caddyfile`, replacing `example.tld` with your own domain:
|
||||
```caddy
|
||||
example.tld {
|
||||
reverse_proxy http://127.0.0.1:3000
|
||||
}
|
||||
```
|
||||
- Reload your caddy configuration
|
||||
|
||||
### 🪶 Apache
|
||||
|
||||
> **Warning**
|
||||
> Apache has some known problems with Firefish. Only use it if you have to.
|
||||
|
||||
- Run `sudo cp ./firefish.apache.conf /etc/apache2/sites-available/ && cd /etc/apache2/sites-available/`
|
||||
- Edit `firefish.apache.conf` to reflect your server properly
|
||||
- Run `sudo a2ensite firefish.apache` to enable the site
|
||||
- Run `sudo service apache2 restart` to reload apache2 configuration
|
||||
## 🚀 Build and launch!
|
||||
|
||||
### 🐢 NodeJS + pm2
|
||||
|
||||
#### `git pull` and run these steps to update Firefish in the future!
|
||||
|
||||
```sh
|
||||
# git pull
|
||||
pnpm install
|
||||
NODE_ENV=production pnpm run build && pnpm run migrate
|
||||
pm2 start "NODE_ENV=production pnpm run start" --name Firefish
|
||||
```
|
||||
|
||||
## 😉 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 Firefish'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 Firefish, run `for p in {3000..4000}; do ss -tlnH | tr -s ' ' | cut -d" " -sf4 | grep -q "${p}$" || echo "${p}"; done | head -n 1`. Replace 3000 with the minimum port and 4000 with the maximum port if you need it.
|
||||
- I'd recommend you use a S3 Bucket/CDN for Object Storage, especially if you use Docker.
|
||||
- I'd ***strongly*** recommend against using CloudFlare, but if you do, make sure to turn code minification off.
|
||||
- For push notifications, run `npx web-push generate-vapid-keys`, then put the public and private keys into Control Panel > General > ServiceWorker.
|
||||
- For translations, make a [DeepL](https://deepl.com) account and generate an API key, then put it into Control Panel > General > DeepL Translation.
|
||||
- To add another admin account:
|
||||
- Go to the user's page > 3 Dots > About > Moderation > turn on "Moderator"
|
||||
- Go back to Overview > click the clipboard icon next to the ID
|
||||
- Run `psql -d firefish` (or whatever the database name is)
|
||||
- Run `UPDATE "user" SET "isAdmin" = true WHERE id='999999';` (replace `999999` with the copied ID)
|
||||
- Restart your Firefish server
|
120
RELEASE_NOTES.md
|
@ -1,120 +0,0 @@
|
|||
# Firefish
|
||||
|
||||
Welcome to the new era of FIREFISH!
|
||||
|
||||
<img src="https://git.joinfirefish.org/firefish/firefish/-/raw/develop/animated.svg" height="320px"/>
|
||||
|
||||
# Changelog
|
||||
|
||||
## Major changes from last release candidate
|
||||
|
||||
- Firefish branding and [new repo](https://git.joinfirefish.org/firefish/firefish)!
|
||||
- Far better Mastodon API support
|
||||
- Edits are now non-experimental
|
||||
- Support for secondary cache server
|
||||
- Link verification with `rel=me`
|
||||
- Store antennas in cache
|
||||
- Post imports with media
|
||||
- Sytle fixes
|
||||
- More translations
|
||||
- Performance upgrades
|
||||
- Bug fixes
|
||||
- Faster build
|
||||
- [FoundKey](https://genau.qwertqwefsday.eu/notes/9h0lqlg05m) -> Firefish migration fixes
|
||||
|
||||
## Major changes from stable
|
||||
|
||||
All of the above, plus:
|
||||
|
||||
- Post editing
|
||||
- Post imports
|
||||
- New post design
|
||||
- New header design
|
||||
- Better accessibility
|
||||
- Server silences
|
||||
- Modmail
|
||||
- New MFM effects
|
||||
- Meilisearch search engine
|
||||
- Channel search
|
||||
- Improved system emails
|
||||
- cuid2 IDs
|
||||
- Emoji skin tones
|
||||
- New 2FA flow
|
||||
- Reduced visual clutter
|
||||
- Deck view improvements
|
||||
|
||||
# Upgrading
|
||||
|
||||
## If upgrading from v13 (old stable)
|
||||
|
||||
**In addition to the rest of the steps after this**:
|
||||
|
||||
- Install the Rust toolchain (v1.68.0 or higher): <https://www.rust-lang.org/tools/install>
|
||||
|
||||
- (Optional) install Meilisearch to use as a search engine instead of Sonic: <https://www.meilisearch.com/>
|
||||
|
||||
- Replace your config file (`.config/default.yml`) with a blank version of the example (`.config/example.yml`) and re-enter the information. This will make things easier.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Upgrade to at least Node v20.3.1 (v20.4.0 recommended).
|
||||
|
||||
- (Optional, recommended) install DragonflyDB and configure under `cacheServer`: <https://www.dragonflydb.io/>
|
||||
|
||||
## Set new repo and pull
|
||||
|
||||
```sh
|
||||
git remote set-url origin https://git.joinfirefish.org/firefish/firefish.git
|
||||
git pull --ff
|
||||
```
|
||||
|
||||
In case you get an error like:
|
||||
```
|
||||
error: The following untracked working tree files would be overwritten by merge:
|
||||
packages/backend/assets/LICENSE
|
||||
Please move or remove them before you merge.
|
||||
Aborting
|
||||
```
|
||||
|
||||
Run:
|
||||
```sh
|
||||
rm ./packages/backend/assets/LICENSE
|
||||
git reset --hard origin/develop
|
||||
git pull --ff
|
||||
```
|
||||
|
||||
## Upgrade packages
|
||||
|
||||
```sh
|
||||
corepack enable
|
||||
pnpm i
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
```sh
|
||||
NODE_ENV=production pnpm run buld
|
||||
```
|
||||
|
||||
## Migrate
|
||||
|
||||
There are 3 new envoriment variables for this upgrade only, because antennas have been moved from the database to the cache.
|
||||
|
||||
- `ANTENNA_MIGRATION_SKIP`: skips copying antennas to cache if `true`. Default is `false` (will clear all antennas if skipped).
|
||||
- `ANTENNA_MIGRATION_COPY_LIMIT`: limits how many entries are copied to cache. Default is `0` (no limit).
|
||||
- `ANTENNA_MIGRATION_READ_LIMIT`: limits how many entires are read from the database
|
||||
in each iteration of migration. Large value may result in faster migration, but also may consume more memory. Default is `10000`.
|
||||
|
||||
With default options:
|
||||
|
||||
```sh
|
||||
NODE_ENV=production pnpm run migrate
|
||||
```
|
||||
|
||||
With custom options (feel free to only use some):
|
||||
|
||||
```sh
|
||||
NODE_ENV=production ANTENNA_MIGRATION_SKIP=false ANTENNA_MIGRATION_COPY_LIMIT=0 ANTENNA_MIGRATION_READ_LIMIT=1000 pnpm run migrate
|
||||
```
|
||||
|
||||
And then restart Calckey...uh... Firefish!
|
16
SECURITY.md
|
@ -1,16 +0,0 @@
|
|||
# Reporting Security Issues
|
||||
|
||||
## Minor Security Issues
|
||||
|
||||
If you discover a minor security issue in Firefish, please report it by sending an
|
||||
email to [kainoa@t1c.dev](mailto:kainoa@t1c.dev).
|
||||
|
||||
## High Security Issues
|
||||
|
||||
If you discover a security issue, which is so high risk, that too much is affected by it, please dont send it over unencrypted communication. You can share your PGP keys with us using kainoa@t1c.dev and after we established a secure communication, send it over E-Mail, or message us using matrix' encrypted private messages at @t1c:matrix.fedibird.com or @cleo:tchncs.de
|
||||
|
||||
|
||||
This will allow us to assess the risk, and make a fix available before we add a
|
||||
bug report to the Codeberg repository.
|
||||
|
||||
Thanks for helping make Firefish safe for everyone.
|
153
animated.svg
|
@ -1,153 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1792 1792">
|
||||
|
||||
<style>
|
||||
/* Eyes */
|
||||
@keyframes firefish-logo-blink {
|
||||
95% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
97.5% {
|
||||
transform: scaleY(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
|
||||
#firefish-logo-eye-l,
|
||||
#firefish-logo-eye-r {
|
||||
transform-origin: center;
|
||||
transform-box: fill-box;
|
||||
animation-name: firefish-logo-blink;
|
||||
animation-duration: 3s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
/* Bubbles */
|
||||
@keyframes firefish-logo-bloop {
|
||||
0% {
|
||||
transform: translateY(400px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
20% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
30% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
80% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
90% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(-400px);
|
||||
}
|
||||
}
|
||||
|
||||
.firefish-logo-bubble {
|
||||
animation-name: firefish-logo-bloop;
|
||||
animation-fill-mode: forwards;
|
||||
animation-iteration-count: infinite;
|
||||
animation-duration: 2s;
|
||||
animation-timing-function: ease-out;
|
||||
fill: #31748f;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#firefish-logo-bubble-1 {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
#firefish-logo-bubble-2 {
|
||||
animation-delay: .2s;
|
||||
}
|
||||
|
||||
#firefish-logo-bubble-3 {
|
||||
animation-delay: .6s;
|
||||
}
|
||||
|
||||
#firefish-logo-bubble-4 {
|
||||
animation-delay: .8s;
|
||||
}
|
||||
|
||||
#firefish-logo-bubble-5 {
|
||||
animation-delay: 1s;
|
||||
}
|
||||
</style>
|
||||
|
||||
<defs>
|
||||
<linearGradient id="firefish-logo-linear-gradient" x1="26.41" y1="1765.71" x2="1492.49" y2="299.62"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#ec476d" />
|
||||
<stop offset="1" stop-color="#f6ae4a" />
|
||||
</linearGradient>
|
||||
<linearGradient id="firefish-logo-linear-gradient-purple" x1="512" y1="1788" x2="512" y2="1398.68"
|
||||
gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#db44db" stop-opacity=".3" />
|
||||
<stop offset=".11" stop-color="#db44db" stop-opacity=".21" />
|
||||
<stop offset=".27" stop-color="#db44db" stop-opacity=".12" />
|
||||
<stop offset=".44" stop-color="#db44db" stop-opacity=".05" />
|
||||
<stop offset=".65" stop-color="#db44db" stop-opacity=".01" />
|
||||
<stop offset="1" stop-color="#db44db" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient id="firefish-logo-linear-gradient-3" x1="-351.25" y1="1382.82" x2="1150.74" y2="-119.17"
|
||||
xlink:href="#firefish-logo-linear-gradient" />
|
||||
<linearGradient id="firefish-logo-linear-gradient-4" x1="412.3" y1="2147.8" x2="1915.31" y2="644.8"
|
||||
xlink:href="#firefish-logo-linear-gradient" />
|
||||
<linearGradient id="firefish-logo-linear-gradient-5" x1="28.25" y1="1763.75" x2="1531.25" y2="260.75"
|
||||
xlink:href="#firefish-logo-linear-gradient" />
|
||||
</defs>
|
||||
|
||||
<!-- Bubbles -->
|
||||
<circle id="firefish-logo-bubble-1" class="firefish-logo-bubble" cx="205" cy="500" r="40" />
|
||||
<circle id="firefish-logo-bubble-2" class="firefish-logo-bubble" cx="140" cy="500" r="80" />
|
||||
<circle id="firefish-logo-bubble-3" class="firefish-logo-bubble" cx="95" cy="500" r="70" />
|
||||
<circle id="firefish-logo-bubble-4" class="firefish-logo-bubble" cx="200" cy="500" r="40" />
|
||||
<circle id="firefish-logo-bubble-5" class="firefish-logo-bubble" cx="95" cy="500" r="20" />
|
||||
|
||||
<g id="firefish-logo-firefish">
|
||||
<g id="firefish-logo-fish">
|
||||
<g id="firefish-logo-body">
|
||||
<path
|
||||
d="m928,768.11H96c-53.02,0-96,42.98-96,96v832c0,53.02,42.98,96,96,96h832c53.02,0,96-42.98,96-96v-832c0-53.02-42.98-96-96-96Z"
|
||||
fill="url(#firefish-logo-linear-gradient)" />
|
||||
</g>
|
||||
<g id="firefish-logo-body-purple-grad">
|
||||
<path
|
||||
d="m928,768.11H96C42.98,768.11,0,811.09,0,864.11v832c0,53.02,42.98,96,96,96h832c53.02,0,96-42.98,96-96v-832c0-53.02-42.98-96-96-96Z"
|
||||
fill="url(#firefish-logo-linear-gradient-purple)" />
|
||||
</g>
|
||||
<g id="firefish-logo-fin-l">
|
||||
<path
|
||||
d="m927.66,639.79h-439.94c-52.98,0-95.94-42.95-95.94-95.94V103.91c0-85.47,103.34-128.27,163.77-67.84l439.94,439.94c60.44,60.44,17.63,163.77-67.84,163.77Z"
|
||||
fill="url(#firefish-logo-linear-gradient-3)" />
|
||||
</g>
|
||||
<g id="firefish-logo-fin-b">
|
||||
<path
|
||||
d="m1694.66,1408.11h-445.51c-53.65,0-97.15-43.5-97.15-97.15v-445.51c0-86.55,104.64-129.9,165.84-68.7l445.51,445.51c61.2,61.2,17.86,165.84-68.7,165.84Z"
|
||||
fill="url(#firefish-logo-linear-gradient-4)" />
|
||||
</g>
|
||||
<g id="firefish-logo-fin-r">
|
||||
<path
|
||||
d="m1694.66,640h-445.51c-53.65,0-97.15-43.5-97.15-97.15V97.34c0-86.55,104.64-129.9,165.84-68.7l445.51,445.51c61.2,61.2,17.86,165.84-68.7,165.84Z"
|
||||
fill="url(#firefish-logo-linear-gradient-5)" />
|
||||
</g>
|
||||
<g id="firefish-logo-eye-l">
|
||||
<circle cx="256" cy="1408.11" r="128" fill="#fff" />
|
||||
</g>
|
||||
<g id="firefish-logo-eye-r">
|
||||
<circle cx="576" cy="1408.11" r="128" fill="#fff" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 4.9 KiB |
|
@ -1,23 +0,0 @@
|
|||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
|
@ -1,38 +0,0 @@
|
|||
apiVersion: v2
|
||||
name: firefish
|
||||
description: A fun, new, open way to experience social media https://joinfirefish.org
|
||||
|
||||
# A chart can be either an 'application' or a 'library' chart.
|
||||
#
|
||||
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||
# to be deployed.
|
||||
#
|
||||
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||
type: application
|
||||
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: 0.1.2
|
||||
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "rc"
|
||||
|
||||
dependencies:
|
||||
- name: elasticsearch
|
||||
version: 19.0.1
|
||||
repository: https://raw.githubusercontent.com/bitnami/charts/archive-full-index/bitnami
|
||||
condition: elasticsearch.enabled
|
||||
- name: postgresql
|
||||
version: 11.1.3
|
||||
repository: https://raw.githubusercontent.com/bitnami/charts/archive-full-index/bitnami
|
||||
condition: postgresql.enabled
|
||||
- name: redis
|
||||
version: 16.13.2
|
||||
repository: https://raw.githubusercontent.com/bitnami/charts/archive-full-index/bitnami
|
||||
condition: redis.enabled
|
|
@ -1,89 +0,0 @@
|
|||
# firefish
|
||||
|
||||
![Version: 0.1.2](https://img.shields.io/badge/Version-0.1.2-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: rc](https://img.shields.io/badge/AppVersion-rc-informational?style=flat-square)
|
||||
|
||||
A fun, new, open way to experience social media https://joinfirefish.org
|
||||
|
||||
## Requirements
|
||||
|
||||
| Repository | Name | Version |
|
||||
|------------|------|---------|
|
||||
| https://raw.githubusercontent.com/bitnami/charts/archive-full-index/bitnami | elasticsearch | 19.0.1 |
|
||||
| https://raw.githubusercontent.com/bitnami/charts/archive-full-index/bitnami | postgresql | 11.1.3 |
|
||||
| https://raw.githubusercontent.com/bitnami/charts/archive-full-index/bitnami | redis | 16.13.2 |
|
||||
|
||||
## Values
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| affinity | object | `{}` | |
|
||||
| autoscaling.enabled | bool | `false` | |
|
||||
| autoscaling.maxReplicas | int | `100` | |
|
||||
| autoscaling.minReplicas | int | `1` | |
|
||||
| autoscaling.targetCPUUtilizationPercentage | int | `80` | |
|
||||
| firefish.allowedPrivateNetworks | list | `[]` | If you want to allow firefish to connect to private ips, enter the cidrs here. |
|
||||
| firefish.deepl.authKey | string | `""` | |
|
||||
| firefish.deepl.isPro | bool | `false` | |
|
||||
| firefish.deepl.managed | bool | `false` | |
|
||||
| firefish.domain | string | `"firefish.local"` | |
|
||||
| firefish.isManagedHosting | bool | `true` | |
|
||||
| firefish.libreTranslate.apiKey | string | `""` | |
|
||||
| firefish.libreTranslate.apiUrl | string | `""` | |
|
||||
| firefish.libreTranslate.managed | bool | `false` | |
|
||||
| firefish.objectStorage.access_key | string | `""` | |
|
||||
| firefish.objectStorage.access_secret | string | `""` | |
|
||||
| firefish.objectStorage.baseUrl | string | `""` | |
|
||||
| firefish.objectStorage.bucket | string | `""` | |
|
||||
| firefish.objectStorage.endpoint | string | `""` | |
|
||||
| firefish.objectStorage.managed | bool | `true` | |
|
||||
| firefish.objectStorage.prefix | string | `"files"` | |
|
||||
| firefish.objectStorage.region | string | `""` | |
|
||||
| firefish.reservedUsernames[0] | string | `"root"` | |
|
||||
| firefish.reservedUsernames[1] | string | `"admin"` | |
|
||||
| firefish.reservedUsernames[2] | string | `"administrator"` | |
|
||||
| firefish.reservedUsernames[3] | string | `"me"` | |
|
||||
| firefish.reservedUsernames[4] | string | `"system"` | |
|
||||
| firefish.smtp.from_address | string | `"notifications@example.com"` | |
|
||||
| firefish.smtp.login | string | `""` | |
|
||||
| firefish.smtp.managed | bool | `true` | |
|
||||
| firefish.smtp.password | string | `""` | |
|
||||
| firefish.smtp.port | int | `587` | |
|
||||
| firefish.smtp.server | string | `"smtp.mailgun.org"` | |
|
||||
| firefish.smtp.useImplicitSslTls | bool | `false` | |
|
||||
| elasticsearch | object | `{"auth":{},"enabled":false,"hostname":"","port":9200,"ssl":false}` | https://github.com/bitnami/charts/tree/master/bitnami/elasticsearch#parameters |
|
||||
| fullnameOverride | string | `""` | |
|
||||
| image.pullPolicy | string | `"IfNotPresent"` | |
|
||||
| image.repository | string | `"registry.joinfirefish.org/firefish/firefish"` | |
|
||||
| image.tag | string | `""` | |
|
||||
| imagePullSecrets | list | `[]` | |
|
||||
| ingress.annotations | object | `{}` | |
|
||||
| ingress.className | string | `""` | |
|
||||
| ingress.enabled | bool | `false` | |
|
||||
| ingress.hosts[0].host | string | `"chart-example.local"` | |
|
||||
| ingress.hosts[0].paths[0].path | string | `"/"` | |
|
||||
| ingress.hosts[0].paths[0].pathType | string | `"ImplementationSpecific"` | |
|
||||
| ingress.tls | list | `[]` | |
|
||||
| nameOverride | string | `""` | |
|
||||
| nodeSelector | object | `{}` | |
|
||||
| podAnnotations | object | `{}` | |
|
||||
| podSecurityContext | object | `{}` | |
|
||||
| postgresql.auth.database | string | `"firefish_production"` | |
|
||||
| postgresql.auth.password | string | `""` | |
|
||||
| postgresql.auth.username | string | `"firefish"` | |
|
||||
| postgresql.enabled | bool | `true` | disable if you want to use an existing db; in which case the values below must match those of that external postgres instance |
|
||||
| redis.auth.password | string | `""` | you must set a password; the password generated by the redis chart will be rotated on each upgrade: |
|
||||
| redis.enabled | bool | `true` | |
|
||||
| redis.hostname | string | `""` | |
|
||||
| redis.port | int | `6379` | |
|
||||
| replicaCount | int | `1` | |
|
||||
| resources | object | `{}` | |
|
||||
| securityContext | object | `{}` | |
|
||||
| service.port | int | `80` | |
|
||||
| service.type | string | `"ClusterIP"` | |
|
||||
| serviceAccount.annotations | object | `{}` | |
|
||||
| serviceAccount.create | bool | `true` | |
|
||||
| serviceAccount.name | string | `""` | |
|
||||
| tolerations | list | `[]` | |
|
||||
|
||||
----------------------------------------------
|
||||
Autogenerated from chart metadata using [helm-docs v1.11.0](https://github.com/norwoodj/helm-docs/releases/v1.11.0)
|
|
@ -1,22 +0,0 @@
|
|||
1. Get the application URL by running these commands:
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- range $host := .Values.ingress.hosts }}
|
||||
{{- range .paths }}
|
||||
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- else if contains "NodePort" .Values.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "firefish.fullname" . }})
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
echo http://$NODE_IP:$NODE_PORT
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "firefish.fullname" . }}'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "firefish.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "firefish.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
||||
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
||||
{{- end }}
|
|
@ -1,329 +0,0 @@
|
|||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "firefish.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "firefish.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "firefish.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "firefish.labels" -}}
|
||||
helm.sh/chart: {{ include "firefish.chart" . }}
|
||||
{{ include "firefish.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "firefish.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "firefish.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "firefish.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "firefish.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified name for dependent services.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
*/}}
|
||||
{{- define "firefish.elasticsearch.fullname" -}}
|
||||
{{- printf "%s-%s" .Release.Name "elasticsearch" | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "firefish.redis.fullname" -}}
|
||||
{{- printf "%s-%s" .Release.Name "redis" | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "firefish.postgresql.fullname" -}}
|
||||
{{- printf "%s-%s" .Release.Name "postgresql" | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
config/default.yml content
|
||||
*/}}
|
||||
{{- define "firefish.configDir.default.yml" -}}
|
||||
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
# Firefish configuration
|
||||
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
# ┌─────┐
|
||||
#───┘ URL └─────────────────────────────────────────────────────
|
||||
|
||||
# Final accessible URL seen by a user.
|
||||
url: "https://{{ .Values.firefish.domain }}/"
|
||||
|
||||
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
||||
# URL SETTINGS AFTER THAT!
|
||||
|
||||
# ┌───────────────────────┐
|
||||
#───┘ Port and TLS settings └───────────────────────────────────
|
||||
|
||||
#
|
||||
# Misskey requires a reverse proxy to support HTTPS connections.
|
||||
#
|
||||
# +----- https://example.tld/ ------------+
|
||||
# +------+ |+-------------+ +----------------+|
|
||||
# | User | ---> || Proxy (443) | ---> | Misskey (3000) ||
|
||||
# +------+ |+-------------+ +----------------+|
|
||||
# +---------------------------------------+
|
||||
#
|
||||
# You need to set up a reverse proxy. (e.g. nginx)
|
||||
# An encrypted connection with HTTPS is highly recommended
|
||||
# because tokens may be transferred in GET requests.
|
||||
|
||||
# The port that your Misskey server should listen on.
|
||||
port: 3000
|
||||
|
||||
# ┌──────────────────────────┐
|
||||
#───┘ PostgreSQL configuration └────────────────────────────────
|
||||
|
||||
db:
|
||||
{{- if .Values.postgresql.enabled }}
|
||||
host: {{ template "firefish.postgresql.fullname" . }}
|
||||
port: '5432'
|
||||
{{- else }}
|
||||
host: {{ .Values.postgresql.postgresqlHostname }}
|
||||
port: {{ .Values.postgresql.postgresqlPort | default "5432" | quote }}
|
||||
{{- end }}
|
||||
|
||||
# Database name
|
||||
db: {{ .Values.postgresql.auth.database }}
|
||||
|
||||
# Auth
|
||||
user: {{ .Values.postgresql.auth.username }}
|
||||
pass: "{{ .Values.postgresql.auth.password }}"
|
||||
|
||||
# Whether disable Caching queries
|
||||
#disableCache: true
|
||||
|
||||
# Extra Connection options
|
||||
#extra:
|
||||
# ssl:
|
||||
# host: localhost
|
||||
# rejectUnauthorized: false
|
||||
|
||||
# ┌─────────────────────┐
|
||||
#───┘ Redis configuration └─────────────────────────────────────
|
||||
|
||||
redis:
|
||||
{{- if .Values.redis.enabled }}
|
||||
host: {{ template "firefish.redis.fullname" . }}-master
|
||||
{{- else }}
|
||||
host: {{ required "When the redis chart is disabled .Values.redis.hostname is required" .Values.redis.hostname }}
|
||||
{{- end }}
|
||||
port: {{ .Values.redis.port | default "6379" | quote }}
|
||||
#family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||
pass: {{ .Values.redis.auth.password | quote }}
|
||||
#prefix: example-prefix
|
||||
#db: 1
|
||||
#user: default
|
||||
#tls:
|
||||
# host: localhost
|
||||
# rejectUnauthorized: false
|
||||
|
||||
# ┌─────────────────────┐
|
||||
#───┘ Sonic configuration └─────────────────────────────────────
|
||||
|
||||
#sonic:
|
||||
# host: localhost
|
||||
# port: 1491
|
||||
# auth: SecretPassword
|
||||
# collection: notes
|
||||
# bucket: default
|
||||
|
||||
# ┌─────────────────────────────┐
|
||||
#───┘ Elasticsearch configuration └─────────────────────────────
|
||||
|
||||
{{- if .Values.elasticsearch.enabled }}
|
||||
elasticsearch:
|
||||
host: {{ template "mastodon.elasticsearch.fullname" . }}-master-hl
|
||||
port: 9200
|
||||
ssl: false
|
||||
{{- else if .Values.elasticsearch.hostname }}
|
||||
elasticsearch:
|
||||
host: {{ .Values.elasticsearch.hostname | quote }}
|
||||
port: {{ .Values.elasticsearch.port }}
|
||||
ssl: {{ .Values.elasticsearch.ssl }}
|
||||
{{- if .Values.elasticsearch.auth }}
|
||||
user: {{ .Values.elasticsearch.auth.username | quote }}
|
||||
pass: {{ .Values.elasticsearch.auth.password | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
# ┌───────────────┐
|
||||
#───┘ ID generation └───────────────────────────────────────────
|
||||
|
||||
# You can select the ID generation method.
|
||||
# You don't usually need to change this setting, but you can
|
||||
# change it according to your preferences.
|
||||
|
||||
# Available methods:
|
||||
# aid ... Short, Millisecond accuracy
|
||||
# meid ... Similar to ObjectID, Millisecond accuracy
|
||||
# ulid ... Millisecond accuracy
|
||||
# objectid ... This is left for backward compatibility
|
||||
|
||||
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
||||
# ID SETTINGS AFTER THAT!
|
||||
|
||||
id: 'aid'
|
||||
|
||||
# ┌─────────────────────┐
|
||||
#───┘ Other configuration └─────────────────────────────────────
|
||||
|
||||
# Max note length, should be < 8000.
|
||||
#maxNoteLength: 3000
|
||||
|
||||
# Maximum lenght of an image caption or file comment (default 1500, max 8192)
|
||||
#maxCaptionLength: 1500
|
||||
|
||||
# Reserved usernames that only the administrator can register with
|
||||
reservedUsernames:
|
||||
{{ .Values.firefish.reservedUsernames | toYaml }}
|
||||
|
||||
# Whether disable HSTS
|
||||
#disableHsts: true
|
||||
|
||||
# Number of worker processes
|
||||
#clusterLimit: 1
|
||||
|
||||
# Job concurrency per worker
|
||||
# deliverJobConcurrency: 128
|
||||
# inboxJobConcurrency: 16
|
||||
|
||||
# Job rate limiter
|
||||
# deliverJobPerSec: 128
|
||||
# inboxJobPerSec: 16
|
||||
|
||||
# Job attempts
|
||||
# deliverJobMaxAttempts: 12
|
||||
# inboxJobMaxAttempts: 8
|
||||
|
||||
# IP address family used for outgoing request (ipv4, ipv6 or dual)
|
||||
#outgoingAddressFamily: ipv4
|
||||
|
||||
# Syslog option
|
||||
#syslog:
|
||||
# host: localhost
|
||||
# port: 514
|
||||
|
||||
# Proxy for HTTP/HTTPS
|
||||
#proxy: http://127.0.0.1:3128
|
||||
|
||||
#proxyBypassHosts: [
|
||||
# 'example.com',
|
||||
# '192.0.2.8'
|
||||
#]
|
||||
|
||||
# Proxy for SMTP/SMTPS
|
||||
#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT
|
||||
#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4
|
||||
#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5
|
||||
|
||||
# Media Proxy
|
||||
#mediaProxy: https://example.com/proxy
|
||||
|
||||
# Proxy remote files (default: false)
|
||||
#proxyRemoteFiles: true
|
||||
|
||||
allowedPrivateNetworks:
|
||||
{{ .Values.firefish.allowedPrivateNetworks | toYaml }}
|
||||
|
||||
# TWA
|
||||
#twa:
|
||||
# nameSpace: android_app
|
||||
# packageName: tld.domain.twa
|
||||
# sha256CertFingerprints: ['AB:CD:EF']
|
||||
|
||||
# Upload or download file size limits (bytes)
|
||||
#maxFileSize: 262144000
|
||||
|
||||
# Managed hosting settings
|
||||
# !!!!!!!!!!
|
||||
# >>>>>> NORMAL SELF-HOSTERS, STAY AWAY! <<<<<<
|
||||
# >>>>>> YOU DON'T NEED THIS! <<<<<<
|
||||
# !!!!!!!!!!
|
||||
# Each category is optional, but if each item in each category is mandatory!
|
||||
# If you mess this up, that's on you, you've been warned...
|
||||
|
||||
#maxUserSignups: 100
|
||||
isManagedHosting: {{ .Values.firefish.isManagedHosting }}
|
||||
deepl:
|
||||
managed: {{ .Values.firefish.deepl.managed }}
|
||||
authKey: {{ .Values.firefish.deepl.authKey | quote}}
|
||||
isPro: {{ .Values.firefish.deepl.isPro }}
|
||||
|
||||
libreTranslate:
|
||||
managed: {{ .Values.firefish.libreTranslate.managed }}
|
||||
apiUrl: {{ .Values.firefish.libreTranslate.apiUrl | quote }}
|
||||
apiKey: {{ .Values.firefish.libreTranslate.apiKey | quote }}
|
||||
|
||||
email:
|
||||
managed: {{ .Values.firefish.smtp.managed }}
|
||||
address: {{ .Values.firefish.smtp.from_address | quote }}
|
||||
host: {{ .Values.firefish.smtp.server | quote }}
|
||||
port: {{ .Values.firefish.smtp.port }}
|
||||
user: {{ .Values.firefish.smtp.login | quote }}
|
||||
pass: {{ .Values.firefish.smtp.password | quote }}
|
||||
useImplicitSslTls: {{ .Values.firefish.smtp.useImplicitSslTls }}
|
||||
objectStorage:
|
||||
managed: {{ .Values.firefish.objectStorage.managed }}
|
||||
baseUrl: {{ .Values.firefish.objectStorage.baseUrl | quote }}
|
||||
bucket: {{ .Values.firefish.objectStorage.bucket | quote }}
|
||||
prefix: {{ .Values.firefish.objectStorage.prefix | quote }}
|
||||
endpoint: {{ .Values.firefish.objectStorage.endpoint | quote }}
|
||||
region: {{ .Values.firefish.objectStorage.region | quote }}
|
||||
accessKey: {{ .Values.firefish.objectStorage.access_key | quote }}
|
||||
secretKey: {{ .Values.firefish.objectStorage.access_secret | quote }}
|
||||
useSsl: true
|
||||
connnectOverProxy: false
|
||||
setPublicReadOnUpload: true
|
||||
s3ForcePathStyle: true
|
||||
|
||||
# !!!!!!!!!!
|
||||
# >>>>>> AGAIN, NORMAL SELF-HOSTERS, STAY AWAY! <<<<<<
|
||||
# >>>>>> YOU DON'T NEED THIS, ABOVE SETTINGS ARE FOR MANAGED HOSTING ONLY! <<<<<<
|
||||
# !!!!!!!!!!
|
||||
|
||||
# Seriously. Do NOT fill out the above settings if you're self-hosting.
|
||||
# They're much better off being set from the control panel.
|
||||
{{- end }}
|
|
@ -1,82 +0,0 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "firefish.fullname" . }}
|
||||
labels:
|
||||
{{- include "firefish.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if not .Values.autoscaling.enabled }}
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "firefish.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
checksum/secret-config: {{ include ( print $.Template.BasePath "/secret-config.yaml" ) . | sha256sum | quote }}
|
||||
{{- with .Values.podAnnotations }}
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "firefish.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "firefish.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
volumes:
|
||||
- name: config-volume
|
||||
secret:
|
||||
secretName: {{ template "firefish.fullname" . }}-config
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
command:
|
||||
- pnpm
|
||||
- run
|
||||
- start
|
||||
env:
|
||||
- name: "NODE_ENV"
|
||||
value: "production"
|
||||
volumeMounts:
|
||||
- name: config-volume
|
||||
mountPath: /firefish/.config
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 3000
|
||||
protocol: TCP
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
failureThreshold: 30
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
|
@ -1,28 +0,0 @@
|
|||
{{- if .Values.autoscaling.enabled }}
|
||||
apiVersion: autoscaling/v2beta1
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ include "firefish.fullname" . }}
|
||||
labels:
|
||||
{{- include "firefish.labels" . | nindent 4 }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ include "firefish.fullname" . }}
|
||||
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||
metrics:
|
||||
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
- type: Resource
|
||||
resource:
|
||||
name: memory
|
||||
targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
|
||||
{{- end }}
|
||||
{{- end }}
|
|
@ -1,61 +0,0 @@
|
|||
{{- if .Values.ingress.enabled -}}
|
||||
{{- $fullName := include "firefish.fullname" . -}}
|
||||
{{- $svcPort := .Values.service.port -}}
|
||||
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
|
||||
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
{{- else -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
{{- end }}
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
labels:
|
||||
{{- include "firefish.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
|
||||
pathType: {{ .pathType }}
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ $fullName }}
|
||||
port:
|
||||
number: {{ $svcPort }}
|
||||
{{- else }}
|
||||
serviceName: {{ $fullName }}
|
||||
servicePort: {{ $svcPort }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
|
@ -1,59 +0,0 @@
|
|||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: {{ include "firefish.fullname" . }}-db-migrate
|
||||
labels:
|
||||
{{- include "firefish.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
"helm.sh/hook": post-install,pre-upgrade
|
||||
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
|
||||
"helm.sh/hook-weight": "-2"
|
||||
spec:
|
||||
template:
|
||||
metadata:
|
||||
name: {{ include "firefish.fullname" . }}-db-migrate
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "firefish.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
volumes:
|
||||
- name: config-volume
|
||||
secret:
|
||||
secretName: {{ template "firefish.fullname" . }}-config
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
command:
|
||||
- pnpm
|
||||
- run
|
||||
- migrate
|
||||
env:
|
||||
- name: "NODE_ENV"
|
||||
value: "production"
|
||||
volumeMounts:
|
||||
- name: config-volume
|
||||
mountPath: /firefish/.config
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
|
@ -1,9 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ template "firefish.fullname" . }}-config
|
||||
labels:
|
||||
{{- include "firefish.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
data:
|
||||
default.yml: {{ include "firefish.configDir.default.yml" . | b64enc }}
|
|
@ -1,15 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "firefish.fullname" . }}
|
||||
labels:
|
||||
{{- include "firefish.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "firefish.selectorLabels" . | nindent 4 }}
|
|
@ -1,12 +0,0 @@
|
|||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "firefish.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "firefish.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
|
@ -1,15 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: "{{ include "firefish.fullname" . }}-test-connection"
|
||||
labels:
|
||||
{{- include "firefish.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
"helm.sh/hook": test
|
||||
spec:
|
||||
containers:
|
||||
- name: wget
|
||||
image: busybox
|
||||
command: ['wget']
|
||||
args: ['{{ include "firefish.fullname" . }}:{{ .Values.service.port }}']
|
||||
restartPolicy: Never
|
|
@ -1,168 +0,0 @@
|
|||
# Default values for firefish.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: registry.joinfirefish.org/firefish/firefish
|
||||
pullPolicy: IfNotPresent
|
||||
# Overrides the image tag whose default is the chart appVersion.
|
||||
tag: ""
|
||||
|
||||
firefish:
|
||||
isManagedHosting: true
|
||||
domain: firefish.local
|
||||
|
||||
deepl:
|
||||
managed: false
|
||||
authKey: ""
|
||||
isPro: false
|
||||
|
||||
libreTranslate:
|
||||
managed: false
|
||||
apiUrl: ""
|
||||
apiKey: ""
|
||||
|
||||
smtp:
|
||||
managed: true
|
||||
from_address: notifications@example.com
|
||||
port: 587
|
||||
server: smtp.mailgun.org
|
||||
useImplicitSslTls: false
|
||||
login: ""
|
||||
password: ""
|
||||
|
||||
objectStorage:
|
||||
managed: true
|
||||
access_key: ""
|
||||
access_secret: ""
|
||||
baseUrl: "" # e.g. "https://my-bucket.nyc3.cdn.digitaloceanspaces.com"
|
||||
bucket: "" # e.g. "my-bucket"
|
||||
prefix: files
|
||||
endpoint: "" # e.g. "nyc3.digitaloceanspaces.com:443"
|
||||
region: "" # e.g. "nyc3"
|
||||
|
||||
# -- If you want to allow firefish to connect to private ips, enter the cidrs here.
|
||||
allowedPrivateNetworks: []
|
||||
# - "10.0.0.0/8"
|
||||
|
||||
reservedUsernames:
|
||||
- root
|
||||
- admin
|
||||
- administrator
|
||||
- me
|
||||
- system
|
||||
|
||||
# https://github.com/bitnami/charts/tree/master/bitnami/postgresql#parameters
|
||||
postgresql:
|
||||
# -- disable if you want to use an existing db; in which case the values below
|
||||
# must match those of that external postgres instance
|
||||
enabled: true
|
||||
# postgresqlHostname: preexisting-postgresql
|
||||
# postgresqlPort: 5432
|
||||
auth:
|
||||
database: firefish_production
|
||||
username: firefish
|
||||
# you must set a password; the password generated by the postgresql chart will
|
||||
# be rotated on each upgrade:
|
||||
# https://github.com/bitnami/charts/tree/master/bitnami/postgresql#upgrade
|
||||
password: ""
|
||||
|
||||
# https://github.com/bitnami/charts/tree/master/bitnami/redis#parameters
|
||||
redis:
|
||||
# disable if you want to use an existing redis instance; in which case the
|
||||
# values below must match those of that external redis instance
|
||||
enabled: true
|
||||
hostname: ""
|
||||
port: 6379
|
||||
auth:
|
||||
# -- you must set a password; the password generated by the redis chart will be
|
||||
# rotated on each upgrade:
|
||||
password: ""
|
||||
|
||||
# -- https://github.com/bitnami/charts/tree/master/bitnami/elasticsearch#parameters
|
||||
elasticsearch:
|
||||
# disable if you want to use an existing redis instance; in which case the
|
||||
# values below must match those of that external elasticsearch instance
|
||||
enabled: false
|
||||
hostname: ""
|
||||
port: 9200
|
||||
ssl: false
|
||||
auth: {}
|
||||
# username: ""
|
||||
# password: ""
|
||||
# @ignored
|
||||
image:
|
||||
tag: 7
|
||||
|
||||
imagePullSecrets: []
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
serviceAccount:
|
||||
# Specifies whether a service account should be created
|
||||
create: true
|
||||
# Annotations to add to the service account
|
||||
annotations: {}
|
||||
# The name of the service account to use.
|
||||
# If not set and create is true, a name is generated using the fullname template
|
||||
name: ""
|
||||
|
||||
podAnnotations: {}
|
||||
|
||||
podSecurityContext: {}
|
||||
# fsGroup: 2000
|
||||
|
||||
securityContext: {}
|
||||
# capabilities:
|
||||
# drop:
|
||||
# - ALL
|
||||
# readOnlyRootFilesystem: true
|
||||
# runAsNonRoot: true
|
||||
# runAsUser: 1000
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
hosts:
|
||||
- host: chart-example.local
|
||||
paths:
|
||||
- path: /
|
||||
pathType: ImplementationSpecific
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - chart-example.local
|
||||
|
||||
resources: {}
|
||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||
# choice for the user. This also increases chances charts run on environments with little
|
||||
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 100
|
||||
targetCPUUtilizationPercentage: 80
|
||||
# targetMemoryUtilizationPercentage: 80
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
98
cliff.toml
|
@ -1,98 +0,0 @@
|
|||
# configuration file for git-cliff (0.1.0)
|
||||
|
||||
[changelog]
|
||||
# changelog header
|
||||
header = """
|
||||
# Changelog\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 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
|
|
@ -1,12 +0,0 @@
|
|||
import { defineConfig } from "cypress";
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
// We've imported your old cypress plugins here.
|
||||
// You may want to clean this up later by importing these.
|
||||
setupNodeEvents(on, config) {
|
||||
return require("./cypress/plugins/index.js")(on, config);
|
||||
},
|
||||
baseUrl: "http://localhost:61812",
|
||||
},
|
||||
});
|
|
@ -1,151 +0,0 @@
|
|||
describe("Before setup instance", () => {
|
||||
beforeEach(() => {
|
||||
cy.resetState();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
|
||||
// waitを入れることでそれを防止できる
|
||||
cy.wait(1000);
|
||||
});
|
||||
|
||||
it("successfully loads", () => {
|
||||
cy.visit("/");
|
||||
});
|
||||
|
||||
it("setup instance", () => {
|
||||
cy.visit("/");
|
||||
|
||||
cy.intercept("POST", "/api/admin/accounts/create").as("signup");
|
||||
|
||||
cy.get("[data-cy-admin-username] input").type("admin");
|
||||
cy.get("[data-cy-admin-password] input").type("admin1234");
|
||||
cy.get("[data-cy-admin-ok]").click();
|
||||
|
||||
// なぜか動かない
|
||||
//cy.wait('@signup').should('have.property', 'response.statusCode');
|
||||
cy.wait("@signup");
|
||||
});
|
||||
});
|
||||
|
||||
describe("After setup instance", () => {
|
||||
beforeEach(() => {
|
||||
cy.resetState();
|
||||
|
||||
// インスタンス初期セットアップ
|
||||
cy.registerUser("admin", "pass", true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
|
||||
// waitを入れることでそれを防止できる
|
||||
cy.wait(1000);
|
||||
});
|
||||
|
||||
it("successfully loads", () => {
|
||||
cy.visit("/");
|
||||
});
|
||||
|
||||
it("signup", () => {
|
||||
cy.visit("/");
|
||||
|
||||
cy.intercept("POST", "/api/signup").as("signup");
|
||||
|
||||
cy.get("[data-cy-signup]").click();
|
||||
cy.get("[data-cy-signup-username] input").type("alice");
|
||||
cy.get("[data-cy-signup-password] input").type("alice1234");
|
||||
cy.get("[data-cy-signup-password-retype] input").type("alice1234");
|
||||
cy.get("[data-cy-signup-submit]").click();
|
||||
|
||||
cy.wait("@signup");
|
||||
});
|
||||
});
|
||||
|
||||
describe("After user signup", () => {
|
||||
beforeEach(() => {
|
||||
cy.resetState();
|
||||
|
||||
// インスタンス初期セットアップ
|
||||
cy.registerUser("admin", "pass", true);
|
||||
|
||||
// ユーザー作成
|
||||
cy.registerUser("alice", "alice1234");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
|
||||
// waitを入れることでそれを防止できる
|
||||
cy.wait(1000);
|
||||
});
|
||||
|
||||
it("successfully loads", () => {
|
||||
cy.visit("/");
|
||||
});
|
||||
|
||||
it("signin", () => {
|
||||
cy.visit("/");
|
||||
|
||||
cy.intercept("POST", "/api/signin").as("signin");
|
||||
|
||||
cy.get("[data-cy-signin]").click();
|
||||
cy.get("[data-cy-signin-username] input").type("alice");
|
||||
// Enterキーでサインインできるかの確認も兼ねる
|
||||
cy.get("[data-cy-signin-password] input").type("alice1234{enter}");
|
||||
|
||||
cy.wait("@signin");
|
||||
});
|
||||
|
||||
it("suspend", function () {
|
||||
cy.request("POST", "/api/admin/suspend-user", {
|
||||
i: this.admin.token,
|
||||
userId: this.alice.id,
|
||||
});
|
||||
|
||||
cy.visit("/");
|
||||
|
||||
cy.get("[data-cy-signin]").click();
|
||||
cy.get("[data-cy-signin-username] input").type("alice");
|
||||
cy.get("[data-cy-signin-password] input").type("alice1234{enter}");
|
||||
|
||||
// TODO: cypressにブラウザの言語指定できる機能が実装され次第英語のみテストするようにする
|
||||
cy.contains(
|
||||
/アカウントが凍結されています|This account has been suspended due to/gi,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("After user singed in", () => {
|
||||
beforeEach(() => {
|
||||
cy.resetState();
|
||||
|
||||
// インスタンス初期セットアップ
|
||||
cy.registerUser("admin", "pass", true);
|
||||
|
||||
// ユーザー作成
|
||||
cy.registerUser("alice", "alice1234");
|
||||
|
||||
cy.login("alice", "alice1234");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
|
||||
// waitを入れることでそれを防止できる
|
||||
cy.wait(1000);
|
||||
});
|
||||
|
||||
it("successfully loads", () => {
|
||||
cy.get("[data-cy-open-post-form]").should("be.visible");
|
||||
});
|
||||
|
||||
it("note", () => {
|
||||
cy.get("[data-cy-open-post-form]").click();
|
||||
cy.get("[data-cy-post-form-text]").type("Hello, Misskey!");
|
||||
cy.get("[data-cy-open-post-form-submit]").click();
|
||||
|
||||
cy.contains("Hello, Misskey!");
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: 投稿フォームの公開範囲指定のテスト
|
||||
// TODO: 投稿フォームのファイル添付のテスト
|
||||
// TODO: 投稿フォームのハッシュタグ保持フィールドのテスト
|
|
@ -1,64 +0,0 @@
|
|||
describe("After user signed in", () => {
|
||||
beforeEach(() => {
|
||||
cy.resetState();
|
||||
cy.viewport("macbook-16");
|
||||
// インスタンス初期セットアップ
|
||||
cy.registerUser("admin", "pass", true);
|
||||
|
||||
// ユーザー作成
|
||||
cy.registerUser("alice", "alice1234");
|
||||
|
||||
cy.login("alice", "alice1234");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// テスト終了直前にページ遷移するようなテストケース(例えばアカウント作成)だと、たぶんCypressのバグでブラウザの内容が次のテストケースに引き継がれてしまう(例えばアカウントが作成し終わった段階からテストが始まる)。
|
||||
// waitを入れることでそれを防止できる
|
||||
cy.wait(1000);
|
||||
});
|
||||
|
||||
it("widget edit toggle is visible", () => {
|
||||
cy.get(".mk-widget-edit").should("be.visible");
|
||||
});
|
||||
|
||||
it("widget select should be visible in edit mode", () => {
|
||||
cy.get(".mk-widget-edit").click();
|
||||
cy.get(".mk-widget-select").should("be.visible");
|
||||
});
|
||||
|
||||
it("first widget should be removed", () => {
|
||||
cy.get(".mk-widget-edit").click();
|
||||
cy.get(".customize-container:first-child .remove._button").click();
|
||||
cy.get(".customize-container").should("have.length", 2);
|
||||
});
|
||||
|
||||
function buildWidgetTest(widgetName) {
|
||||
it(`${widgetName} widget should get added`, () => {
|
||||
cy.get(".mk-widget-edit").click();
|
||||
cy.get(".mk-widget-select select").select(widgetName, { force: true });
|
||||
cy.get(".bg._modalBg.transparent").click({ multiple: true, force: true });
|
||||
cy.get(".mk-widget-add").click({ force: true });
|
||||
cy.get(`.mkw-${widgetName}`).should("exist");
|
||||
});
|
||||
}
|
||||
|
||||
buildWidgetTest("memo");
|
||||
buildWidgetTest("notifications");
|
||||
buildWidgetTest("timeline");
|
||||
buildWidgetTest("calendar");
|
||||
buildWidgetTest("rss");
|
||||
buildWidgetTest("trends");
|
||||
buildWidgetTest("clock");
|
||||
buildWidgetTest("activity");
|
||||
buildWidgetTest("photos");
|
||||
buildWidgetTest("digitalClock");
|
||||
buildWidgetTest("federation");
|
||||
buildWidgetTest("postForm");
|
||||
buildWidgetTest("slideshow");
|
||||
buildWidgetTest("serverMetric");
|
||||
buildWidgetTest("onlineUsers");
|
||||
buildWidgetTest("jobQueue");
|
||||
buildWidgetTest("button");
|
||||
buildWidgetTest("aiscript");
|
||||
buildWidgetTest("aichan");
|
||||
});
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
/// <reference types="cypress" />
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
//
|
||||
// You can change the location of this file or turn off loading
|
||||
// the plugins file with the 'pluginsFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/plugins-guide
|
||||
// ***********************************************************
|
||||
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
};
|
|
@ -1,57 +0,0 @@
|
|||
// ***********************************************
|
||||
// This example commands.js shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
|
||||
Cypress.Commands.add("resetState", () => {
|
||||
cy.window((win) => {
|
||||
win.indexedDB.deleteDatabase("keyval-store");
|
||||
});
|
||||
cy.request("POST", "/api/reset-db").as("reset");
|
||||
cy.get("@reset").its("status").should("equal", 204);
|
||||
cy.reload(true);
|
||||
});
|
||||
|
||||
Cypress.Commands.add("registerUser", (username, password, isAdmin = false) => {
|
||||
const route = isAdmin ? "/api/admin/accounts/create" : "/api/signup";
|
||||
|
||||
cy.request("POST", route, {
|
||||
username: username,
|
||||
password: password,
|
||||
})
|
||||
.its("body")
|
||||
.as(username);
|
||||
});
|
||||
|
||||
Cypress.Commands.add("login", (username, password) => {
|
||||
cy.visit("/");
|
||||
|
||||
cy.intercept("POST", "/api/signin").as("signin");
|
||||
|
||||
cy.get("[data-cy-signin]").click();
|
||||
cy.get("[data-cy-signin-username] input").type(username);
|
||||
cy.get("[data-cy-signin-password] input").type(`${password}{enter}`);
|
||||
|
||||
cy.wait("@signin").as("signedIn");
|
||||
});
|
|
@ -1,34 +0,0 @@
|
|||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import "./commands";
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
|
||||
Cypress.on("uncaught:exception", (err, runnable) => {
|
||||
if (
|
||||
[
|
||||
// Chrome
|
||||
"ResizeObserver loop limit exceeded",
|
||||
|
||||
// Firefox
|
||||
"ResizeObserver loop completed with undelivered notifications",
|
||||
].some((msg) => err.message.includes(msg))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
});
|
|
@ -1,57 +0,0 @@
|
|||
version: "3"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: registry.joinfirefish.org/firefish/firefish
|
||||
build: ..
|
||||
container_name: firefish_web
|
||||
restart: always
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
# - es
|
||||
ports:
|
||||
- "3000:3000"
|
||||
networks:
|
||||
- network
|
||||
# - web
|
||||
volumes:
|
||||
- ../files:/firefish/files
|
||||
- ../.config:/firefish/.config:ro
|
||||
|
||||
redis:
|
||||
restart: always
|
||||
container_name: firefish_redis
|
||||
image: docker.io/redis:7.0-alpine
|
||||
networks:
|
||||
- network
|
||||
volumes:
|
||||
- ../redis:/data
|
||||
|
||||
db:
|
||||
restart: always
|
||||
image: docker.io/postgres:12.2-alpine
|
||||
container_name: firefish_db
|
||||
networks:
|
||||
- network
|
||||
env_file:
|
||||
- ../.config/docker.env
|
||||
volumes:
|
||||
- ../db:/var/lib/postgresql/data
|
||||
|
||||
# es:
|
||||
# restart: always
|
||||
# image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.4.2
|
||||
# environment:
|
||||
# - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
|
||||
# - "TAKE_FILE_OWNERSHIP=111"
|
||||
# networks:
|
||||
# - network
|
||||
# volumes:
|
||||
# - ./elasticsearch:/usr/share/elasticsearch/data
|
||||
|
||||
networks:
|
||||
network:
|
||||
# web:
|
||||
# external:
|
||||
# name: web
|
|
@ -1,5 +0,0 @@
|
|||
# API Documentation
|
||||
|
||||
You can find interactive API documentation at any Firefish instance. https://firefish.social/api-doc
|
||||
|
||||
You can also find auto-generated documentation for firefish-js [here](../packages/firefish-js/markdown/firefish-js.md).
|
|
@ -1,108 +0,0 @@
|
|||
# 🌎 Firefish Developer Docs
|
||||
|
||||
## Nix Dev Environment
|
||||
The Firefish repo comes with a Nix-based shell environment to help make development as easy as possible!
|
||||
|
||||
Please note, however, that this environment will not work on Windows outside of a WSL2 environment.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Installed the [Nix Package Manager](https://nixos.org/download.html) (use the comman on their website)
|
||||
- Installed [direnv](https://direnv.net/docs/installation.html) and added its hook to your shell. (package manager)
|
||||
|
||||
Once the repo is cloned to your computer, follow these next few steps inside the Firefish folder:
|
||||
|
||||
- Run `direnv allow`. This will build the environment and install all needed tools.
|
||||
- Run `install-deps`, then `prepare-config`, to install the node dependencies and prepare the needed config files.
|
||||
- In a second terminal, run `devenv up`. This will spawn a **Redis** server, a **Postgres** server, and the **Firefish** server in dev mode.
|
||||
- Once you see the Firefish banner printed in your second terminal, run `migrate` in the first.
|
||||
- Once migrations finish, open http://localhost:3000 in your web browser.
|
||||
- You should now see the admin user creation screen!
|
||||
|
||||
Note: When you want to restart a dev server, all you need to do is run `devenv up`, no other steps are necessary.
|
||||
|
||||
# Possible Troubles with the dev enviroment
|
||||
(this doesn't have to be done under normal conditions, this is for future reference)
|
||||
|
||||
### direnv
|
||||
If you have any trouble with `direnv allow`
|
||||
Check that the contents of `.envrc` have the same version of nix-direnv that is specified here:
|
||||
> nix-direnv under -> installation -> using direnv source url
|
||||
> https://github.com/nix-community/nix-direnv#direnv-source_url
|
||||
|
||||
there should be no errors during `direnv allow`
|
||||
|
||||
### outdated nix packages
|
||||
if `install-deps` or any subsequent command doesn't run due to versioning problems
|
||||
`flake.nix` and `flake.lock` may be outdated
|
||||
|
||||
delete `flake.lock`, or better, run `nix flake update --extra-experimental-features flakes --extra-experimental-features nix-command`
|
||||
after that, run `direnv rebuild`
|
||||
|
||||
if there are any errors, you might have to change `flake.nix`
|
||||
(because the available options can change between versions - consider getting support in [the matrix channel](https://matrix.to/#/#firefish:matrix.fedibird.com))
|
||||
|
||||
### after changing a node version
|
||||
in my case, i had to change the node version from 19, to 18
|
||||
|
||||
! before proceeding, make sure to delete all build artifacts!
|
||||
remove `node_modules` and `built` folders, and maybe `.devenv` and `.direnv` as well
|
||||
manually, or run `npm cache clean --force` and `pnpm cleanall`
|
||||
|
||||
### Windows Subsystem for Linux
|
||||
if `devenv up` terminates because of wrong folder permissions,
|
||||
|
||||
create the file `/etc/wsl.conf` in your distro and add
|
||||
```shell
|
||||
[automount]
|
||||
options = "metadata"
|
||||
```
|
||||
|
||||
this allows `chmod` calls to actually have an effect.
|
||||
the build scripts DO actually set the permissions, it just needs to work in wsl.
|
||||
|
||||
### devenv up
|
||||
devenv up may take a looong time. (some say this is fake news, maybe it was bad luck in my case)
|
||||
|
||||
do not get spooked by this error:
|
||||
```
|
||||
> firefish@14.0.0-dev32 start /mnt/.../firefish
|
||||
> pnpm --filter backend run start
|
||||
|
||||
|
||||
> backend@ start /mnt/.../firefish/packages/backend
|
||||
> pnpm node ./built/index.js
|
||||
|
||||
node:internal/modules/cjs/loader:1078
|
||||
throw err;
|
||||
^
|
||||
|
||||
Error: Cannot find module '/mnt/.../firefish/packages/backend/built/index.js'
|
||||
at Module._resolveFilename (node:internal/modules/cjs/loader:1075:15)
|
||||
at Module._load (node:internal/modules/cjs/loader:920:27)
|
||||
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
|
||||
at node:internal/main/run_main_module:23:47 {
|
||||
code: 'MODULE_NOT_FOUND',
|
||||
requireStack: []
|
||||
}
|
||||
|
||||
Node.js v18.16.0
|
||||
undefined
|
||||
/mnt/.../firefish/packages/backend:
|
||||
ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL backend@ start: `pnpm node ./built/index.js`
|
||||
Exit status 1
|
||||
ELIFECYCLE Command failed with exit code 1.
|
||||
```
|
||||
|
||||
the script is designed to constantly try to start the server, while the build is still running.
|
||||
this just means that the build isn't finished yet.
|
||||
|
||||
at some point you should see a banner that says "Firefish" in big letters -
|
||||
then you're good to go and can run `migrate` (in another terminal)!
|
||||
|
||||
if you don't see the banner,
|
||||
and it's for some reason stuck on `Finished 'build' after 917 ms` for a view minutes,
|
||||
|
||||
just leave devenv running and open another terminal in the folder
|
||||
run `migrate` and then `pnpm --filter backend run start` by yourself
|
||||
the server should start
|
|
@ -1,48 +0,0 @@
|
|||
# 🐳 Running a Firefish server with Docker
|
||||
|
||||
## Pre-built docker container
|
||||
[registry.joinfirefish.org/firefish/firefish](https://git.joinfirefish.org/firefish/firefish/container_registry)
|
||||
|
||||
## `docker-compose`
|
||||
|
||||
There is a `docker-compose.yml` in the root of the project that you can use to build the container from source
|
||||
|
||||
- .config/docker.env (**db config settings**)
|
||||
- .config/default.yml (**firefish server settings**)
|
||||
|
||||
## Configuring
|
||||
|
||||
Rename the files:
|
||||
|
||||
`cp .config/example.yml .config/default.yml`
|
||||
|
||||
`cp .config/example.env .config/docker.env`
|
||||
|
||||
then edit them according to your environment.
|
||||
You can configure `docker.env` with anything you like, but you will have to pay attention to the `default.yml` file:
|
||||
- `url` should be set to the URL you will be hosting the web interface for the server at.
|
||||
- `host`, `db`, `user`, `pass` will have to be configured in the `PostgreSQL configuration` section - `host` is the name of the postgres container (eg: *firefish_db_1*), and the others should match your `docker.env`.
|
||||
- `host`will need to be configured in the *Redis configuration* section - it is the name of the redis container (eg: *firefish_redis_1*)
|
||||
- `auth` will need to be configured in the *Sonic* section - cannot be the default `SecretPassword`
|
||||
|
||||
Everything else can be left as-is.
|
||||
|
||||
## Running docker-compose
|
||||
|
||||
The [prebuilt container for firefish](https://git.joinfirefish.org/firefish/firefish/container_registry) is fairly large, and may take a few minutes to download and extract using docker.
|
||||
|
||||
Copy `docker-compose.yml` and the `config/` to a directory, then run the **docker-compose** command:
|
||||
`docker-compose up -d`.
|
||||
|
||||
NOTE: This will take some time to come fully online, even after download and extracting the container images, and it may emit some error messages before completing successfully. Specifically, the `db` container needs to initialize and so isn't available to the `web` container right away. Only once the `db` container comes online does the `web` container start building and initializing the firefish tables.
|
||||
|
||||
Once the server is up you can use a web browser to access the web interface at `http://serverip:3000` (where `serverip` is the IP of the server you are running the firefish server on).
|
||||
|
||||
## Docker for development
|
||||
|
||||
```sh
|
||||
cd dev/
|
||||
docker-compose build
|
||||
docker-compose run --rm web pnpm run init
|
||||
docker-compose up -d
|
||||
```
|
|
@ -1,41 +0,0 @@
|
|||
diff --git a/packages/backend/migration/1661376843000-remove-mentioned-remote-users-column.js b/packages/backend/migration/1661376843000-remove-mentioned-remote-users-column.js
|
||||
index 42d79b5b5..1fd5e0f10 100644
|
||||
--- a/packages/backend/migration/1661376843000-remove-mentioned-remote-users-column.js
|
||||
+++ b/packages/backend/migration/1661376843000-remove-mentioned-remote-users-column.js
|
||||
@@ -7,6 +7,22 @@ export class removeMentionedRemoteUsersColumn1661376843000 {
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "note" ADD "mentionedRemoteUsers" TEXT NOT NULL DEFAULT '[]'::text`);
|
||||
- await queryRunner.query(`UPDATE "note" SET "mentionedRemoteUsers" = (SELECT COALESCE(json_agg(row_to_json("data"))::text, '[]') FROM (SELECT "url", "uri", "username", "host" FROM "user" JOIN "user_profile" ON "user"."id" = "user_profile". "userId" WHERE "user"."host" IS NOT NULL AND "user"."id" = ANY("note"."mentions")) AS "data")`);
|
||||
+ await queryRunner.query(`
|
||||
+ CREATE TEMP TABLE IF NOT EXISTS "temp_mentions" AS
|
||||
+ SELECT "id", "url", "uri", "username", "host"
|
||||
+ FROM "user"
|
||||
+ JOIN "user_profile" ON "user"."id" = "user_profile"."userId" WHERE "user"."host" IS NOT NULL
|
||||
+ `);
|
||||
+
|
||||
+ await queryRunner.query(`
|
||||
+ CREATE UNIQUE INDEX "temp_mentions_id" ON "temp_mentions"("id")
|
||||
+ `);
|
||||
+
|
||||
+ await queryRunner.query(`
|
||||
+ UPDATE "note" SET "mentionedRemoteUsers" = (
|
||||
+ SELECT COALESCE(json_agg(row_to_json("data")::jsonb - 'id')::text, '[]') FROM "temp_mentions" AS "data"
|
||||
+ WHERE "data"."id" = ANY("note"."mentions")
|
||||
+ )
|
||||
+ `);
|
||||
}
|
||||
}
|
||||
diff --git a/packages/backend/migration/1663399074403-resize-comments-drive-file.js b/packages/backend/migration/1663399074403-resize-comments-drive-file.js
|
||||
index a037f1655..0873aec9b 100644
|
||||
--- a/packages/backend/migration/1663399074403-resize-comments-drive-file.js
|
||||
+++ b/packages/backend/migration/1663399074403-resize-comments-drive-file.js
|
||||
@@ -9,6 +9,6 @@ export class resizeCommentsDriveFile1663399074403 {
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
- await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "comment" TYPE character varying(512)`);
|
||||
- }
|
||||
+ console.log('This migration cannot be reverted, skipping...');
|
||||
+ }
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
# Running a Firefish server with Kubernetes and Helm
|
||||
|
||||
This is a [Helm](https://helm.sh/) chart directory in the root of the project
|
||||
that you can use to deploy firefish to a Kubernetes cluster
|
||||
|
||||
## Deployment
|
||||
|
||||
1. Copy the example helm values and make your changes:
|
||||
```shell
|
||||
cp .config/helm_values_example.yml .config/helm_values.yml
|
||||
```
|
||||
|
||||
2. Update helm dependencies:
|
||||
```shell
|
||||
cd chart
|
||||
helm dependency list $dir 2> /dev/null | tail +2 | head -n -1 | awk '{ print "helm repo add " $1 " " $3 }' | while read cmd; do $cmd; done;
|
||||
cd ../
|
||||
```
|
||||
|
||||
3. Create the firefish helm release (also used to update existing deployment):
|
||||
```shell
|
||||
helm upgrade \
|
||||
--install \
|
||||
--namespace firefish \
|
||||
--create-namespace \
|
||||
firefish chart/ \
|
||||
-f .config/helm_values.yml
|
||||
```
|
||||
|
||||
4. Watch your firefish server spin up:
|
||||
```shell
|
||||
kubectl -n firefish get po -w
|
||||
```
|
||||
|
||||
5. Initial the admin user and managed config:
|
||||
```shell
|
||||
export firefish_USERNAME="my_desired_admin_handle" && \
|
||||
export firefish_PASSWORD="myDesiredInitialPassword" && \
|
||||
export firefish_HOST="firefish.example.com" && \
|
||||
export firefish_TOKEN=$(curl -X POST https://$firefish_HOST/api/admin/accounts/create -H "Content-Type: application/json" -d "{ \"username\":\"$firefish_USERNAME\", \"password\":\"$firefish_PASSWORD\" }" | jq -r '.token') && \
|
||||
echo "Save this token: ${firefish_TOKEN}" && \
|
||||
curl -X POST -H "Authorization: Bearer $firefish_TOKEN" https://$firefish_HOST/api/admin/accounts/hosted
|
||||
```
|
||||
|
||||
6. Enjoy!
|
106
docs/migrate.md
|
@ -1,106 +0,0 @@
|
|||
# 🚚 Migrating from Misskey/FoundKey to Firefish
|
||||
|
||||
All the guides below assume you're starting in the root of the repo directory.
|
||||
|
||||
### Before proceeding
|
||||
|
||||
- **Ensure you have stopped all master and worker processes of Misskey.**
|
||||
- **Ensure you have backups of the database before performing any commands.**
|
||||
|
||||
## Misskey v13 and above
|
||||
|
||||
Tested with Misskey v13.11.3.
|
||||
|
||||
If your Misskey v13 is older, we recommend updating your Misskey to v13.11.3.
|
||||
|
||||
```sh
|
||||
wget -O mkv13.patch https://git.joinfirefish.org/firefish/firefish/-/raw/develop/docs/mkv13.patch
|
||||
wget -O mkv13_restore.patch https://git.joinfirefish.org/firefish/firefish/-/raw/develop/docs/mkv13_restore.patch
|
||||
git apply mkv13.patch mkv13_restore.patch
|
||||
|
||||
cd packages/backend
|
||||
|
||||
LINE_NUM="$(pnpm typeorm migration:show -d ormconfig.js | grep -n activeEmailValidation1657346559800 | cut -d ':' -f 1)"
|
||||
NUM_MIGRATIONS="$(pnpm typeorm migration:show -d ormconfig.js | tail -n+"$LINE_NUM" | grep '\[X\]' | wc -l)"
|
||||
|
||||
for i in $(seq 1 $NUM_MIGRATIONS); do pnpm typeorm migration:revert -d ormconfig.js; done
|
||||
|
||||
cd ../../
|
||||
|
||||
git remote set-url origin https://git.joinfirefish.org/firefish/firefish.git
|
||||
git fetch origin
|
||||
git stash push
|
||||
rm -rf fluent-emojis misskey-assets
|
||||
git switch main # or beta or develop
|
||||
git pull --ff
|
||||
wget -O renote_muting.patch https://git.joinfirefish.org/firefish/firefish/-/raw/develop/docs/renote_muting.patch
|
||||
git apply renote_muting.patch
|
||||
|
||||
pnpm install
|
||||
NODE_ENV=production pnpm run build
|
||||
pnpm run migrate
|
||||
git stash push
|
||||
```
|
||||
|
||||
Depending on the version you're migrating from, you may have to open Postgres with `psql -d your_database` and run the following commands:
|
||||
|
||||
```sql
|
||||
ALTER TABLE "meta" ADD COLUMN "disableLocalTimeline" boolean DEFAULT false;
|
||||
ALTER TABLE "meta" ADD COLUMN "disableGlobalTimeline" boolean DEFAULT false;
|
||||
ALTER TABLE "meta" ADD COLUMN "localDriveCapacityMb" integer DEFAULT 512;
|
||||
ALTER TABLE "meta" ADD COLUMN "remoteDriveCapacityMb" integer DEFAULT 128;
|
||||
ALTER TABLE "user" ADD COLUMN "isSilenced" boolean DEFAULT false;
|
||||
ALTER TABLE "user" ADD COLUMN "isAdmin" boolean DEFAULT false;
|
||||
ALTER TABLE "user" ADD COLUMN "isModerator" boolean DEFAULT false;
|
||||
ALTER TABLE "user" ADD COLUMN "remoteDriveCapacityMb" integer DEFAULT 128;
|
||||
ALTER TABLE "user" ADD COLUMN "driveCapacityOverrideMb" integer DEFAULT 128;
|
||||
ALTER TABLE "instance" ADD COLUMN "caughtAt" date;
|
||||
ALTER TABLE "instance" ADD COLUMN "latestRequestSentAt" date;
|
||||
ALTER TABLE "instance" ADD COLUMN "latestStatus" character varying(512);
|
||||
ALTER TABLE "instance" ADD COLUMN "lastCommunicatedAt" date;
|
||||
```
|
||||
|
||||
then quit with `\q`, and restart Firefish.
|
||||
|
||||
Note: Ignore errors of `column "xxx" of relation "xxx" already exists`.
|
||||
|
||||
If no other errors happened, your Firefish is ready to launch!
|
||||
|
||||
## Misskey v12.119 and before
|
||||
|
||||
```sh
|
||||
git remote set-url origin https://git.joinfirefish.org/firefish/firefish.git
|
||||
git fetch
|
||||
git checkout main # or beta or develop
|
||||
git pull --ff
|
||||
|
||||
NODE_ENV=production pnpm run migrate
|
||||
# build using prefered method
|
||||
```
|
||||
|
||||
## FoundKey
|
||||
|
||||
```sh
|
||||
wget -O fk.patch https://git.joinfirefish.org/firefish/firefish/-/raw/develop/docs/fk.patch
|
||||
git apply fk.patch
|
||||
cd packages/backend
|
||||
|
||||
LINE_NUM="$(npx typeorm migration:show -d ormconfig.js | grep -n uniformThemecolor1652859567549 | cut -d ':' -f 1)"
|
||||
NUM_MIGRATIONS="$(npx typeorm migration:show -d ormconfig.js | tail -n+"$LINE_NUM" | grep '\[X\]' | wc -l)"
|
||||
|
||||
for i in $(seq 1 $NUM_MIGRATIONS); do
|
||||
npx typeorm migration:revert -d ormconfig.js
|
||||
done
|
||||
|
||||
git remote set-url origin https://git.joinfirefish.org/firefish/firefish.git
|
||||
git fetch
|
||||
git checkout main # or beta or develop
|
||||
git pull --ff
|
||||
|
||||
NODE_ENV=production pnpm run migrate
|
||||
# build using prefered method
|
||||
```
|
||||
|
||||
## Reverse
|
||||
|
||||
You ***cannot*** migrate back to Misskey from Firefish due to re-hashing passwords on signin with argon2. You can migrate from Firefish to FoundKey, although this is not recommended due to FoundKey being end-of-life, and may have some problems with alt-text.
|
|
@ -1,45 +0,0 @@
|
|||
diff --git a/packages/backend/migration/1672704017999-remove-lastCommunicatedAt.js b/packages/backend/migration/1672704017999-remove-lastCommunicatedAt.js
|
||||
index 38a676985..c4ae690e0 100644
|
||||
--- a/packages/backend/migration/1672704017999-remove-lastCommunicatedAt.js
|
||||
+++ b/packages/backend/migration/1672704017999-remove-lastCommunicatedAt.js
|
||||
@@ -6,6 +6,8 @@ export class removeLastCommunicatedAt1672704017999 {
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
- await queryRunner.query(`ALTER TABLE "instance" ADD "lastCommunicatedAt" TIMESTAMP WITH TIME ZONE NOT NULL`);
|
||||
+ await queryRunner.query(`ALTER TABLE "instance" ADD "lastCommunicatedAt" TIMESTAMP WITH TIME ZONE`);
|
||||
+ await queryRunner.query(`UPDATE "instance" SET "lastCommunicatedAt" = COALESCE("infoUpdatedAt", "caughtAt")`);
|
||||
+ await queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "lastCommunicatedAt" SET NOT NULL`);
|
||||
}
|
||||
}
|
||||
diff --git a/packages/backend/migration/1673336077243-PollChoiceLength.js b/packages/backend/migration/1673336077243-PollChoiceLength.js
|
||||
index 810c626e0..5809528cb 100644
|
||||
--- a/packages/backend/migration/1673336077243-PollChoiceLength.js
|
||||
+++ b/packages/backend/migration/1673336077243-PollChoiceLength.js
|
||||
@@ -6,6 +6,6 @@ export class PollChoiceLength1673336077243 {
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
- await queryRunner.query(`ALTER TABLE "poll" ALTER COLUMN "choices" TYPE character varying(128) array`);
|
||||
+ //await queryRunner.query(`ALTER TABLE "poll" ALTER COLUMN "choices" TYPE character varying(128) array`);
|
||||
}
|
||||
}
|
||||
diff --git a/packages/backend/migration/1674118260469-achievement.js b/packages/backend/migration/1674118260469-achievement.js
|
||||
index 131ab96f8..57a922f83 100644
|
||||
--- a/packages/backend/migration/1674118260469-achievement.js
|
||||
+++ b/packages/backend/migration/1674118260469-achievement.js
|
||||
@@ -18,12 +18,13 @@ export class achievement1674118260469 {
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'pollEnded')`);
|
||||
+ await queryRunner.query(`CREATE TYPE "public"."notification_type_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum_old"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum_old"[]`);
|
||||
await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`);
|
||||
await queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum"`);
|
||||
await queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum_old" RENAME TO "user_profile_mutingnotificationtypes_enum"`);
|
||||
- await queryRunner.query(`CREATE TYPE "public"."notification_type_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`);
|
||||
+ await queryRunner.query(`DELETE FROM "public"."notification" WHERE "type" = 'achievementEarned'`);
|
||||
await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "public"."notification_type_enum_old" USING "type"::"text"::"public"."notification_type_enum_old"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."notification_type_enum"`);
|
||||
await queryRunner.query(`ALTER TYPE "public"."notification_type_enum_old" RENAME TO "notification_type_enum"`);
|
|
@ -1,127 +0,0 @@
|
|||
diff --git a/packages/backend/migration/1680491187535-cleanup.js b/packages/backend/migration/1680491187535-cleanup.js
|
||||
index 1e609ca06..0e6accf3e 100644
|
||||
--- a/packages/backend/migration/1680491187535-cleanup.js
|
||||
+++ b/packages/backend/migration/1680491187535-cleanup.js
|
||||
@@ -1,10 +1,40 @@
|
||||
export class cleanup1680491187535 {
|
||||
- name = 'cleanup1680491187535'
|
||||
+ name = "cleanup1680491187535";
|
||||
|
||||
- async up(queryRunner) {
|
||||
- await queryRunner.query(`DROP TABLE "antenna_note" `);
|
||||
- }
|
||||
+ async up(queryRunner) {
|
||||
+ await queryRunner.query(`DROP TABLE "antenna_note" `);
|
||||
+ }
|
||||
|
||||
- async down(queryRunner) {
|
||||
- }
|
||||
+ async down(queryRunner) {
|
||||
+ await queryRunner.query(
|
||||
+ `CREATE TABLE antenna_note ( id character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "antennaId" character varying(32) NOT NULL, read boolean DEFAULT false NOT NULL)`,
|
||||
+ );
|
||||
+ await queryRunner.query(
|
||||
+ `COMMENT ON COLUMN antenna_note."noteId" IS 'The note ID.'`,
|
||||
+ );
|
||||
+ await queryRunner.query(
|
||||
+ `COMMENT ON COLUMN antenna_note."antennaId" IS 'The antenna ID.'`,
|
||||
+ );
|
||||
+ await queryRunner.query(
|
||||
+ `ALTER TABLE ONLY antenna_note ADD CONSTRAINT "PK_fb28d94d0989a3872df19fd6ef8" PRIMARY KEY (id)`,
|
||||
+ );
|
||||
+ await queryRunner.query(
|
||||
+ `CREATE INDEX "IDX_0d775946662d2575dfd2068a5f" ON antenna_note USING btree ("antennaId")`,
|
||||
+ );
|
||||
+ await queryRunner.query(
|
||||
+ `CREATE UNIQUE INDEX "IDX_335a0bf3f904406f9ef3dd51c2" ON antenna_note USING btree ("noteId", "antennaId")`,
|
||||
+ );
|
||||
+ await queryRunner.query(
|
||||
+ `CREATE INDEX "IDX_9937ea48d7ae97ffb4f3f063a4" ON antenna_note USING btree (read)`,
|
||||
+ );
|
||||
+ await queryRunner.query(
|
||||
+ `CREATE INDEX "IDX_bd0397be22147e17210940e125" ON antenna_note USING btree ("noteId")`,
|
||||
+ );
|
||||
+ await queryRunner.query(
|
||||
+ `ALTER TABLE ONLY antenna_note ADD CONSTRAINT "FK_0d775946662d2575dfd2068a5f5" FOREIGN KEY ("antennaId") REFERENCES antenna(id) ON DELETE CASCADE`,
|
||||
+ );
|
||||
+ await queryRunner.query(
|
||||
+ `ALTER TABLE ONLY antenna_note ADD CONSTRAINT "FK_bd0397be22147e17210940e125b" FOREIGN KEY ("noteId") REFERENCES note(id) ON DELETE CASCADE`,
|
||||
+ );
|
||||
+ }
|
||||
}
|
||||
diff --git a/packages/backend/migration/1680582195041-cleanup.js b/packages/backend/migration/1680582195041-cleanup.js
|
||||
index c587e456a..a91d6ff3c 100644
|
||||
--- a/packages/backend/migration/1680582195041-cleanup.js
|
||||
+++ b/packages/backend/migration/1680582195041-cleanup.js
|
||||
@@ -1,11 +1,64 @@
|
||||
export class cleanup1680582195041 {
|
||||
- name = 'cleanup1680582195041'
|
||||
+ name = "cleanup1680582195041";
|
||||
|
||||
- async up(queryRunner) {
|
||||
- await queryRunner.query(`DROP TABLE "notification" `);
|
||||
- }
|
||||
+ async up(queryRunner) {
|
||||
+ await queryRunner.query(`DROP TABLE "notification"`);
|
||||
+ }
|
||||
|
||||
- async down(queryRunner) {
|
||||
-
|
||||
- }
|
||||
+ async down(queryRunner) {
|
||||
+ await queryRunner.query(
|
||||
+ `CREATE TABLE notification ( id character varying(32) NOT NULL, "createdAt" timestamp with time zone NOT NULL, "notifieeId" character varying(32) NOT NULL, "notifierId" character varying(32), "isRead" boolean DEFAULT false NOT NULL, "noteId" character varying(32), reaction character varying(128), choice integer, "followRequestId" character varying(32), type notification_type_enum NOT NULL, "customBody" character varying(2048), "customHeader" character varying(256), "customIcon" character varying(1024), "appAccessTokenId" character varying(32), achievement character varying(128))`,
|
||||
+ );
|
||||
+ await queryRunner.query(
|
||||
+ `COMMENT ON COLUMN notification."createdAt" IS 'The created date of the Notification.'`,
|
||||
+ );
|
||||
+ await queryRunner.query(
|
||||
+ `COMMENT ON COLUMN notification."notifieeId" IS 'The ID of recipient user of the Notification.'`,
|
||||
+ );
|
||||
+ await queryRunner.query(
|
||||
+ `COMMENT ON COLUMN notification."notifierId" IS 'The ID of sender user of the Notification.'`,
|
||||
+ );
|
||||
+ await queryRunner.query(
|
||||
+ `COMMENT ON COLUMN notification."isRead" IS 'Whether the Notification is read.'`,
|
||||
+ );
|
||||
+ await queryRunner.query(
|
||||
+ `COMMENT ON COLUMN notification.type IS 'The type of the Notification.'`,
|
||||
+ );
|
||||
+ await queryRunner.query(
|
||||
+ `ALTER TABLE ONLY notification ADD CONSTRAINT "PK_705b6c7cdf9b2c2ff7ac7872cb7" PRIMARY KEY (id)`,
|
||||
+ );
|
||||
+ await queryRunner.query(
|
||||
+ `CREATE INDEX "IDX_080ab397c379af09b9d2169e5b" ON notification USING btree ("isRead")`,
|
||||
+ );
|
||||
+ await queryRunner.query(
|
||||
+ `CREATE INDEX "IDX_33f33cc8ef29d805a97ff4628b" ON notification USING btree (type)`,
|
||||
+ );
|
||||
+ await queryRunner.query(
|
||||
+ `CREATE INDEX "IDX_3b4e96eec8d36a8bbb9d02aa71" ON notification USING btree ("notifierId")`,
|
||||
+ );
|
||||
+ await queryRunner.query(
|
||||
+ `CREATE INDEX "IDX_3c601b70a1066d2c8b517094cb" ON notification USING btree ("notifieeId")`,
|
||||
+ );
|
||||
+ await queryRunner.query(
|
||||
+ `CREATE INDEX "IDX_b11a5e627c41d4dc3170f1d370" ON notification USING btree ("createdAt")`,
|
||||
+ );
|
||||
+ await queryRunner.query(
|
||||
+ `CREATE INDEX "IDX_e22bf6bda77b6adc1fd9e75c8c" ON notification USING btree ("appAccessTokenId")`,
|
||||
+ );
|
||||
+ await queryRunner.query(
|
||||
+ `ALTER TABLE ONLY notification ADD CONSTRAINT "FK_3b4e96eec8d36a8bbb9d02aa710" FOREIGN KEY ("notifierId") REFERENCES "user"(id) ON DELETE CASCADE`,
|
||||
+ );
|
||||
+ await queryRunner.query(
|
||||
+ `ALTER TABLE ONLY notification ADD CONSTRAINT "FK_3c601b70a1066d2c8b517094cb9" FOREIGN KEY ("notifieeId") REFERENCES "user"(id) ON DELETE CASCADE`,
|
||||
+ );
|
||||
+ await queryRunner.query(
|
||||
+ `ALTER TABLE ONLY notification ADD CONSTRAINT "FK_769cb6b73a1efe22ddf733ac453" FOREIGN KEY ("noteId") REFERENCES note(id) ON DELETE CASCADE`,
|
||||
+ );
|
||||
+ await queryRunner.query(
|
||||
+ `ALTER TABLE ONLY notification ADD CONSTRAINT "FK_bd7fab507621e635b32cd31892c" FOREIGN KEY ("followRequestId") REFERENCES follow_request(id) ON DELETE CASCADE`,
|
||||
+ );
|
||||
+ await queryRunner.query(
|
||||
+ `ALTER TABLE ONLY notification ADD CONSTRAINT "FK_e22bf6bda77b6adc1fd9e75c8c9" FOREIGN KEY ("appAccessTokenId") REFERENCES access_token(id) ON DELETE CASCADE`,
|
||||
+ );
|
||||
+ }
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
diff --git a/packages/backend/migration/1665091090561-add-renote-muting.js b/packages/backend/migration/1665091090561-add-renote-muting.js
|
||||
index 2c76aaff5..f8541c818 100644
|
||||
--- a/packages/backend/migration/1665091090561-add-renote-muting.js
|
||||
+++ b/packages/backend/migration/1665091090561-add-renote-muting.js
|
||||
@@ -4,18 +4,6 @@ export class addRenoteMuting1665091090561 {
|
||||
}
|
||||
|
||||
async up(queryRunner) {
|
||||
- await queryRunner.query(
|
||||
- `CREATE TABLE "renote_muting" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "muteeId" character varying(32) NOT NULL, "muterId" character varying(32) NOT NULL, CONSTRAINT "PK_renoteMuting_id" PRIMARY KEY ("id"))`,
|
||||
- );
|
||||
- await queryRunner.query(
|
||||
- `CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt") `,
|
||||
- );
|
||||
- await queryRunner.query(
|
||||
- `CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId") `,
|
||||
- );
|
||||
- await queryRunner.query(
|
||||
- `CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId") `,
|
||||
- );
|
||||
}
|
||||
|
||||
async down(queryRunner) {}
|
|
@ -1,13 +0,0 @@
|
|||
# Replace example.tld with your domain
|
||||
|
||||
<VirtualHost *:80>
|
||||
ServerName example.tld
|
||||
# For WebSocket
|
||||
ProxyPass "/streaming" "ws://127.0.0.1:3000/streaming/"
|
||||
# Proxy to Node
|
||||
ProxyPass "/" "http://127.0.0.1:3000/"
|
||||
ProxyPassReverse "/" "http://127.0.0.1:3000/"
|
||||
ProxyPreserveHost On
|
||||
# For files proxy
|
||||
AllowEncodedSlashes On
|
||||
</VirtualHost>
|
|
@ -1,72 +0,0 @@
|
|||
# Replace example.tld with your domain
|
||||
|
||||
# For WebSocket
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
proxy_cache_path /tmp/nginx_cache levels=1:2 keys_zone=cache1:16m max_size=1g inactive=720m use_temp_path=off;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name example.tld;
|
||||
|
||||
# For SSL domain validation
|
||||
root /var/www/html;
|
||||
location /.well-known/acme-challenge/ { allow all; }
|
||||
location /.well-known/pki-validation/ { allow all; }
|
||||
location / { return 301 https://$server_name$request_uri; }
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name example.tld;
|
||||
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:ssl_session_cache:10m;
|
||||
ssl_session_tickets off;
|
||||
|
||||
# To use Let's Encrypt certificate
|
||||
ssl_certificate /etc/letsencrypt/live/example.tld/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/example.tld/privkey.pem;
|
||||
|
||||
# To use Debian/Ubuntu's self-signed certificate (For testing or before issuing a certificate)
|
||||
#ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
|
||||
#ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
|
||||
|
||||
# SSL protocol settings
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
|
||||
# Change to your upload limit
|
||||
client_max_body_size 80m;
|
||||
|
||||
# Proxy to Node
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_http_version 1.1;
|
||||
proxy_redirect off;
|
||||
|
||||
# If it's behind another reverse proxy or CDN, remove the following.
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
|
||||
# For WebSocket
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
|
||||
# Cache settings
|
||||
proxy_cache cache1;
|
||||
proxy_cache_lock on;
|
||||
proxy_cache_use_stale updating;
|
||||
add_header X-Cache $upstream_cache_status;
|
||||
}
|
||||
}
|
294
flake.lock
|
@ -1,294 +0,0 @@
|
|||
{
|
||||
"nodes": {
|
||||
"devenv": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"nix": "nix",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"pre-commit-hooks": "pre-commit-hooks"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1685521914,
|
||||
"narHash": "sha256-0fdFP5IASLwJ0PSXrErW8PZon9TVYmi8VRF8OtjGkV4=",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"rev": "e206d8f2e3e8d6aa943656052f15bdfea8146b8d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"fenix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1685514167,
|
||||
"narHash": "sha256-urRxF0ZGSNeZjM4kALNg3wTh7fBscbqQmS6S/HU7Wms=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "3abfea51663583186f687c49a157eab1639349ca",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1673956053,
|
||||
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-parts": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": "nixpkgs-lib"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1685457039,
|
||||
"narHash": "sha256-bEFtQm+YyLxQjKQAaBHJyPN1z2wbhBnr2g1NJWSYjwM=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "80717d11615b6f42d1ad2e18ead51193fc15de69",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"locked": {
|
||||
"lastModified": 1667395993,
|
||||
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"devenv",
|
||||
"pre-commit-hooks",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1660459072,
|
||||
"narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "a20de23b925fd8264fd7fad6454652e142fd7f73",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"lowdown-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1633514407,
|
||||
"narHash": "sha256-Dw32tiMjdK9t3ETl5fzGrutQTzh2rufgZV4A/BbxuD4=",
|
||||
"owner": "kristapsdz",
|
||||
"repo": "lowdown",
|
||||
"rev": "d2c2b44ff6c27b936ec27358a2653caaef8f73b8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "kristapsdz",
|
||||
"repo": "lowdown",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix": {
|
||||
"inputs": {
|
||||
"lowdown-src": "lowdown-src",
|
||||
"nixpkgs": [
|
||||
"devenv",
|
||||
"nixpkgs"
|
||||
],
|
||||
"nixpkgs-regression": "nixpkgs-regression"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1676545802,
|
||||
"narHash": "sha256-EK4rZ+Hd5hsvXnzSzk2ikhStJnD63odF7SzsQ8CuSPU=",
|
||||
"owner": "domenkozar",
|
||||
"repo": "nix",
|
||||
"rev": "7c91803598ffbcfe4a55c44ac6d49b2cf07a527f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "domenkozar",
|
||||
"ref": "relaxed-flakes",
|
||||
"repo": "nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1678875422,
|
||||
"narHash": "sha256-T3o6NcQPwXjxJMn2shz86Chch4ljXgZn746c2caGxd8=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "126f49a01de5b7e35a43fd43f891ecf6d3a51459",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-lib": {
|
||||
"locked": {
|
||||
"dir": "lib",
|
||||
"lastModified": 1682879489,
|
||||
"narHash": "sha256-sASwo8gBt7JDnOOstnps90K1wxmVfyhsTPPNTGBPjjg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "da45bf6ec7bbcc5d1e14d3795c025199f28e0de0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"dir": "lib",
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-regression": {
|
||||
"locked": {
|
||||
"lastModified": 1643052045,
|
||||
"narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-stable": {
|
||||
"locked": {
|
||||
"lastModified": 1678872516,
|
||||
"narHash": "sha256-/E1YwtMtFAu2KUQKV/1+KFuReYPANM2Rzehk84VxVoc=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "9b8e5abb18324c7fe9f07cb100c3cd4a29cda8b8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-22.11",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1685399834,
|
||||
"narHash": "sha256-Lt7//5snriXSdJo5hlVcDkpERL1piiih0UXIz1RUcC4=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "58c85835512b0db938600b6fe13cc3e3dc4b364e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"pre-commit-hooks": {
|
||||
"inputs": {
|
||||
"flake-compat": [
|
||||
"devenv",
|
||||
"flake-compat"
|
||||
],
|
||||
"flake-utils": "flake-utils",
|
||||
"gitignore": "gitignore",
|
||||
"nixpkgs": [
|
||||
"devenv",
|
||||
"nixpkgs"
|
||||
],
|
||||
"nixpkgs-stable": "nixpkgs-stable"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1682596858,
|
||||
"narHash": "sha256-Hf9XVpqaGqe/4oDGr30W8HlsWvJXtMsEPHDqHZA6dDg=",
|
||||
"owner": "cachix",
|
||||
"repo": "pre-commit-hooks.nix",
|
||||
"rev": "fb58866e20af98779017134319b5663b8215d912",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "pre-commit-hooks.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"devenv": "devenv",
|
||||
"fenix": "fenix",
|
||||
"flake-parts": "flake-parts",
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
}
|
||||
},
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1685465261,
|
||||
"narHash": "sha256-aJ2nUinUrNcFi+pb47bS5IIAeSiUEEPLJY8W4Q8Pcjk=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "d2b3caa5b5694125fad04a9699e919444439f6a2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "rust-lang",
|
||||
"ref": "nightly",
|
||||
"repo": "rust-analyzer",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
86
flake.nix
|
@ -1,86 +0,0 @@
|
|||
{
|
||||
description = "Firefish development flake";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
|
||||
# Flake Parts framework(https://flake.parts)
|
||||
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||
# Devenv for better devShells(https://devenv.sh)
|
||||
devenv.url = "github:cachix/devenv";
|
||||
# Fenix for rust development
|
||||
fenix.url = "github:nix-community/fenix";
|
||||
fenix.inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
outputs = inputs@{ flake-parts, ... }:
|
||||
flake-parts.lib.mkFlake { inherit inputs; } {
|
||||
imports = [
|
||||
inputs.devenv.flakeModule
|
||||
];
|
||||
|
||||
# Define the systems that this works on. Only tested with x66_64-linux, add more if you test and it works.
|
||||
systems = [
|
||||
"x86_64-linux"
|
||||
];
|
||||
# Expose these attributes for every system defined above.
|
||||
perSystem = { config, pkgs, ... }: {
|
||||
# Devenv shells
|
||||
devenv = {
|
||||
shells = {
|
||||
# The default shell, used by nix-direnv
|
||||
default = {
|
||||
name = "firefish-dev-shell";
|
||||
# Add additional packages to our environment
|
||||
packages = [
|
||||
pkgs.nodePackages.pnpm
|
||||
|
||||
pkgs.python3
|
||||
];
|
||||
# No need to warn on a new version, we'll update as needed.
|
||||
devenv.warnOnNewVersion = false;
|
||||
# Enable typescript support
|
||||
languages.typescript.enable = true;
|
||||
# Enable javascript for NPM and PNPM
|
||||
languages.javascript.enable = true;
|
||||
languages.javascript.package = pkgs.nodejs_18;
|
||||
# Enable stable Rust for the backend
|
||||
languages.rust.enable = true;
|
||||
languages.rust.version = "stable";
|
||||
processes = {
|
||||
dev-server.exec = "pnpm run dev";
|
||||
};
|
||||
scripts = {
|
||||
build.exec = "pnpm run build";
|
||||
clean.exec = "pnpm run clean";
|
||||
clear-state.exec = "rm -rf .devenv/state/redis .devenv/state/postgres";
|
||||
format.exec = "pnpm run format";
|
||||
install-deps.exec = "pnpm install";
|
||||
migrate.exec = "pnpm run migrate";
|
||||
prepare-config.exec = "cp .config/devenv.yml .config/default.yml";
|
||||
};
|
||||
services = {
|
||||
postgres = {
|
||||
enable = true;
|
||||
package = pkgs.postgresql_12;
|
||||
initialDatabases = [{
|
||||
name = "firefish";
|
||||
}];
|
||||
initialScript = ''
|
||||
CREATE USER firefish WITH PASSWORD 'firefish';
|
||||
ALTER USER firefish WITH SUPERUSER;
|
||||
GRANT ALL ON DATABASE firefish TO firefish;
|
||||
'';
|
||||
listen_addresses = "127.0.0.1";
|
||||
port = 5432;
|
||||
};
|
||||
redis = {
|
||||
enable = true;
|
||||
bind = "127.0.0.1";
|
||||
port = 6379;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
# **DO NOT edit locale files** except `ja-JP.yml`.
|
||||
|
||||
When you add text to the ja-JP file (of misskey-dev/misskey), it will automatically be applied to other language files.
|
||||
Translations added in ja-JP file should contain the original Japanese strings.
|
||||
|
||||
Please see [Contribution guide](../CONTRIBUTING.md) for more information.
|
|
@ -4,7 +4,7 @@
|
|||
"codename": "aqua",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.joinfirefish.org/firefish/firefish.git"
|
||||
"url": "https://code.naskya.net/naskya/firefish"
|
||||
},
|
||||
"packageManager": "pnpm@8.6.11",
|
||||
"private": true,
|
||||
|
@ -24,11 +24,6 @@
|
|||
"lint": "pnpm -r --parallel run lint",
|
||||
"debug": "pnpm run build:debug && pnpm run start",
|
||||
"build:debug": "pnpm -r --parallel run build:debug && pnpm run gulp",
|
||||
"cy:open": "cypress open --browser --e2e --config-file=cypress.config.ts",
|
||||
"cy:run": "cypress run",
|
||||
"e2e": "start-server-and-test start:test http://localhost:61812 cy:run",
|
||||
"mocha": "pnpm --filter backend run mocha",
|
||||
"test": "pnpm run mocha",
|
||||
"format": "pnpm -r --parallel run format",
|
||||
"clean": "pnpm node ./scripts/clean.js",
|
||||
"clean-all": "pnpm node ./scripts/clean-all.js",
|
||||
|
@ -51,7 +46,6 @@
|
|||
"@types/node": "20.4.9",
|
||||
"chalk": "4.1.2",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "10.11.0",
|
||||
"execa": "5.1.1",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-cssnano": "2.1.3",
|
||||
|
@ -60,7 +54,6 @@
|
|||
"gulp-terser": "2.1.0",
|
||||
"install-peers": "^1.0.4",
|
||||
"rome": "^12.1.3",
|
||||
"start-server-and-test": "1.15.2",
|
||||
"typescript": "5.1.6"
|
||||
}
|
||||
}
|
||||
|
|
8
packages/backend/.idea/.gitignore
vendored
|
@ -1,8 +0,0 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"extension": ["ts","js","cjs","mjs"],
|
||||
"node-option": [
|
||||
"experimental-specifier-resolution=node",
|
||||
"loader=./test/loader.js"
|
||||
],
|
||||
"slow": 1000,
|
||||
"timeout": 30000,
|
||||
"exit": true
|
||||
}
|
10
packages/backend/.vscode/settings.json
vendored
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"typescript.tsdk": "node_modules\\typescript\\lib",
|
||||
"path-intellisense.mappings": {
|
||||
"@": "${workspaceRoot}/packages/backend/src/"
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
import test from "ava";
|
||||
|
||||
import {
|
||||
convertId,
|
||||
IdConvertType,
|
||||
nativeInitIdGenerator,
|
||||
nativeCreateId,
|
||||
nativeRandomStr,
|
||||
} from "../built/index.js";
|
||||
|
||||
test("convert to mastodon id", (t) => {
|
||||
t.is(convertId("9gf61ehcxv", IdConvertType.MastodonId), "960365976481219");
|
||||
t.is(
|
||||
convertId("9fbr9z0wbrjqyd3u", IdConvertType.MastodonId),
|
||||
"2083785058661759970208986",
|
||||
);
|
||||
t.is(
|
||||
convertId("9fbs680oyviiqrol9md73p8g", IdConvertType.MastodonId),
|
||||
"5878598648988104013828532260828151168",
|
||||
);
|
||||
});
|
||||
|
||||
test("create cuid2 with timestamp prefix", (t) => {
|
||||
nativeInitIdGenerator(16, "");
|
||||
t.not(nativeCreateId(Date.now()), nativeCreateId(Date.now()));
|
||||
t.is(nativeCreateId(Date.now()).length, 16);
|
||||
});
|
||||
|
||||
test("create random string", (t) => {
|
||||
t.not(nativeRandomStr(16), nativeRandomStr(16));
|
||||
t.is(nativeRandomStr(24).length, 24);
|
||||
});
|
|
@ -39,13 +39,9 @@
|
|||
"build:migration": "cargo build --locked --release --manifest-path ./migration/Cargo.toml && cp -v ./target/release/migration ./built/migration",
|
||||
"build:debug": "napi build --features napi --platform ./built/ && cargo build --locked --manifest-path ./migration/Cargo.toml && cp -v ./target/debug/migration ./built/migration",
|
||||
"prepublishOnly": "napi prepublish -t npm",
|
||||
"test": "pnpm run cargo:test && pnpm run build:napi && ava",
|
||||
"universal": "napi universal",
|
||||
"version": "napi version",
|
||||
"format": "cargo fmt --all",
|
||||
"lint": "cargo clippy --fix",
|
||||
"cargo:test": "pnpm run cargo:unit && pnpm run cargo:integration",
|
||||
"cargo:unit": "cargo test unit_test && cargo test -F napi unit_test",
|
||||
"cargo:integration": "cargo test -F noarray int_test -- --test-threads=1"
|
||||
"lint": "cargo clippy --fix"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,216 +0,0 @@
|
|||
#![cfg(not(feature = "napi"))]
|
||||
|
||||
mod model;
|
||||
|
||||
use chrono::Utc;
|
||||
use native_utils::database;
|
||||
use native_utils::model::entity;
|
||||
use native_utils::model::entity::sea_orm_active_enums::AntennaSrcEnum;
|
||||
use native_utils::util::{
|
||||
id::{create_id, init_id},
|
||||
random::gen_string,
|
||||
};
|
||||
use sea_orm::{
|
||||
sea_query::TableCreateStatement, ActiveModelTrait, ConnectionTrait, DbBackend, DbConn, DbErr,
|
||||
EntityTrait, IntoActiveModel, TransactionTrait,
|
||||
};
|
||||
|
||||
/// Insert predefined entries in the database.
|
||||
async fn prepare() {
|
||||
database::init_database("sqlite::memory:")
|
||||
.await
|
||||
.expect("Unable to initialize database connection");
|
||||
let db = database::get_database().expect("Unable to get database connection from pool");
|
||||
setup_schema(db).await;
|
||||
setup_model(db).await;
|
||||
}
|
||||
|
||||
/// Setup schemas in the database.
|
||||
async fn setup_schema(db: &DbConn) {
|
||||
let schema = sea_orm::Schema::new(DbBackend::Sqlite);
|
||||
let mut stmts: Vec<TableCreateStatement> = Vec::new();
|
||||
macro_rules! create_table_statement {
|
||||
($a:tt) => {
|
||||
stmts.push(schema.create_table_from_entity(entity::$a::Entity).if_not_exists().to_owned());
|
||||
};
|
||||
($a:tt, $($b:tt),+) => {
|
||||
create_table_statement!($a);
|
||||
create_table_statement!($($b),+);
|
||||
};
|
||||
}
|
||||
create_table_statement!(
|
||||
abuse_user_report,
|
||||
access_token,
|
||||
ad,
|
||||
announcement_read,
|
||||
announcement,
|
||||
antenna_note,
|
||||
antenna,
|
||||
app,
|
||||
attestation_challenge,
|
||||
auth_session,
|
||||
blocking,
|
||||
channel_following,
|
||||
channel_note_pining,
|
||||
channel,
|
||||
clip_note,
|
||||
clip,
|
||||
drive_file,
|
||||
drive_folder,
|
||||
emoji,
|
||||
following,
|
||||
follow_request,
|
||||
gallery_like,
|
||||
gallery_post,
|
||||
hashtag,
|
||||
instance,
|
||||
messaging_message,
|
||||
meta,
|
||||
migrations,
|
||||
moderation_log,
|
||||
muted_note,
|
||||
muting,
|
||||
note_edit,
|
||||
note_favorite,
|
||||
note_reaction,
|
||||
note,
|
||||
note_thread_muting,
|
||||
note_unread,
|
||||
note_watching,
|
||||
notification,
|
||||
page_like,
|
||||
page,
|
||||
password_reset_request,
|
||||
poll,
|
||||
poll_vote,
|
||||
promo_note,
|
||||
promo_read,
|
||||
registration_ticket,
|
||||
registry_item,
|
||||
relay,
|
||||
renote_muting,
|
||||
signin,
|
||||
sw_subscription,
|
||||
used_username,
|
||||
user_group_invitation,
|
||||
user_group_invite,
|
||||
user_group_joining,
|
||||
user_group,
|
||||
user_ip,
|
||||
user_keypair,
|
||||
user_list_joining,
|
||||
user_list,
|
||||
user_note_pining,
|
||||
user_pending,
|
||||
user_profile,
|
||||
user_publickey,
|
||||
user,
|
||||
user_security_key,
|
||||
webhook
|
||||
);
|
||||
db.transaction::<_, (), DbErr>(|txn| {
|
||||
Box::pin(async move {
|
||||
for stmt in stmts {
|
||||
txn.execute(txn.get_database_backend().build(&stmt)).await?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.await
|
||||
.expect("Unable to setup schemas");
|
||||
}
|
||||
|
||||
/// Delete all entries in the database.
|
||||
async fn cleanup() {
|
||||
let db = database::get_database().expect("Unable to get database connection from pool");
|
||||
db.transaction::<_, (), DbErr>(|txn| {
|
||||
Box::pin(async move {
|
||||
entity::user::Entity::delete_many().exec(txn).await.unwrap();
|
||||
entity::antenna::Entity::delete_many()
|
||||
.exec(txn)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.await
|
||||
.expect("Unable to delete predefined models");
|
||||
}
|
||||
|
||||
async fn setup_model(db: &DbConn) {
|
||||
init_id(16, "");
|
||||
|
||||
db.transaction::<_, (), DbErr>(|txn| {
|
||||
Box::pin(async move {
|
||||
let user_id = create_id(0).unwrap();
|
||||
let name = "Alice";
|
||||
let user_model = entity::user::Model {
|
||||
id: user_id.to_owned(),
|
||||
created_at: Utc::now().into(),
|
||||
username: name.to_lowercase(),
|
||||
username_lower: name.to_lowercase(),
|
||||
name: Some(name.to_string()),
|
||||
token: Some(gen_string(16)),
|
||||
is_admin: true,
|
||||
..Default::default()
|
||||
};
|
||||
user_model
|
||||
.into_active_model()
|
||||
.reset_all()
|
||||
.insert(txn)
|
||||
.await?;
|
||||
let antenna_model = entity::antenna::Model {
|
||||
id: create_id(0).unwrap(),
|
||||
created_at: Utc::now().into(),
|
||||
user_id: user_id.to_owned(),
|
||||
name: "Alice Antenna".to_string(),
|
||||
src: AntennaSrcEnum::All,
|
||||
keywords: vec![
|
||||
vec!["foo".to_string(), "bar".to_string()],
|
||||
vec!["foobar".to_string()],
|
||||
]
|
||||
.into(),
|
||||
exclude_keywords: vec![
|
||||
vec!["abc".to_string()],
|
||||
vec!["def".to_string(), "ghi".to_string()],
|
||||
]
|
||||
.into(),
|
||||
notify: true,
|
||||
case_sensitive: true,
|
||||
..Default::default()
|
||||
};
|
||||
antenna_model
|
||||
.into_active_model()
|
||||
.reset_all()
|
||||
.insert(txn)
|
||||
.await?;
|
||||
let note_model = entity::note::Model {
|
||||
id: create_id(0).unwrap(),
|
||||
created_at: Utc::now().into(),
|
||||
text: Some("Testing 123".to_string()),
|
||||
user_id: user_id.to_owned(),
|
||||
..Default::default()
|
||||
};
|
||||
note_model
|
||||
.into_active_model()
|
||||
.reset_all()
|
||||
.insert(txn)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.await
|
||||
.expect("Unable to setup predefined models");
|
||||
}
|
||||
|
||||
mod int_test {
|
||||
use super::{cleanup, prepare};
|
||||
|
||||
#[tokio::test]
|
||||
async fn can_prepare_and_cleanup() {
|
||||
prepare().await;
|
||||
cleanup().await;
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
mod repository;
|
|
@ -1 +0,0 @@
|
|||
mod antenna;
|
|
@ -1,116 +0,0 @@
|
|||
mod int_test {
|
||||
use native_utils::{database, model, util};
|
||||
|
||||
use model::{
|
||||
entity::{antenna, antenna_note, note, user},
|
||||
repository::Repository,
|
||||
schema,
|
||||
};
|
||||
use pretty_assertions::assert_eq;
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter};
|
||||
|
||||
use crate::{cleanup, prepare};
|
||||
|
||||
#[tokio::test]
|
||||
async fn can_pack() {
|
||||
prepare().await;
|
||||
let db = database::get_database().unwrap();
|
||||
|
||||
let alice_antenna = user::Entity::find()
|
||||
.filter(user::Column::Username.eq("alice"))
|
||||
.find_also_related(antenna::Entity)
|
||||
.one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("alice not found")
|
||||
.1
|
||||
.expect("alice's antenna not found");
|
||||
|
||||
let packed = alice_antenna
|
||||
.to_owned()
|
||||
.pack()
|
||||
.await
|
||||
.expect("Unable to pack");
|
||||
|
||||
let packed_by_id = antenna::Model::pack_by_id(alice_antenna.id.to_owned())
|
||||
.await
|
||||
.expect("Unable to pack");
|
||||
|
||||
let result = schema::Antenna {
|
||||
id: alice_antenna.id,
|
||||
created_at: alice_antenna.created_at.into(),
|
||||
name: "Alice Antenna".to_string(),
|
||||
keywords: vec![
|
||||
vec!["foo".to_string(), "bar".to_string()],
|
||||
vec!["foobar".to_string()],
|
||||
],
|
||||
exclude_keywords: vec![
|
||||
vec!["abc".to_string()],
|
||||
vec!["def".to_string(), "ghi".to_string()],
|
||||
],
|
||||
src: schema::AntennaSrc::All,
|
||||
user_list_id: None,
|
||||
user_group_id: None,
|
||||
users: vec![],
|
||||
instances: vec![],
|
||||
case_sensitive: true,
|
||||
notify: true,
|
||||
with_replies: false,
|
||||
with_file: false,
|
||||
has_unread_note: false,
|
||||
};
|
||||
|
||||
assert_eq!(packed, result);
|
||||
assert_eq!(packed_by_id, result);
|
||||
|
||||
cleanup().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unread_note() {
|
||||
prepare().await;
|
||||
let db = database::get_database().unwrap();
|
||||
|
||||
let (alice, alice_antenna) = user::Entity::find()
|
||||
.filter(user::Column::Username.eq("alice"))
|
||||
.find_also_related(antenna::Entity)
|
||||
.one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("alice not found");
|
||||
let alice_antenna = alice_antenna.expect("alice's antenna not found");
|
||||
let packed = alice_antenna
|
||||
.to_owned()
|
||||
.pack()
|
||||
.await
|
||||
.expect("Unable to pack");
|
||||
assert_eq!(packed.has_unread_note, false);
|
||||
|
||||
let note_model = note::Entity::find()
|
||||
.filter(note::Column::UserId.eq(alice.id))
|
||||
.one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("note not found");
|
||||
let antenna_note = antenna_note::Model {
|
||||
id: util::id::create_id(0).unwrap(),
|
||||
antenna_id: alice_antenna.id.to_owned(),
|
||||
note_id: note_model.id.to_owned(),
|
||||
read: false,
|
||||
};
|
||||
antenna_note
|
||||
.into_active_model()
|
||||
.reset_all()
|
||||
.insert(db)
|
||||
.await
|
||||
.unwrap();
|
||||
let packed = alice_antenna
|
||||
.to_owned()
|
||||
.pack()
|
||||
.await
|
||||
.expect("Unable to pack");
|
||||
assert_eq!(packed.has_unread_note, true);
|
||||
|
||||
cleanup().await;
|
||||
}
|
||||
}
|
|
@ -17,8 +17,6 @@
|
|||
"build:debug": "pnpm swc src -d built -s -D",
|
||||
"watch": "pnpm swc src -d built -D -w",
|
||||
"lint": "pnpm rome check --apply *",
|
||||
"mocha": "cross-env NODE_ENV=test TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha",
|
||||
"test": "pnpm run mocha",
|
||||
"format": "pnpm rome format * --write"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
@ -164,7 +162,6 @@
|
|||
"@types/koa__cors": "3.3.0",
|
||||
"@types/koa__multer": "2.0.4",
|
||||
"@types/koa__router": "8.0.11",
|
||||
"@types/mocha": "9.1.1",
|
||||
"@types/node": "18.11.18",
|
||||
"@types/node-fetch": "3.0.3",
|
||||
"@types/nodemailer": "6.4.9",
|
||||
|
@ -191,7 +188,6 @@
|
|||
"eslint": "^8.46.0",
|
||||
"execa": "6.1.0",
|
||||
"json5-loader": "4.0.1",
|
||||
"mocha": "10.2.0",
|
||||
"pug": "3.0.2",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"swc-loader": "^0.2.3",
|
||||
|
|
|
@ -1,102 +0,0 @@
|
|||
process.env.NODE_ENV = "test";
|
||||
|
||||
import * as assert from "assert";
|
||||
import rndstr from "rndstr";
|
||||
import { initDb } from "../src/db/postgre.js";
|
||||
import { initTestDb } from "./utils.js";
|
||||
|
||||
describe("ActivityPub", () => {
|
||||
before(async () => {
|
||||
//await initTestDb();
|
||||
await initDb();
|
||||
});
|
||||
|
||||
describe("Parse minimum object", () => {
|
||||
const host = "https://host1.test";
|
||||
const preferredUsername = `${rndstr("A-Z", 4)}${rndstr("a-z", 4)}`;
|
||||
const actorId = `${host}/users/${preferredUsername.toLowerCase()}`;
|
||||
|
||||
const actor = {
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
id: actorId,
|
||||
type: "Person",
|
||||
preferredUsername,
|
||||
inbox: `${actorId}/inbox`,
|
||||
outbox: `${actorId}/outbox`,
|
||||
};
|
||||
|
||||
const post = {
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
id: `${host}/users/${rndstr("0-9a-z", 8)}`,
|
||||
type: "Note",
|
||||
attributedTo: actor.id,
|
||||
to: "https://www.w3.org/ns/activitystreams#Public",
|
||||
content: "あ",
|
||||
};
|
||||
|
||||
it("Minimum Actor", async () => {
|
||||
const { MockResolver } = await import("./misc/mock-resolver.js");
|
||||
const { createPerson } = await import(
|
||||
"../src/remote/activitypub/models/person.js"
|
||||
);
|
||||
|
||||
const resolver = new MockResolver();
|
||||
resolver._register(actor.id, actor);
|
||||
|
||||
const user = await createPerson(actor.id, resolver);
|
||||
|
||||
assert.deepStrictEqual(user.uri, actor.id);
|
||||
assert.deepStrictEqual(user.username, actor.preferredUsername);
|
||||
assert.deepStrictEqual(user.inbox, actor.inbox);
|
||||
});
|
||||
|
||||
it("Minimum Note", async () => {
|
||||
const { MockResolver } = await import("./misc/mock-resolver.js");
|
||||
const { createNote } = await import(
|
||||
"../src/remote/activitypub/models/note.js"
|
||||
);
|
||||
|
||||
const resolver = new MockResolver();
|
||||
resolver._register(actor.id, actor);
|
||||
resolver._register(post.id, post);
|
||||
|
||||
const note = await createNote(post.id, resolver, true);
|
||||
|
||||
assert.deepStrictEqual(note?.uri, post.id);
|
||||
assert.deepStrictEqual(note.visibility, "public");
|
||||
assert.deepStrictEqual(note.text, post.content);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Truncate long name", () => {
|
||||
const host = "https://host1.test";
|
||||
const preferredUsername = `${rndstr("A-Z", 4)}${rndstr("a-z", 4)}`;
|
||||
const actorId = `${host}/users/${preferredUsername.toLowerCase()}`;
|
||||
|
||||
const name = rndstr("0-9a-z", 129);
|
||||
|
||||
const actor = {
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
id: actorId,
|
||||
type: "Person",
|
||||
preferredUsername,
|
||||
name,
|
||||
inbox: `${actorId}/inbox`,
|
||||
outbox: `${actorId}/outbox`,
|
||||
};
|
||||
|
||||
it("Actor", async () => {
|
||||
const { MockResolver } = await import("./misc/mock-resolver.js");
|
||||
const { createPerson } = await import(
|
||||
"../src/remote/activitypub/models/person.js"
|
||||
);
|
||||
|
||||
const resolver = new MockResolver();
|
||||
resolver._register(actor.id, actor);
|
||||
|
||||
const user = await createPerson(actor.id, resolver);
|
||||
|
||||
assert.deepStrictEqual(user.name, actor.name.substr(0, 128));
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,75 +0,0 @@
|
|||
import * as assert from "assert";
|
||||
import httpSignature from "@peertube/http-signature";
|
||||
import { genRsaKeyPair } from "../src/misc/gen-key-pair.js";
|
||||
import {
|
||||
createSignedPost,
|
||||
createSignedGet,
|
||||
} from "../src/remote/activitypub/ap-request.js";
|
||||
|
||||
export const buildParsedSignature = (
|
||||
signingString: string,
|
||||
signature: string,
|
||||
algorithm: string,
|
||||
) => {
|
||||
return {
|
||||
scheme: "Signature",
|
||||
params: {
|
||||
keyId: "KeyID", // dummy, not used for verify
|
||||
algorithm: algorithm,
|
||||
headers: ["(request-target)", "date", "host", "digest"], // dummy, not used for verify
|
||||
signature: signature,
|
||||
},
|
||||
signingString: signingString,
|
||||
algorithm: algorithm.toUpperCase(),
|
||||
keyId: "KeyID", // dummy, not used for verify
|
||||
};
|
||||
};
|
||||
|
||||
describe("ap-request", () => {
|
||||
it("createSignedPost with verify", async () => {
|
||||
const keypair = await genRsaKeyPair();
|
||||
const key = { keyId: "x", privateKeyPem: keypair.privateKey };
|
||||
const url = "https://example.com/inbox";
|
||||
const activity = { a: 1 };
|
||||
const body = JSON.stringify(activity);
|
||||
const headers = {
|
||||
"User-Agent": "UA",
|
||||
};
|
||||
|
||||
const req = createSignedPost({
|
||||
key,
|
||||
url,
|
||||
body,
|
||||
additionalHeaders: headers,
|
||||
});
|
||||
|
||||
const parsed = buildParsedSignature(
|
||||
req.signingString,
|
||||
req.signature,
|
||||
"rsa-sha256",
|
||||
);
|
||||
|
||||
const result = httpSignature.verifySignature(parsed, keypair.publicKey);
|
||||
assert.deepStrictEqual(result, true);
|
||||
});
|
||||
|
||||
it("createSignedGet with verify", async () => {
|
||||
const keypair = await genRsaKeyPair();
|
||||
const key = { keyId: "x", privateKeyPem: keypair.privateKey };
|
||||
const url = "https://example.com/outbox";
|
||||
const headers = {
|
||||
"User-Agent": "UA",
|
||||
};
|
||||
|
||||
const req = createSignedGet({ key, url, additionalHeaders: headers });
|
||||
|
||||
const parsed = buildParsedSignature(
|
||||
req.signingString,
|
||||
req.signature,
|
||||
"rsa-sha256",
|
||||
);
|
||||
|
||||
const result = httpSignature.verifySignature(parsed, keypair.publicKey);
|
||||
assert.deepStrictEqual(result, true);
|
||||
});
|
||||
});
|
|
@ -1,535 +0,0 @@
|
|||
process.env.NODE_ENV = "test";
|
||||
|
||||
import * as assert from "assert";
|
||||
import * as childProcess from "child_process";
|
||||
import {
|
||||
async,
|
||||
signup,
|
||||
request,
|
||||
post,
|
||||
startServer,
|
||||
shutdownServer,
|
||||
} from "./utils.js";
|
||||
|
||||
describe("API visibility", () => {
|
||||
let p: childProcess.ChildProcess;
|
||||
|
||||
before(async () => {
|
||||
p = await startServer();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await shutdownServer(p);
|
||||
});
|
||||
|
||||
describe("Note visibility", async () => {
|
||||
//#region vars
|
||||
/** ヒロイン */
|
||||
let alice: any;
|
||||
/** フォロワー */
|
||||
let follower: any;
|
||||
/** 非フォロワー */
|
||||
let other: any;
|
||||
/** 非フォロワーでもリプライやメンションをされた人 */
|
||||
let target: any;
|
||||
/** specified mentionでmentionを飛ばされる人 */
|
||||
let target2: any;
|
||||
|
||||
/** public-post */
|
||||
let pub: any;
|
||||
/** home-post */
|
||||
let home: any;
|
||||
/** followers-post */
|
||||
let fol: any;
|
||||
/** specified-post */
|
||||
let spe: any;
|
||||
|
||||
/** public-reply to target's post */
|
||||
let pubR: any;
|
||||
/** home-reply to target's post */
|
||||
let homeR: any;
|
||||
/** followers-reply to target's post */
|
||||
let folR: any;
|
||||
/** specified-reply to target's post */
|
||||
let speR: any;
|
||||
|
||||
/** public-mention to target */
|
||||
let pubM: any;
|
||||
/** home-mention to target */
|
||||
let homeM: any;
|
||||
/** followers-mention to target */
|
||||
let folM: any;
|
||||
/** specified-mention to target */
|
||||
let speM: any;
|
||||
|
||||
/** reply target post */
|
||||
let tgt: any;
|
||||
//#endregion
|
||||
|
||||
const show = async (noteId: any, by: any) => {
|
||||
return await request(
|
||||
"/notes/show",
|
||||
{
|
||||
noteId,
|
||||
},
|
||||
by,
|
||||
);
|
||||
};
|
||||
|
||||
before(async () => {
|
||||
//#region prepare
|
||||
// signup
|
||||
alice = await signup({ username: "alice" });
|
||||
follower = await signup({ username: "follower" });
|
||||
other = await signup({ username: "other" });
|
||||
target = await signup({ username: "target" });
|
||||
target2 = await signup({ username: "target2" });
|
||||
|
||||
// follow alice <= follower
|
||||
await request("/following/create", { userId: alice.id }, follower);
|
||||
|
||||
// normal posts
|
||||
pub = await post(alice, { text: "x", visibility: "public" });
|
||||
home = await post(alice, { text: "x", visibility: "home" });
|
||||
fol = await post(alice, { text: "x", visibility: "followers" });
|
||||
spe = await post(alice, {
|
||||
text: "x",
|
||||
visibility: "specified",
|
||||
visibleUserIds: [target.id],
|
||||
});
|
||||
|
||||
// replies
|
||||
tgt = await post(target, { text: "y", visibility: "public" });
|
||||
pubR = await post(alice, {
|
||||
text: "x",
|
||||
replyId: tgt.id,
|
||||
visibility: "public",
|
||||
});
|
||||
homeR = await post(alice, {
|
||||
text: "x",
|
||||
replyId: tgt.id,
|
||||
visibility: "home",
|
||||
});
|
||||
folR = await post(alice, {
|
||||
text: "x",
|
||||
replyId: tgt.id,
|
||||
visibility: "followers",
|
||||
});
|
||||
speR = await post(alice, {
|
||||
text: "x",
|
||||
replyId: tgt.id,
|
||||
visibility: "specified",
|
||||
});
|
||||
|
||||
// mentions
|
||||
pubM = await post(alice, {
|
||||
text: "@target x",
|
||||
replyId: tgt.id,
|
||||
visibility: "public",
|
||||
});
|
||||
homeM = await post(alice, {
|
||||
text: "@target x",
|
||||
replyId: tgt.id,
|
||||
visibility: "home",
|
||||
});
|
||||
folM = await post(alice, {
|
||||
text: "@target x",
|
||||
replyId: tgt.id,
|
||||
visibility: "followers",
|
||||
});
|
||||
speM = await post(alice, {
|
||||
text: "@target2 x",
|
||||
replyId: tgt.id,
|
||||
visibility: "specified",
|
||||
});
|
||||
//#endregion
|
||||
});
|
||||
|
||||
//#region show post
|
||||
// public
|
||||
it("[show] public-postを自分が見れる", async(async () => {
|
||||
const res = await show(pub.id, alice);
|
||||
assert.strictEqual(res.body.text, "x");
|
||||
}));
|
||||
|
||||
it("[show] public-postをフォロワーが見れる", async(async () => {
|
||||
const res = await show(pub.id, follower);
|
||||
assert.strictEqual(res.body.text, "x");
|
||||
}));
|
||||
|
||||
it("[show] public-postを非フォロワーが見れる", async(async () => {
|
||||
const res = await show(pub.id, other);
|
||||
assert.strictEqual(res.body.text, "x");
|
||||
}));
|
||||
|
||||
it("[show] public-postを未認証が見れる", async(async () => {
|
||||
const res = await show(pub.id, null);
|
||||
assert.strictEqual(res.body.text, "x");
|
||||
}));
|
||||
|
||||
// home
|
||||
it("[show] home-postを自分が見れる", async(async () => {
|
||||
const res = await show(home.id, alice);
|
||||
assert.strictEqual(res.body.text, "x");
|
||||
}));
|
||||
|
||||
it("[show] home-postをフォロワーが見れる", async(async () => {
|
||||
const res = await show(home.id, follower);
|
||||
assert.strictEqual(res.body.text, "x");
|
||||
}));
|
||||
|
||||
it("[show] home-postを非フォロワーが見れる", async(async () => {
|
||||
const res = await show(home.id, other);
|
||||
assert.strictEqual(res.body.text, "x");
|
||||
}));
|
||||
|
||||
it("[show] home-postを未認証が見れる", async(async () => {
|
||||
const res = await show(home.id, null);
|
||||
assert.strictEqual(res.body.text, "x");
|
||||
}));
|
||||
|
||||
// followers
|
||||
it("[show] followers-postを自分が見れる", async(async () => {
|
||||
const res = await show(fol.id, alice);
|
||||
assert.strictEqual(res.body.text, "x");
|
||||
}));
|
||||
|
||||
it("[show] followers-postをフォロワーが見れる", async(async () => {
|
||||
const res = await show(fol.id, follower);
|
||||
assert.strictEqual(res.body.text, "x");
|
||||
}));
|
||||
|
||||
it("[show] followers-postを非フォロワーが見れない", async(async () => {
|
||||
const res = await show(fol.id, other);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
|
||||
it("[show] followers-postを未認証が見れない", async(async () => {
|
||||
const res = await show(fol.id, null);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
|
||||
// specified
|
||||
it("[show] specified-postを自分が見れる", async(async () => {
|
||||
const res = await show(spe.id, alice);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
|
||||
it("[show] specified-postを指定ユーザーが見れる", async(async () => {
|
||||
const res = await show(spe.id, target);
|
||||
assert.strictEqual(res.body.text, "x");
|
||||
}));
|
||||
|
||||
it("[show] specified-postをフォロワーが見れない", async(async () => {
|
||||
const res = await show(spe.id, follower);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
|
||||
it("[show] specified-postを非フォロワーが見れない", async(async () => {
|
||||
const res = await show(spe.id, other);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
|
||||
it("[show] specified-postを未認証が見れない", async(async () => {
|
||||
const res = await show(spe.id, null);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
//#endregion
|
||||
|
||||
//#region show reply
|
||||
// public
|
||||
it("[show] public-replyを自分が見れる", async(async () => {
|
||||
const res = await show(pubR.id, alice);
|
||||
assert.strictEqual(res.body.text, "x");
|
||||
}));
|
||||
|
||||
it("[show] public-replyをされた人が見れる", async(async () => {
|
||||
const res = await show(pubR.id, target);
|
||||
assert.strictEqual(res.body.text, "x");
|
||||
}));
|
||||
|
||||
it("[show] public-replyをフォロワーが見れる", async(async () => {
|
||||
const res = await show(pubR.id, follower);
|
||||
assert.strictEqual(res.body.text, "x");
|
||||
}));
|
||||
|
||||
it("[show] public-replyを非フォロワーが見れる", async(async () => {
|
||||
const res = await show(pubR.id, other);
|
||||
assert.strictEqual(res.body.text, "x");
|
||||
}));
|
||||
|
||||
it("[show] public-replyを未認証が見れる", async(async () => {
|
||||
const res = await show(pubR.id, null);
|
||||
assert.strictEqual(res.body.text, "x");
|
||||
}));
|
||||
|
||||
// home
|
||||
it("[show] home-replyを自分が見れる", async(async () => {
|
||||
const res = await show(homeR.id, alice);
|
||||
assert.strictEqual(res.body.text, "x");
|
||||
}));
|
||||
|
||||
it("[show] home-replyをされた人が見れる", async(async () => {
|
||||
const res = await show(homeR.id, target);
|
||||
assert.strictEqual(res.body.text, "x");
|
||||
}));
|
||||
|
||||
it("[show] home-replyをフォロワーが見れる", async(async () => {
|
||||
const res = await show(homeR.id, follower);
|
||||
assert.strictEqual(res.body.text, "x");
|
||||
}));
|
||||
|
||||
it("[show] home-replyを非フォロワーが見れる", async(async () => {
|
||||
const res = await show(homeR.id, other);
|
||||
assert.strictEqual(res.body.text, "x");
|
||||
}));
|
||||
|
||||
it("[show] home-replyを未認証が見れる", async(async () => {
|
||||
const res = await show(homeR.id, null);
|
||||
assert.strictEqual(res.body.text, "x");
|
||||
}));
|
||||
|
||||
// followers
|
||||
it("[show] followers-replyを自分が見れる", async(async () => {
|
||||
const res = await show(folR.id, alice);
|
||||
assert.strictEqual(res.body.text, "x");
|
||||
}));
|
||||
|
||||
it("[show] followers-replyを非フォロワーでもリプライされていれば見れる", async(async () => {
|
||||
const res = await show(folR.id, target);
|
||||
assert.strictEqual(res.body.text, "x");
|
||||
}));
|
||||
|
||||
it("[show] followers-replyをフォロワーが見れる", async(async () => {
|
||||
const res = await show(folR.id, follower);
|
||||
assert.strictEqual(res.body.text, "x");
|
||||
}));
|
||||
|
||||
it("[show] followers-replyを非フォロワーが見れない", async(async () => {
|
||||
const res = await show(folR.id, other);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
|
||||
it("[show] followers-replyを未認証が見れない", async(async () => {
|
||||
const res = await show(folR.id, null);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
|
||||
// specified
|
||||
it("[show] specified-replyを自分が見れる", async(async () => {
|
||||
const res = await show(speR.id, alice);
|
||||
assert.strictEqual(res.body.text, "x");
|
||||
}));
|
||||
|
||||
it("[show] specified-replyを指定ユーザーが見れる", async(async () => {
|
||||
const res = await show(speR.id, target);
|
||||
assert.strictEqual(res.body.text, "x");
|
||||
}));
|
||||
|
||||
it("[show] specified-replyをされた人が指定されてなくても見れる", async(async () => {
|
||||
const res = await show(speR.id, target);
|
||||
assert.strictEqual(res.body.text, "x");
|
||||
}));
|
||||
|
||||
it("[show] specified-replyをフォロワーが見れない", async(async () => {
|
||||
const res = await show(speR.id, follower);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
|
||||
it("[show] specified-replyを非フォロワーが見れない", async(async () => {
|
||||
const res = await show(speR.id, other);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
|
||||
it("[show] specified-replyを未認証が見れない", async(async () => {
|
||||
const res = await show(speR.id, null);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
//#endregion
|
||||
|
||||
//#region show mention
|
||||
// public
|
||||
it("[show] public-mentionを自分が見れる", async(async () => {
|
||||
const res = await show(pubM.id, alice);
|
||||
assert.strictEqual(res.body.text, "@target x");
|
||||
}));
|
||||
|
||||
it("[show] public-mentionをされた人が見れる", async(async () => {
|
||||
const res = await show(pubM.id, target);
|
||||
assert.strictEqual(res.body.text, "@target x");
|
||||
}));
|
||||
|
||||
it("[show] public-mentionをフォロワーが見れる", async(async () => {
|
||||
const res = await show(pubM.id, follower);
|
||||
assert.strictEqual(res.body.text, "@target x");
|
||||
}));
|
||||
|
||||
it("[show] public-mentionを非フォロワーが見れる", async(async () => {
|
||||
const res = await show(pubM.id, other);
|
||||
assert.strictEqual(res.body.text, "@target x");
|
||||
}));
|
||||
|
||||
it("[show] public-mentionを未認証が見れる", async(async () => {
|
||||
const res = await show(pubM.id, null);
|
||||
assert.strictEqual(res.body.text, "@target x");
|
||||
}));
|
||||
|
||||
// home
|
||||
it("[show] home-mentionを自分が見れる", async(async () => {
|
||||
const res = await show(homeM.id, alice);
|
||||
assert.strictEqual(res.body.text, "@target x");
|
||||
}));
|
||||
|
||||
it("[show] home-mentionをされた人が見れる", async(async () => {
|
||||
const res = await show(homeM.id, target);
|
||||
assert.strictEqual(res.body.text, "@target x");
|
||||
}));
|
||||
|
||||
it("[show] home-mentionをフォロワーが見れる", async(async () => {
|
||||
const res = await show(homeM.id, follower);
|
||||
assert.strictEqual(res.body.text, "@target x");
|
||||
}));
|
||||
|
||||
it("[show] home-mentionを非フォロワーが見れる", async(async () => {
|
||||
const res = await show(homeM.id, other);
|
||||
assert.strictEqual(res.body.text, "@target x");
|
||||
}));
|
||||
|
||||
it("[show] home-mentionを未認証が見れる", async(async () => {
|
||||
const res = await show(homeM.id, null);
|
||||
assert.strictEqual(res.body.text, "@target x");
|
||||
}));
|
||||
|
||||
// followers
|
||||
it("[show] followers-mentionを自分が見れる", async(async () => {
|
||||
const res = await show(folM.id, alice);
|
||||
assert.strictEqual(res.body.text, "@target x");
|
||||
}));
|
||||
|
||||
it("[show] followers-mentionをメンションされていれば非フォロワーでも見れる", async(async () => {
|
||||
const res = await show(folM.id, target);
|
||||
assert.strictEqual(res.body.text, "@target x");
|
||||
}));
|
||||
|
||||
it("[show] followers-mentionをフォロワーが見れる", async(async () => {
|
||||
const res = await show(folM.id, follower);
|
||||
assert.strictEqual(res.body.text, "@target x");
|
||||
}));
|
||||
|
||||
it("[show] followers-mentionを非フォロワーが見れない", async(async () => {
|
||||
const res = await show(folM.id, other);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
|
||||
it("[show] followers-mentionを未認証が見れない", async(async () => {
|
||||
const res = await show(folM.id, null);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
|
||||
// specified
|
||||
it("[show] specified-mentionを自分が見れる", async(async () => {
|
||||
const res = await show(speM.id, alice);
|
||||
assert.strictEqual(res.body.text, "@target2 x");
|
||||
}));
|
||||
|
||||
it("[show] specified-mentionを指定ユーザーが見れる", async(async () => {
|
||||
const res = await show(speM.id, target);
|
||||
assert.strictEqual(res.body.text, "@target2 x");
|
||||
}));
|
||||
|
||||
it("[show] specified-mentionをされた人が指定されてなかったら見れない", async(async () => {
|
||||
const res = await show(speM.id, target2);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
|
||||
it("[show] specified-mentionをフォロワーが見れない", async(async () => {
|
||||
const res = await show(speM.id, follower);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
|
||||
it("[show] specified-mentionを非フォロワーが見れない", async(async () => {
|
||||
const res = await show(speM.id, other);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
|
||||
it("[show] specified-mentionを未認証が見れない", async(async () => {
|
||||
const res = await show(speM.id, null);
|
||||
assert.strictEqual(res.status, 404);
|
||||
}));
|
||||
//#endregion
|
||||
|
||||
//#region HTL
|
||||
it("[HTL] public-post が 自分が見れる", async(async () => {
|
||||
const res = await request("/notes/timeline", { limit: 100 }, alice);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter((n: any) => n.id == pub.id);
|
||||
assert.strictEqual(notes[0].text, "x");
|
||||
}));
|
||||
|
||||
it("[HTL] public-post が 非フォロワーから見れない", async(async () => {
|
||||
const res = await request("/notes/timeline", { limit: 100 }, other);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter((n: any) => n.id == pub.id);
|
||||
assert.strictEqual(notes.length, 0);
|
||||
}));
|
||||
|
||||
it("[HTL] followers-post が フォロワーから見れる", async(async () => {
|
||||
const res = await request("/notes/timeline", { limit: 100 }, follower);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter((n: any) => n.id == fol.id);
|
||||
assert.strictEqual(notes[0].text, "x");
|
||||
}));
|
||||
//#endregion
|
||||
|
||||
//#region RTL
|
||||
it("[replies] followers-reply が フォロワーから見れる", async(async () => {
|
||||
const res = await request(
|
||||
"/notes/replies",
|
||||
{ noteId: tgt.id, limit: 100 },
|
||||
follower,
|
||||
);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter((n: any) => n.id == folR.id);
|
||||
assert.strictEqual(notes[0].text, "x");
|
||||
}));
|
||||
|
||||
it("[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない", async(async () => {
|
||||
const res = await request(
|
||||
"/notes/replies",
|
||||
{ noteId: tgt.id, limit: 100 },
|
||||
other,
|
||||
);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter((n: any) => n.id == folR.id);
|
||||
assert.strictEqual(notes.length, 0);
|
||||
}));
|
||||
|
||||
it("[replies] followers-reply が 非フォロワー (リプライ先である) から見れる", async(async () => {
|
||||
const res = await request(
|
||||
"/notes/replies",
|
||||
{ noteId: tgt.id, limit: 100 },
|
||||
target,
|
||||
);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter((n: any) => n.id == folR.id);
|
||||
assert.strictEqual(notes[0].text, "x");
|
||||
}));
|
||||
//#endregion
|
||||
|
||||
//#region MTL
|
||||
it("[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる", async(async () => {
|
||||
const res = await request("/notes/mentions", { limit: 100 }, target);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter((n: any) => n.id == folR.id);
|
||||
assert.strictEqual(notes[0].text, "x");
|
||||
}));
|
||||
|
||||
it("[mentions] followers-mention が 非フォロワー (メンション先である) から見れる", async(async () => {
|
||||
const res = await request("/notes/mentions", { limit: 100 }, target);
|
||||
assert.strictEqual(res.status, 200);
|
||||
const notes = res.body.filter((n: any) => n.id == folM.id);
|
||||
assert.strictEqual(notes[0].text, "@target x");
|
||||
}));
|
||||
//#endregion
|
||||
});
|
||||
});
|
|
@ -1,92 +0,0 @@
|
|||
process.env.NODE_ENV = "test";
|
||||
|
||||
import * as assert from "assert";
|
||||
import * as childProcess from "child_process";
|
||||
import {
|
||||
async,
|
||||
signup,
|
||||
request,
|
||||
post,
|
||||
react,
|
||||
uploadFile,
|
||||
startServer,
|
||||
shutdownServer,
|
||||
} from "./utils.js";
|
||||
|
||||
describe("API", () => {
|
||||
let p: childProcess.ChildProcess;
|
||||
let alice: any;
|
||||
let bob: any;
|
||||
let carol: any;
|
||||
|
||||
before(async () => {
|
||||
p = await startServer();
|
||||
alice = await signup({ username: "alice" });
|
||||
bob = await signup({ username: "bob" });
|
||||
carol = await signup({ username: "carol" });
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await shutdownServer(p);
|
||||
});
|
||||
|
||||
describe("General validation", () => {
|
||||
it("wrong type", async(async () => {
|
||||
const res = await request("/test", {
|
||||
required: true,
|
||||
string: 42,
|
||||
});
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it("missing require param", async(async () => {
|
||||
const res = await request("/test", {
|
||||
string: "a",
|
||||
});
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it("invalid misskey:id (empty string)", async(async () => {
|
||||
const res = await request("/test", {
|
||||
required: true,
|
||||
id: "",
|
||||
});
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it("valid misskey:id", async(async () => {
|
||||
const res = await request("/test", {
|
||||
required: true,
|
||||
id: "8wvhjghbxu",
|
||||
});
|
||||
assert.strictEqual(res.status, 200);
|
||||
}));
|
||||
|
||||
it("default value", async(async () => {
|
||||
const res = await request("/test", {
|
||||
required: true,
|
||||
string: "a",
|
||||
});
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.body.default, "hello");
|
||||
}));
|
||||
|
||||
it("can set null even if it has default value", async(async () => {
|
||||
const res = await request("/test", {
|
||||
required: true,
|
||||
nullableDefault: null,
|
||||
});
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.body.nullableDefault, null);
|
||||
}));
|
||||
|
||||
it("cannot set undefined if it has default value", async(async () => {
|
||||
const res = await request("/test", {
|
||||
required: true,
|
||||
nullableDefault: undefined,
|
||||
});
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.body.nullableDefault, "hello");
|
||||
}));
|
||||
});
|
||||
});
|
|
@ -1,129 +0,0 @@
|
|||
process.env.NODE_ENV = "test";
|
||||
|
||||
import * as assert from "assert";
|
||||
import * as childProcess from "child_process";
|
||||
import {
|
||||
async,
|
||||
signup,
|
||||
request,
|
||||
post,
|
||||
startServer,
|
||||
shutdownServer,
|
||||
} from "./utils.js";
|
||||
|
||||
describe("Block", () => {
|
||||
let p: childProcess.ChildProcess;
|
||||
|
||||
// alice blocks bob
|
||||
let alice: any;
|
||||
let bob: any;
|
||||
let carol: any;
|
||||
|
||||
before(async () => {
|
||||
p = await startServer();
|
||||
alice = await signup({ username: "alice" });
|
||||
bob = await signup({ username: "bob" });
|
||||
carol = await signup({ username: "carol" });
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await shutdownServer(p);
|
||||
});
|
||||
|
||||
it("Block作成", async(async () => {
|
||||
const res = await request(
|
||||
"/blocking/create",
|
||||
{
|
||||
userId: bob.id,
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
}));
|
||||
|
||||
it("ブロックされているユーザーをフォローできない", async(async () => {
|
||||
const res = await request("/following/create", { userId: alice.id }, bob);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
assert.strictEqual(
|
||||
res.body.error.id,
|
||||
"c4ab57cc-4e41-45e9-bfd9-584f61e35ce0",
|
||||
);
|
||||
}));
|
||||
|
||||
it("ブロックされているユーザーにリアクションできない", async(async () => {
|
||||
const note = await post(alice, { text: "hello" });
|
||||
|
||||
const res = await request(
|
||||
"/notes/reactions/create",
|
||||
{ noteId: note.id, reaction: "👍" },
|
||||
bob,
|
||||
);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
assert.strictEqual(
|
||||
res.body.error.id,
|
||||
"20ef5475-9f38-4e4c-bd33-de6d979498ec",
|
||||
);
|
||||
}));
|
||||
|
||||
it("ブロックされているユーザーに返信できない", async(async () => {
|
||||
const note = await post(alice, { text: "hello" });
|
||||
|
||||
const res = await request(
|
||||
"/notes/create",
|
||||
{ replyId: note.id, text: "yo" },
|
||||
bob,
|
||||
);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
assert.strictEqual(
|
||||
res.body.error.id,
|
||||
"b390d7e1-8a5e-46ed-b625-06271cafd3d3",
|
||||
);
|
||||
}));
|
||||
|
||||
it("ブロックされているユーザーのノートをRenoteできない", async(async () => {
|
||||
const note = await post(alice, { text: "hello" });
|
||||
|
||||
const res = await request(
|
||||
"/notes/create",
|
||||
{ renoteId: note.id, text: "yo" },
|
||||
bob,
|
||||
);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
assert.strictEqual(
|
||||
res.body.error.id,
|
||||
"b390d7e1-8a5e-46ed-b625-06271cafd3d3",
|
||||
);
|
||||
}));
|
||||
|
||||
// TODO: ユーザーリストに入れられないテスト
|
||||
|
||||
// TODO: ユーザーリストから除外されるテスト
|
||||
|
||||
it("タイムライン(LTL)にブロックされているユーザーの投稿が含まれない", async(async () => {
|
||||
const aliceNote = await post(alice);
|
||||
const bobNote = await post(bob);
|
||||
const carolNote = await post(carol);
|
||||
|
||||
const res = await request("/notes/local-timeline", {}, bob);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(
|
||||
res.body.some((note: any) => note.id === aliceNote.id),
|
||||
false,
|
||||
);
|
||||
assert.strictEqual(
|
||||
res.body.some((note: any) => note.id === bobNote.id),
|
||||
true,
|
||||
);
|
||||
assert.strictEqual(
|
||||
res.body.some((note: any) => note.id === carolNote.id),
|
||||
true,
|
||||
);
|
||||
}));
|
||||
});
|
|
@ -1,575 +0,0 @@
|
|||
process.env.NODE_ENV = "test";
|
||||
|
||||
import * as assert from "assert";
|
||||
import * as lolex from "@sinonjs/fake-timers";
|
||||
import TestChart from "../src/services/chart/charts/test.js";
|
||||
import TestGroupedChart from "../src/services/chart/charts/test-grouped.js";
|
||||
import TestUniqueChart from "../src/services/chart/charts/test-unique.js";
|
||||
import TestIntersectionChart from "../src/services/chart/charts/test-intersection.js";
|
||||
import { initDb } from "../src/db/postgre.js";
|
||||
|
||||
describe("Chart", () => {
|
||||
let testChart: TestChart;
|
||||
let testGroupedChart: TestGroupedChart;
|
||||
let testUniqueChart: TestUniqueChart;
|
||||
let testIntersectionChart: TestIntersectionChart;
|
||||
let clock: lolex.InstalledClock;
|
||||
|
||||
beforeEach(async () => {
|
||||
await initDb(true);
|
||||
|
||||
testChart = new TestChart();
|
||||
testGroupedChart = new TestGroupedChart();
|
||||
testUniqueChart = new TestUniqueChart();
|
||||
testIntersectionChart = new TestIntersectionChart();
|
||||
|
||||
clock = lolex.install({
|
||||
now: new Date(Date.UTC(2000, 0, 1, 0, 0, 0)),
|
||||
shouldClearNativeTimers: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clock.uninstall();
|
||||
});
|
||||
|
||||
it("Can updates", async () => {
|
||||
await testChart.increment();
|
||||
await testChart.save();
|
||||
|
||||
const chartHours = await testChart.getChart("hour", 3, null);
|
||||
const chartDays = await testChart.getChart("day", 3, null);
|
||||
|
||||
assert.deepStrictEqual(chartHours, {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [1, 0, 0],
|
||||
total: [1, 0, 0],
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(chartDays, {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [1, 0, 0],
|
||||
total: [1, 0, 0],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("Can updates (dec)", async () => {
|
||||
await testChart.decrement();
|
||||
await testChart.save();
|
||||
|
||||
const chartHours = await testChart.getChart("hour", 3, null);
|
||||
const chartDays = await testChart.getChart("day", 3, null);
|
||||
|
||||
assert.deepStrictEqual(chartHours, {
|
||||
foo: {
|
||||
dec: [1, 0, 0],
|
||||
inc: [0, 0, 0],
|
||||
total: [-1, 0, 0],
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(chartDays, {
|
||||
foo: {
|
||||
dec: [1, 0, 0],
|
||||
inc: [0, 0, 0],
|
||||
total: [-1, 0, 0],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("Empty chart", async () => {
|
||||
const chartHours = await testChart.getChart("hour", 3, null);
|
||||
const chartDays = await testChart.getChart("day", 3, null);
|
||||
|
||||
assert.deepStrictEqual(chartHours, {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [0, 0, 0],
|
||||
total: [0, 0, 0],
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(chartDays, {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [0, 0, 0],
|
||||
total: [0, 0, 0],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("Can updates at multiple times at same time", async () => {
|
||||
await testChart.increment();
|
||||
await testChart.increment();
|
||||
await testChart.increment();
|
||||
await testChart.save();
|
||||
|
||||
const chartHours = await testChart.getChart("hour", 3, null);
|
||||
const chartDays = await testChart.getChart("day", 3, null);
|
||||
|
||||
assert.deepStrictEqual(chartHours, {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [3, 0, 0],
|
||||
total: [3, 0, 0],
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(chartDays, {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [3, 0, 0],
|
||||
total: [3, 0, 0],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("複数回saveされてもデータの更新は一度だけ", async () => {
|
||||
await testChart.increment();
|
||||
await testChart.save();
|
||||
await testChart.save();
|
||||
await testChart.save();
|
||||
|
||||
const chartHours = await testChart.getChart("hour", 3, null);
|
||||
const chartDays = await testChart.getChart("day", 3, null);
|
||||
|
||||
assert.deepStrictEqual(chartHours, {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [1, 0, 0],
|
||||
total: [1, 0, 0],
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(chartDays, {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [1, 0, 0],
|
||||
total: [1, 0, 0],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("Can updates at different times", async () => {
|
||||
await testChart.increment();
|
||||
await testChart.save();
|
||||
|
||||
clock.tick("01:00:00");
|
||||
|
||||
await testChart.increment();
|
||||
await testChart.save();
|
||||
|
||||
const chartHours = await testChart.getChart("hour", 3, null);
|
||||
const chartDays = await testChart.getChart("day", 3, null);
|
||||
|
||||
assert.deepStrictEqual(chartHours, {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [1, 1, 0],
|
||||
total: [2, 1, 0],
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(chartDays, {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [2, 0, 0],
|
||||
total: [2, 0, 0],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// 仕様上はこうなってほしいけど、実装は難しそうなのでskip
|
||||
/*
|
||||
it('Can updates at different times without save', async () => {
|
||||
await testChart.increment();
|
||||
|
||||
clock.tick('01:00:00');
|
||||
|
||||
await testChart.increment();
|
||||
await testChart.save();
|
||||
|
||||
const chartHours = await testChart.getChart('hour', 3, null);
|
||||
const chartDays = await testChart.getChart('day', 3, null);
|
||||
|
||||
assert.deepStrictEqual(chartHours, {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [1, 1, 0],
|
||||
total: [2, 1, 0]
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(chartDays, {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [2, 0, 0],
|
||||
total: [2, 0, 0]
|
||||
},
|
||||
});
|
||||
});
|
||||
*/
|
||||
|
||||
it("Can padding", async () => {
|
||||
await testChart.increment();
|
||||
await testChart.save();
|
||||
|
||||
clock.tick("02:00:00");
|
||||
|
||||
await testChart.increment();
|
||||
await testChart.save();
|
||||
|
||||
const chartHours = await testChart.getChart("hour", 3, null);
|
||||
const chartDays = await testChart.getChart("day", 3, null);
|
||||
|
||||
assert.deepStrictEqual(chartHours, {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [1, 0, 1],
|
||||
total: [2, 1, 1],
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(chartDays, {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [2, 0, 0],
|
||||
total: [2, 0, 0],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// 要求された範囲にログがひとつもない場合でもパディングできる
|
||||
it("Can padding from past range", async () => {
|
||||
await testChart.increment();
|
||||
await testChart.save();
|
||||
|
||||
clock.tick("05:00:00");
|
||||
|
||||
const chartHours = await testChart.getChart("hour", 3, null);
|
||||
const chartDays = await testChart.getChart("day", 3, null);
|
||||
|
||||
assert.deepStrictEqual(chartHours, {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [0, 0, 0],
|
||||
total: [1, 1, 1],
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(chartDays, {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [1, 0, 0],
|
||||
total: [1, 0, 0],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// 要求された範囲の最も古い箇所に位置するログが存在しない場合でもパディングできる
|
||||
// Issue #3190
|
||||
it("Can padding from past range 2", async () => {
|
||||
await testChart.increment();
|
||||
await testChart.save();
|
||||
|
||||
clock.tick("05:00:00");
|
||||
|
||||
await testChart.increment();
|
||||
await testChart.save();
|
||||
|
||||
const chartHours = await testChart.getChart("hour", 3, null);
|
||||
const chartDays = await testChart.getChart("day", 3, null);
|
||||
|
||||
assert.deepStrictEqual(chartHours, {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [1, 0, 0],
|
||||
total: [2, 1, 1],
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(chartDays, {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [2, 0, 0],
|
||||
total: [2, 0, 0],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("Can specify offset", async () => {
|
||||
await testChart.increment();
|
||||
await testChart.save();
|
||||
|
||||
clock.tick("01:00:00");
|
||||
|
||||
await testChart.increment();
|
||||
await testChart.save();
|
||||
|
||||
const chartHours = await testChart.getChart(
|
||||
"hour",
|
||||
3,
|
||||
new Date(Date.UTC(2000, 0, 1, 0, 0, 0)),
|
||||
);
|
||||
const chartDays = await testChart.getChart(
|
||||
"day",
|
||||
3,
|
||||
new Date(Date.UTC(2000, 0, 1, 0, 0, 0)),
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(chartHours, {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [1, 0, 0],
|
||||
total: [1, 0, 0],
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(chartDays, {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [2, 0, 0],
|
||||
total: [2, 0, 0],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("Can specify offset (floor time)", async () => {
|
||||
clock.tick("00:30:00");
|
||||
|
||||
await testChart.increment();
|
||||
await testChart.save();
|
||||
|
||||
clock.tick("01:30:00");
|
||||
|
||||
await testChart.increment();
|
||||
await testChart.save();
|
||||
|
||||
const chartHours = await testChart.getChart(
|
||||
"hour",
|
||||
3,
|
||||
new Date(Date.UTC(2000, 0, 1, 0, 0, 0)),
|
||||
);
|
||||
const chartDays = await testChart.getChart(
|
||||
"day",
|
||||
3,
|
||||
new Date(Date.UTC(2000, 0, 1, 0, 0, 0)),
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(chartHours, {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [1, 0, 0],
|
||||
total: [1, 0, 0],
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(chartDays, {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [2, 0, 0],
|
||||
total: [2, 0, 0],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe("Grouped", () => {
|
||||
it("Can updates", async () => {
|
||||
await testGroupedChart.increment("alice");
|
||||
await testGroupedChart.save();
|
||||
|
||||
const aliceChartHours = await testGroupedChart.getChart(
|
||||
"hour",
|
||||
3,
|
||||
null,
|
||||
"alice",
|
||||
);
|
||||
const aliceChartDays = await testGroupedChart.getChart(
|
||||
"day",
|
||||
3,
|
||||
null,
|
||||
"alice",
|
||||
);
|
||||
const bobChartHours = await testGroupedChart.getChart(
|
||||
"hour",
|
||||
3,
|
||||
null,
|
||||
"bob",
|
||||
);
|
||||
const bobChartDays = await testGroupedChart.getChart(
|
||||
"day",
|
||||
3,
|
||||
null,
|
||||
"bob",
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(aliceChartHours, {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [1, 0, 0],
|
||||
total: [1, 0, 0],
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(aliceChartDays, {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [1, 0, 0],
|
||||
total: [1, 0, 0],
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(bobChartHours, {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [0, 0, 0],
|
||||
total: [0, 0, 0],
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(bobChartDays, {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [0, 0, 0],
|
||||
total: [0, 0, 0],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Unique increment", () => {
|
||||
it("Can updates", async () => {
|
||||
await testUniqueChart.uniqueIncrement("alice");
|
||||
await testUniqueChart.uniqueIncrement("alice");
|
||||
await testUniqueChart.uniqueIncrement("bob");
|
||||
await testUniqueChart.save();
|
||||
|
||||
const chartHours = await testUniqueChart.getChart("hour", 3, null);
|
||||
const chartDays = await testUniqueChart.getChart("day", 3, null);
|
||||
|
||||
assert.deepStrictEqual(chartHours, {
|
||||
foo: [2, 0, 0],
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(chartDays, {
|
||||
foo: [2, 0, 0],
|
||||
});
|
||||
});
|
||||
|
||||
describe("Intersection", () => {
|
||||
it("条件が満たされていない場合はカウントされない", async () => {
|
||||
await testIntersectionChart.addA("alice");
|
||||
await testIntersectionChart.addA("bob");
|
||||
await testIntersectionChart.addB("carol");
|
||||
await testIntersectionChart.save();
|
||||
|
||||
const chartHours = await testIntersectionChart.getChart(
|
||||
"hour",
|
||||
3,
|
||||
null,
|
||||
);
|
||||
const chartDays = await testIntersectionChart.getChart("day", 3, null);
|
||||
|
||||
assert.deepStrictEqual(chartHours, {
|
||||
a: [2, 0, 0],
|
||||
b: [1, 0, 0],
|
||||
aAndB: [0, 0, 0],
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(chartDays, {
|
||||
a: [2, 0, 0],
|
||||
b: [1, 0, 0],
|
||||
aAndB: [0, 0, 0],
|
||||
});
|
||||
});
|
||||
|
||||
it("条件が満たされている場合にカウントされる", async () => {
|
||||
await testIntersectionChart.addA("alice");
|
||||
await testIntersectionChart.addA("bob");
|
||||
await testIntersectionChart.addB("carol");
|
||||
await testIntersectionChart.addB("alice");
|
||||
await testIntersectionChart.save();
|
||||
|
||||
const chartHours = await testIntersectionChart.getChart(
|
||||
"hour",
|
||||
3,
|
||||
null,
|
||||
);
|
||||
const chartDays = await testIntersectionChart.getChart("day", 3, null);
|
||||
|
||||
assert.deepStrictEqual(chartHours, {
|
||||
a: [2, 0, 0],
|
||||
b: [2, 0, 0],
|
||||
aAndB: [1, 0, 0],
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(chartDays, {
|
||||
a: [2, 0, 0],
|
||||
b: [2, 0, 0],
|
||||
aAndB: [1, 0, 0],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Resync", () => {
|
||||
it("Can resync", async () => {
|
||||
testChart.total = 1;
|
||||
|
||||
await testChart.resync();
|
||||
|
||||
const chartHours = await testChart.getChart("hour", 3, null);
|
||||
const chartDays = await testChart.getChart("day", 3, null);
|
||||
|
||||
assert.deepStrictEqual(chartHours, {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [0, 0, 0],
|
||||
total: [1, 0, 0],
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(chartDays, {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [0, 0, 0],
|
||||
total: [1, 0, 0],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("Can resync (2)", async () => {
|
||||
await testChart.increment();
|
||||
await testChart.save();
|
||||
|
||||
clock.tick("01:00:00");
|
||||
|
||||
testChart.total = 100;
|
||||
|
||||
await testChart.resync();
|
||||
|
||||
const chartHours = await testChart.getChart("hour", 3, null);
|
||||
const chartDays = await testChart.getChart("day", 3, null);
|
||||
|
||||
assert.deepStrictEqual(chartHours, {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [0, 1, 0],
|
||||
total: [100, 1, 0],
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(chartDays, {
|
||||
foo: {
|
||||
dec: [0, 0, 0],
|
||||
inc: [1, 0, 0],
|
||||
total: [100, 0, 0],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,15 +0,0 @@
|
|||
version: "3"
|
||||
|
||||
services:
|
||||
redistest:
|
||||
image: redis:6
|
||||
ports:
|
||||
- "127.0.0.1:56312:6379"
|
||||
|
||||
dbtest:
|
||||
image: postgres:13
|
||||
ports:
|
||||
- "127.0.0.1:54312:5432"
|
||||
environment:
|
||||
POSTGRES_DB: "test-misskey"
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
|
@ -1,865 +0,0 @@
|
|||
/*
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as childProcess from 'child_process';
|
||||
import { async, signup, request, post, react, uploadFile, startServer, shutdownServer } from './utils.js';
|
||||
|
||||
describe('API: Endpoints', () => {
|
||||
let p: childProcess.ChildProcess;
|
||||
let alice: any;
|
||||
let bob: any;
|
||||
let carol: any;
|
||||
|
||||
before(async () => {
|
||||
p = await startServer();
|
||||
alice = await signup({ username: 'alice' });
|
||||
bob = await signup({ username: 'bob' });
|
||||
carol = await signup({ username: 'carol' });
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await shutdownServer(p);
|
||||
});
|
||||
|
||||
describe('signup', () => {
|
||||
it('不正なユーザー名でアカウントが作成できない', async(async () => {
|
||||
const res = await request('/signup', {
|
||||
username: 'test.',
|
||||
password: 'test'
|
||||
});
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('空のパスワードでアカウントが作成できない', async(async () => {
|
||||
const res = await request('/signup', {
|
||||
username: 'test',
|
||||
password: ''
|
||||
});
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('正しくアカウントが作成できる', async(async () => {
|
||||
const me = {
|
||||
username: 'test1',
|
||||
password: 'test1'
|
||||
};
|
||||
|
||||
const res = await request('/signup', me);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.username, me.username);
|
||||
}));
|
||||
|
||||
it('同じユーザー名のアカウントは作成できない', async(async () => {
|
||||
await signup({
|
||||
username: 'test2'
|
||||
});
|
||||
|
||||
const res = await request('/signup', {
|
||||
username: 'test2',
|
||||
password: 'test2'
|
||||
});
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('signin', () => {
|
||||
it('間違ったパスワードでサインインできない', async(async () => {
|
||||
await signup({
|
||||
username: 'test3',
|
||||
password: 'foo'
|
||||
});
|
||||
|
||||
const res = await request('/signin', {
|
||||
username: 'test3',
|
||||
password: 'bar'
|
||||
});
|
||||
|
||||
assert.strictEqual(res.status, 403);
|
||||
}));
|
||||
|
||||
it('クエリをインジェクションできない', async(async () => {
|
||||
await signup({
|
||||
username: 'test4'
|
||||
});
|
||||
|
||||
const res = await request('/signin', {
|
||||
username: 'test4',
|
||||
password: {
|
||||
$gt: ''
|
||||
}
|
||||
});
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('正しい情報でサインインできる', async(async () => {
|
||||
await signup({
|
||||
username: 'test5',
|
||||
password: 'foo'
|
||||
});
|
||||
|
||||
const res = await request('/signin', {
|
||||
username: 'test5',
|
||||
password: 'foo'
|
||||
});
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('i/update', () => {
|
||||
it('アカウント設定を更新できる', async(async () => {
|
||||
const myName = '大室櫻子';
|
||||
const myLocation = '七森中';
|
||||
const myBirthday = '2000-09-07';
|
||||
|
||||
const res = await request('/i/update', {
|
||||
name: myName,
|
||||
location: myLocation,
|
||||
birthday: myBirthday
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.name, myName);
|
||||
assert.strictEqual(res.body.location, myLocation);
|
||||
assert.strictEqual(res.body.birthday, myBirthday);
|
||||
}));
|
||||
|
||||
it('名前を空白にできない', async(async () => {
|
||||
const res = await request('/i/update', {
|
||||
name: ' '
|
||||
}, alice);
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('誕生日の設定を削除できる', async(async () => {
|
||||
await request('/i/update', {
|
||||
birthday: '2000-09-07'
|
||||
}, alice);
|
||||
|
||||
const res = await request('/i/update', {
|
||||
birthday: null
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.birthday, null);
|
||||
}));
|
||||
|
||||
it('不正な誕生日の形式で怒られる', async(async () => {
|
||||
const res = await request('/i/update', {
|
||||
birthday: '2000/09/07'
|
||||
}, alice);
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('users/show', () => {
|
||||
it('ユーザーが取得できる', async(async () => {
|
||||
const res = await request('/users/show', {
|
||||
userId: alice.id
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.id, alice.id);
|
||||
}));
|
||||
|
||||
it('ユーザーが存在しなかったら怒る', async(async () => {
|
||||
const res = await request('/users/show', {
|
||||
userId: '000000000000000000000000'
|
||||
});
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('間違ったIDで怒られる', async(async () => {
|
||||
const res = await request('/users/show', {
|
||||
userId: 'kyoppie'
|
||||
});
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('notes/show', () => {
|
||||
it('投稿が取得できる', async(async () => {
|
||||
const myPost = await post(alice, {
|
||||
text: 'test'
|
||||
});
|
||||
|
||||
const res = await request('/notes/show', {
|
||||
noteId: myPost.id
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.id, myPost.id);
|
||||
assert.strictEqual(res.body.text, myPost.text);
|
||||
}));
|
||||
|
||||
it('投稿が存在しなかったら怒る', async(async () => {
|
||||
const res = await request('/notes/show', {
|
||||
noteId: '000000000000000000000000'
|
||||
});
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('間違ったIDで怒られる', async(async () => {
|
||||
const res = await request('/notes/show', {
|
||||
noteId: 'kyoppie'
|
||||
});
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('notes/reactions/create', () => {
|
||||
it('リアクションできる', async(async () => {
|
||||
const bobPost = await post(bob);
|
||||
|
||||
const alice = await signup({ username: 'alice' });
|
||||
const res = await request('/notes/reactions/create', {
|
||||
noteId: bobPost.id,
|
||||
reaction: '🚀',
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 204);
|
||||
|
||||
const resNote = await request('/notes/show', {
|
||||
noteId: bobPost.id,
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(resNote.status, 200);
|
||||
assert.strictEqual(resNote.body.reactions['🚀'], [alice.id]);
|
||||
}));
|
||||
|
||||
it('自分の投稿にもリアクションできる', async(async () => {
|
||||
const myPost = await post(alice);
|
||||
|
||||
const res = await request('/notes/reactions/create', {
|
||||
noteId: myPost.id,
|
||||
reaction: '🚀',
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 204);
|
||||
}));
|
||||
|
||||
it('二重にリアクションできない', async(async () => {
|
||||
const bobPost = await post(bob);
|
||||
|
||||
await react(alice, bobPost, 'like');
|
||||
|
||||
const res = await request('/notes/reactions/create', {
|
||||
noteId: bobPost.id,
|
||||
reaction: '🚀',
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('存在しない投稿にはリアクションできない', async(async () => {
|
||||
const res = await request('/notes/reactions/create', {
|
||||
noteId: '000000000000000000000000',
|
||||
reaction: '🚀',
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('空のパラメータで怒られる', async(async () => {
|
||||
const res = await request('/notes/reactions/create', {}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('間違ったIDで怒られる', async(async () => {
|
||||
const res = await request('/notes/reactions/create', {
|
||||
noteId: 'kyoppie',
|
||||
reaction: '🚀',
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('following/create', () => {
|
||||
it('フォローできる', async(async () => {
|
||||
const res = await request('/following/create', {
|
||||
userId: alice.id
|
||||
}, bob);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
}));
|
||||
|
||||
it('既にフォローしている場合は怒る', async(async () => {
|
||||
const res = await request('/following/create', {
|
||||
userId: alice.id
|
||||
}, bob);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('存在しないユーザーはフォローできない', async(async () => {
|
||||
const res = await request('/following/create', {
|
||||
userId: '000000000000000000000000'
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('自分自身はフォローできない', async(async () => {
|
||||
const res = await request('/following/create', {
|
||||
userId: alice.id
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('空のパラメータで怒られる', async(async () => {
|
||||
const res = await request('/following/create', {}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('間違ったIDで怒られる', async(async () => {
|
||||
const res = await request('/following/create', {
|
||||
userId: 'foo'
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('following/delete', () => {
|
||||
it('フォロー解除できる', async(async () => {
|
||||
await request('/following/create', {
|
||||
userId: alice.id
|
||||
}, bob);
|
||||
|
||||
const res = await request('/following/delete', {
|
||||
userId: alice.id
|
||||
}, bob);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
}));
|
||||
|
||||
it('フォローしていない場合は怒る', async(async () => {
|
||||
const res = await request('/following/delete', {
|
||||
userId: alice.id
|
||||
}, bob);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('存在しないユーザーはフォロー解除できない', async(async () => {
|
||||
const res = await request('/following/delete', {
|
||||
userId: '000000000000000000000000'
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('自分自身はフォロー解除できない', async(async () => {
|
||||
const res = await request('/following/delete', {
|
||||
userId: alice.id
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('空のパラメータで怒られる', async(async () => {
|
||||
const res = await request('/following/delete', {}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('間違ったIDで怒られる', async(async () => {
|
||||
const res = await request('/following/delete', {
|
||||
userId: 'kyoppie'
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('drive', () => {
|
||||
it('ドライブ情報を取得できる', async(async () => {
|
||||
await uploadFile({
|
||||
userId: alice.id,
|
||||
size: 256
|
||||
});
|
||||
await uploadFile({
|
||||
userId: alice.id,
|
||||
size: 512
|
||||
});
|
||||
await uploadFile({
|
||||
userId: alice.id,
|
||||
size: 1024
|
||||
});
|
||||
const res = await request('/drive', {}, alice);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||
expect(res.body).have.property('usage').eql(1792);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('drive/files/create', () => {
|
||||
it('ファイルを作成できる', async(async () => {
|
||||
const res = await uploadFile(alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.name, 'Lenna.png');
|
||||
}));
|
||||
|
||||
it('ファイルに名前を付けられる', async(async () => {
|
||||
const res = await assert.request(server)
|
||||
.post('/drive/files/create')
|
||||
.field('i', alice.token)
|
||||
.field('name', 'Belmond.png')
|
||||
.attach('file', fs.readFileSync(__dirname + '/resources/Lenna.png'), 'Lenna.png');
|
||||
|
||||
expect(res).have.status(200);
|
||||
expect(res.body).be.a('object');
|
||||
expect(res.body).have.property('name').eql('Belmond.png');
|
||||
}));
|
||||
|
||||
it('ファイル無しで怒られる', async(async () => {
|
||||
const res = await request('/drive/files/create', {}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('SVGファイルを作成できる', async(async () => {
|
||||
const res = await uploadFile(alice, __dirname + '/resources/image.svg');
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.name, 'image.svg');
|
||||
assert.strictEqual(res.body.type, 'image/svg+xml');
|
||||
}));
|
||||
});
|
||||
|
||||
describe('drive/files/update', () => {
|
||||
it('名前を更新できる', async(async () => {
|
||||
const file = await uploadFile(alice);
|
||||
const newName = 'いちごパスタ.png';
|
||||
|
||||
const res = await request('/drive/files/update', {
|
||||
fileId: file.id,
|
||||
name: newName
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.name, newName);
|
||||
}));
|
||||
|
||||
it('他人のファイルは更新できない', async(async () => {
|
||||
const file = await uploadFile(bob);
|
||||
|
||||
const res = await request('/drive/files/update', {
|
||||
fileId: file.id,
|
||||
name: 'いちごパスタ.png'
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('親フォルダを更新できる', async(async () => {
|
||||
const file = await uploadFile(alice);
|
||||
const folder = (await request('/drive/folders/create', {
|
||||
name: 'test'
|
||||
}, alice)).body;
|
||||
|
||||
const res = await request('/drive/files/update', {
|
||||
fileId: file.id,
|
||||
folderId: folder.id
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.folderId, folder.id);
|
||||
}));
|
||||
|
||||
it('親フォルダを無しにできる', async(async () => {
|
||||
const file = await uploadFile(alice);
|
||||
|
||||
const folder = (await request('/drive/folders/create', {
|
||||
name: 'test'
|
||||
}, alice)).body;
|
||||
|
||||
await request('/drive/files/update', {
|
||||
fileId: file.id,
|
||||
folderId: folder.id
|
||||
}, alice);
|
||||
|
||||
const res = await request('/drive/files/update', {
|
||||
fileId: file.id,
|
||||
folderId: null
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.folderId, null);
|
||||
}));
|
||||
|
||||
it('他人のフォルダには入れられない', async(async () => {
|
||||
const file = await uploadFile(alice);
|
||||
const folder = (await request('/drive/folders/create', {
|
||||
name: 'test'
|
||||
}, bob)).body;
|
||||
|
||||
const res = await request('/drive/files/update', {
|
||||
fileId: file.id,
|
||||
folderId: folder.id
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('存在しないフォルダで怒られる', async(async () => {
|
||||
const file = await uploadFile(alice);
|
||||
|
||||
const res = await request('/drive/files/update', {
|
||||
fileId: file.id,
|
||||
folderId: '000000000000000000000000'
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('不正なフォルダIDで怒られる', async(async () => {
|
||||
const file = await uploadFile(alice);
|
||||
|
||||
const res = await request('/drive/files/update', {
|
||||
fileId: file.id,
|
||||
folderId: 'foo'
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('ファイルが存在しなかったら怒る', async(async () => {
|
||||
const res = await request('/drive/files/update', {
|
||||
fileId: '000000000000000000000000',
|
||||
name: 'いちごパスタ.png'
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('間違ったIDで怒られる', async(async () => {
|
||||
const res = await request('/drive/files/update', {
|
||||
fileId: 'kyoppie',
|
||||
name: 'いちごパスタ.png'
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('drive/folders/create', () => {
|
||||
it('フォルダを作成できる', async(async () => {
|
||||
const res = await request('/drive/folders/create', {
|
||||
name: 'test'
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.name, 'test');
|
||||
}));
|
||||
});
|
||||
|
||||
describe('drive/folders/update', () => {
|
||||
it('名前を更新できる', async(async () => {
|
||||
const folder = (await request('/drive/folders/create', {
|
||||
name: 'test'
|
||||
}, alice)).body;
|
||||
|
||||
const res = await request('/drive/folders/update', {
|
||||
folderId: folder.id,
|
||||
name: 'new name'
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.name, 'new name');
|
||||
}));
|
||||
|
||||
it('他人のフォルダを更新できない', async(async () => {
|
||||
const folder = (await request('/drive/folders/create', {
|
||||
name: 'test'
|
||||
}, bob)).body;
|
||||
|
||||
const res = await request('/drive/folders/update', {
|
||||
folderId: folder.id,
|
||||
name: 'new name'
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('親フォルダを更新できる', async(async () => {
|
||||
const folder = (await request('/drive/folders/create', {
|
||||
name: 'test'
|
||||
}, alice)).body;
|
||||
const parentFolder = (await request('/drive/folders/create', {
|
||||
name: 'parent'
|
||||
}, alice)).body;
|
||||
|
||||
const res = await request('/drive/folders/update', {
|
||||
folderId: folder.id,
|
||||
parentId: parentFolder.id
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.parentId, parentFolder.id);
|
||||
}));
|
||||
|
||||
it('親フォルダを無しに更新できる', async(async () => {
|
||||
const folder = (await request('/drive/folders/create', {
|
||||
name: 'test'
|
||||
}, alice)).body;
|
||||
const parentFolder = (await request('/drive/folders/create', {
|
||||
name: 'parent'
|
||||
}, alice)).body;
|
||||
await request('/drive/folders/update', {
|
||||
folderId: folder.id,
|
||||
parentId: parentFolder.id
|
||||
}, alice);
|
||||
|
||||
const res = await request('/drive/folders/update', {
|
||||
folderId: folder.id,
|
||||
parentId: null
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.parentId, null);
|
||||
}));
|
||||
|
||||
it('他人のフォルダを親フォルダに設定できない', async(async () => {
|
||||
const folder = (await request('/drive/folders/create', {
|
||||
name: 'test'
|
||||
}, alice)).body;
|
||||
const parentFolder = (await request('/drive/folders/create', {
|
||||
name: 'parent'
|
||||
}, bob)).body;
|
||||
|
||||
const res = await request('/drive/folders/update', {
|
||||
folderId: folder.id,
|
||||
parentId: parentFolder.id
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('フォルダが循環するような構造にできない', async(async () => {
|
||||
const folder = (await request('/drive/folders/create', {
|
||||
name: 'test'
|
||||
}, alice)).body;
|
||||
const parentFolder = (await request('/drive/folders/create', {
|
||||
name: 'parent'
|
||||
}, alice)).body;
|
||||
await request('/drive/folders/update', {
|
||||
folderId: parentFolder.id,
|
||||
parentId: folder.id
|
||||
}, alice);
|
||||
|
||||
const res = await request('/drive/folders/update', {
|
||||
folderId: folder.id,
|
||||
parentId: parentFolder.id
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('フォルダが循環するような構造にできない(再帰的)', async(async () => {
|
||||
const folderA = (await request('/drive/folders/create', {
|
||||
name: 'test'
|
||||
}, alice)).body;
|
||||
const folderB = (await request('/drive/folders/create', {
|
||||
name: 'test'
|
||||
}, alice)).body;
|
||||
const folderC = (await request('/drive/folders/create', {
|
||||
name: 'test'
|
||||
}, alice)).body;
|
||||
await request('/drive/folders/update', {
|
||||
folderId: folderB.id,
|
||||
parentId: folderA.id
|
||||
}, alice);
|
||||
await request('/drive/folders/update', {
|
||||
folderId: folderC.id,
|
||||
parentId: folderB.id
|
||||
}, alice);
|
||||
|
||||
const res = await request('/drive/folders/update', {
|
||||
folderId: folderA.id,
|
||||
parentId: folderC.id
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('フォルダが循環するような構造にできない(自身)', async(async () => {
|
||||
const folderA = (await request('/drive/folders/create', {
|
||||
name: 'test'
|
||||
}, alice)).body;
|
||||
|
||||
const res = await request('/drive/folders/update', {
|
||||
folderId: folderA.id,
|
||||
parentId: folderA.id
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('存在しない親フォルダを設定できない', async(async () => {
|
||||
const folder = (await request('/drive/folders/create', {
|
||||
name: 'test'
|
||||
}, alice)).body;
|
||||
|
||||
const res = await request('/drive/folders/update', {
|
||||
folderId: folder.id,
|
||||
parentId: '000000000000000000000000'
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('不正な親フォルダIDで怒られる', async(async () => {
|
||||
const folder = (await request('/drive/folders/create', {
|
||||
name: 'test'
|
||||
}, alice)).body;
|
||||
|
||||
const res = await request('/drive/folders/update', {
|
||||
folderId: folder.id,
|
||||
parentId: 'foo'
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('存在しないフォルダを更新できない', async(async () => {
|
||||
const res = await request('/drive/folders/update', {
|
||||
folderId: '000000000000000000000000'
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('不正なフォルダIDで怒られる', async(async () => {
|
||||
const res = await request('/drive/folders/update', {
|
||||
folderId: 'foo'
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('messaging/messages/create', () => {
|
||||
it('メッセージを送信できる', async(async () => {
|
||||
const res = await request('/messaging/messages/create', {
|
||||
userId: bob.id,
|
||||
text: 'test'
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.text, 'test');
|
||||
}));
|
||||
|
||||
it('自分自身にはメッセージを送信できない', async(async () => {
|
||||
const res = await request('/messaging/messages/create', {
|
||||
userId: alice.id,
|
||||
text: 'Yo'
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('存在しないユーザーにはメッセージを送信できない', async(async () => {
|
||||
const res = await request('/messaging/messages/create', {
|
||||
userId: '000000000000000000000000',
|
||||
text: 'test'
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('不正なユーザーIDで怒られる', async(async () => {
|
||||
const res = await request('/messaging/messages/create', {
|
||||
userId: 'foo',
|
||||
text: 'test'
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('テキストが無くて怒られる', async(async () => {
|
||||
const res = await request('/messaging/messages/create', {
|
||||
userId: bob.id
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it('文字数オーバーで怒られる', async(async () => {
|
||||
const res = await request('/messaging/messages/create', {
|
||||
userId: bob.id,
|
||||
text: '!'.repeat(1001)
|
||||
}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('notes/replies', () => {
|
||||
it('自分に閲覧権限のない投稿は含まれない', async(async () => {
|
||||
const alicePost = await post(alice, {
|
||||
text: 'foo'
|
||||
});
|
||||
|
||||
await post(bob, {
|
||||
replyId: alicePost.id,
|
||||
text: 'bar',
|
||||
visibility: 'specified',
|
||||
visibleUserIds: [alice.id]
|
||||
});
|
||||
|
||||
const res = await request('/notes/replies', {
|
||||
noteId: alicePost.id
|
||||
}, carol);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.length, 0);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('notes/timeline', () => {
|
||||
it('フォロワー限定投稿が含まれる', async(async () => {
|
||||
await request('/following/create', {
|
||||
userId: alice.id
|
||||
}, bob);
|
||||
|
||||
const alicePost = await post(alice, {
|
||||
text: 'foo',
|
||||
visibility: 'followers'
|
||||
});
|
||||
|
||||
const res = await request('/notes/timeline', {}, bob);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.length, 1);
|
||||
assert.strictEqual(res.body[0].id, alicePost.id);
|
||||
}));
|
||||
});
|
||||
});
|
||||
*/
|
|
@ -1,50 +0,0 @@
|
|||
import * as assert from "assert";
|
||||
|
||||
import { parse } from "mfm-js";
|
||||
import { extractMentions } from "../src/misc/extract-mentions.js";
|
||||
|
||||
describe("Extract mentions", () => {
|
||||
it("simple", () => {
|
||||
const ast = parse("@foo @bar @baz")!;
|
||||
const mentions = extractMentions(ast);
|
||||
assert.deepStrictEqual(mentions, [
|
||||
{
|
||||
username: "foo",
|
||||
acct: "@foo",
|
||||
host: null,
|
||||
},
|
||||
{
|
||||
username: "bar",
|
||||
acct: "@bar",
|
||||
host: null,
|
||||
},
|
||||
{
|
||||
username: "baz",
|
||||
acct: "@baz",
|
||||
host: null,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("nested", () => {
|
||||
const ast = parse("@foo **@bar** @baz")!;
|
||||
const mentions = extractMentions(ast);
|
||||
assert.deepStrictEqual(mentions, [
|
||||
{
|
||||
username: "foo",
|
||||
acct: "@foo",
|
||||
host: null,
|
||||
},
|
||||
{
|
||||
username: "bar",
|
||||
acct: "@bar",
|
||||
host: null,
|
||||
},
|
||||
{
|
||||
username: "baz",
|
||||
acct: "@baz",
|
||||
host: null,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -1,213 +0,0 @@
|
|||
process.env.NODE_ENV = "test";
|
||||
|
||||
import * as assert from "assert";
|
||||
import * as childProcess from "child_process";
|
||||
import * as openapi from "@redocly/openapi-core";
|
||||
import {
|
||||
async,
|
||||
startServer,
|
||||
signup,
|
||||
post,
|
||||
request,
|
||||
simpleGet,
|
||||
port,
|
||||
shutdownServer,
|
||||
} from "./utils.js";
|
||||
|
||||
// Request Accept
|
||||
const ONLY_AP = "application/activity+json";
|
||||
const PREFER_AP = "application/activity+json, */*";
|
||||
const PREFER_HTML = "text/html, */*";
|
||||
const UNSPECIFIED = "*/*";
|
||||
|
||||
// Response Contet-Type
|
||||
const AP = "application/activity+json; charset=utf-8";
|
||||
const JSON = "application/json; charset=utf-8";
|
||||
const HTML = "text/html; charset=utf-8";
|
||||
|
||||
describe("Fetch resource", () => {
|
||||
let p: childProcess.ChildProcess;
|
||||
|
||||
let alice: any;
|
||||
let alicesPost: any;
|
||||
|
||||
before(async () => {
|
||||
p = await startServer();
|
||||
alice = await signup({ username: "alice" });
|
||||
alicesPost = await post(alice, {
|
||||
text: "test",
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await shutdownServer(p);
|
||||
});
|
||||
|
||||
describe("Common", () => {
|
||||
it("meta", async(async () => {
|
||||
const res = await request("/meta", {});
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
}));
|
||||
|
||||
it("GET root", async(async () => {
|
||||
const res = await simpleGet("/");
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.type, HTML);
|
||||
}));
|
||||
|
||||
it("GET docs", async(async () => {
|
||||
const res = await simpleGet("/docs/ja-JP/about");
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.type, HTML);
|
||||
}));
|
||||
|
||||
it("GET api-doc", async(async () => {
|
||||
const res = await simpleGet("/api-doc");
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.type, HTML);
|
||||
}));
|
||||
|
||||
it("GET api.json", async(async () => {
|
||||
const res = await simpleGet("/api.json");
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.type, JSON);
|
||||
}));
|
||||
|
||||
it("Validate api.json", async(async () => {
|
||||
const config = await openapi.loadConfig();
|
||||
const result = await openapi.bundle({
|
||||
config,
|
||||
ref: `http://localhost:${port}/api.json`,
|
||||
});
|
||||
|
||||
for (const problem of result.problems) {
|
||||
console.log(`${problem.message} - ${problem.location[0]?.pointer}`);
|
||||
}
|
||||
|
||||
assert.strictEqual(result.problems.length, 0);
|
||||
}));
|
||||
|
||||
it("GET favicon.ico", async(async () => {
|
||||
const res = await simpleGet("/favicon.ico");
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.type, "image/x-icon");
|
||||
}));
|
||||
|
||||
it("GET apple-touch-icon.png", async(async () => {
|
||||
const res = await simpleGet("/apple-touch-icon.png");
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.type, "image/png");
|
||||
}));
|
||||
|
||||
it("GET twemoji svg", async(async () => {
|
||||
const res = await simpleGet("/twemoji/2764.svg");
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.type, "image/svg+xml");
|
||||
}));
|
||||
|
||||
it("GET twemoji svg with hyphen", async(async () => {
|
||||
const res = await simpleGet("/twemoji/2764-fe0f-200d-1f525.svg");
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.type, "image/svg+xml");
|
||||
}));
|
||||
});
|
||||
|
||||
describe("/@:username", () => {
|
||||
it("Only AP => AP", async(async () => {
|
||||
const res = await simpleGet(`/@${alice.username}`, ONLY_AP);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.type, AP);
|
||||
}));
|
||||
|
||||
it("Prefer AP => AP", async(async () => {
|
||||
const res = await simpleGet(`/@${alice.username}`, PREFER_AP);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.type, AP);
|
||||
}));
|
||||
|
||||
it("Prefer HTML => HTML", async(async () => {
|
||||
const res = await simpleGet(`/@${alice.username}`, PREFER_HTML);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.type, HTML);
|
||||
}));
|
||||
|
||||
it("Unspecified => HTML", async(async () => {
|
||||
const res = await simpleGet(`/@${alice.username}`, UNSPECIFIED);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.type, HTML);
|
||||
}));
|
||||
});
|
||||
|
||||
describe("/users/:id", () => {
|
||||
it("Only AP => AP", async(async () => {
|
||||
const res = await simpleGet(`/users/${alice.id}`, ONLY_AP);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.type, AP);
|
||||
}));
|
||||
|
||||
it("Prefer AP => AP", async(async () => {
|
||||
const res = await simpleGet(`/users/${alice.id}`, PREFER_AP);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.type, AP);
|
||||
}));
|
||||
|
||||
it("Prefer HTML => Redirect to /@:username", async(async () => {
|
||||
const res = await simpleGet(`/users/${alice.id}`, PREFER_HTML);
|
||||
assert.strictEqual(res.status, 302);
|
||||
assert.strictEqual(res.location, `/@${alice.username}`);
|
||||
}));
|
||||
|
||||
it("Undecided => HTML", async(async () => {
|
||||
const res = await simpleGet(`/users/${alice.id}`, UNSPECIFIED);
|
||||
assert.strictEqual(res.status, 302);
|
||||
assert.strictEqual(res.location, `/@${alice.username}`);
|
||||
}));
|
||||
});
|
||||
|
||||
describe("/notes/:id", () => {
|
||||
it("Only AP => AP", async(async () => {
|
||||
const res = await simpleGet(`/notes/${alicesPost.id}`, ONLY_AP);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.type, AP);
|
||||
}));
|
||||
|
||||
it("Prefer AP => AP", async(async () => {
|
||||
const res = await simpleGet(`/notes/${alicesPost.id}`, PREFER_AP);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.type, AP);
|
||||
}));
|
||||
|
||||
it("Prefer HTML => HTML", async(async () => {
|
||||
const res = await simpleGet(`/notes/${alicesPost.id}`, PREFER_HTML);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.type, HTML);
|
||||
}));
|
||||
|
||||
it("Unspecified => HTML", async(async () => {
|
||||
const res = await simpleGet(`/notes/${alicesPost.id}`, UNSPECIFIED);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.type, HTML);
|
||||
}));
|
||||
});
|
||||
|
||||
describe("Feeds", () => {
|
||||
it("RSS", async(async () => {
|
||||
const res = await simpleGet(`/@${alice.username}.rss`, UNSPECIFIED);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.type, "application/rss+xml; charset=utf-8");
|
||||
}));
|
||||
|
||||
it("ATOM", async(async () => {
|
||||
const res = await simpleGet(`/@${alice.username}.atom`, UNSPECIFIED);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.type, "application/atom+xml; charset=utf-8");
|
||||
}));
|
||||
|
||||
it("JSON", async(async () => {
|
||||
const res = await simpleGet(`/@${alice.username}.json`, UNSPECIFIED);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.type, "application/json; charset=utf-8");
|
||||
}));
|
||||
});
|
||||
});
|
|
@ -1,283 +0,0 @@
|
|||
process.env.NODE_ENV = "test";
|
||||
|
||||
import * as assert from "assert";
|
||||
import * as childProcess from "child_process";
|
||||
import {
|
||||
async,
|
||||
signup,
|
||||
request,
|
||||
post,
|
||||
react,
|
||||
connectStream,
|
||||
startServer,
|
||||
shutdownServer,
|
||||
simpleGet,
|
||||
} from "./utils.js";
|
||||
|
||||
describe("FF visibility", () => {
|
||||
let p: childProcess.ChildProcess;
|
||||
|
||||
let alice: any;
|
||||
let bob: any;
|
||||
let carol: any;
|
||||
|
||||
before(async () => {
|
||||
p = await startServer();
|
||||
alice = await signup({ username: "alice" });
|
||||
bob = await signup({ username: "bob" });
|
||||
carol = await signup({ username: "carol" });
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await shutdownServer(p);
|
||||
});
|
||||
|
||||
it("ffVisibility が public なユーザーのフォロー/フォロワーを誰でも見れる", async(async () => {
|
||||
await request(
|
||||
"/i/update",
|
||||
{
|
||||
ffVisibility: "public",
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
const followingRes = await request(
|
||||
"/users/following",
|
||||
{
|
||||
userId: alice.id,
|
||||
},
|
||||
bob,
|
||||
);
|
||||
const followersRes = await request(
|
||||
"/users/followers",
|
||||
{
|
||||
userId: alice.id,
|
||||
},
|
||||
bob,
|
||||
);
|
||||
|
||||
assert.strictEqual(followingRes.status, 200);
|
||||
assert.strictEqual(Array.isArray(followingRes.body), true);
|
||||
assert.strictEqual(followersRes.status, 200);
|
||||
assert.strictEqual(Array.isArray(followersRes.body), true);
|
||||
}));
|
||||
|
||||
it("ffVisibility が followers なユーザーのフォロー/フォロワーを自分で見れる", async(async () => {
|
||||
await request(
|
||||
"/i/update",
|
||||
{
|
||||
ffVisibility: "followers",
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
const followingRes = await request(
|
||||
"/users/following",
|
||||
{
|
||||
userId: alice.id,
|
||||
},
|
||||
alice,
|
||||
);
|
||||
const followersRes = await request(
|
||||
"/users/followers",
|
||||
{
|
||||
userId: alice.id,
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
assert.strictEqual(followingRes.status, 200);
|
||||
assert.strictEqual(Array.isArray(followingRes.body), true);
|
||||
assert.strictEqual(followersRes.status, 200);
|
||||
assert.strictEqual(Array.isArray(followersRes.body), true);
|
||||
}));
|
||||
|
||||
it("ffVisibility が followers なユーザーのフォロー/フォロワーを非フォロワーが見れない", async(async () => {
|
||||
await request(
|
||||
"/i/update",
|
||||
{
|
||||
ffVisibility: "followers",
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
const followingRes = await request(
|
||||
"/users/following",
|
||||
{
|
||||
userId: alice.id,
|
||||
},
|
||||
bob,
|
||||
);
|
||||
const followersRes = await request(
|
||||
"/users/followers",
|
||||
{
|
||||
userId: alice.id,
|
||||
},
|
||||
bob,
|
||||
);
|
||||
|
||||
assert.strictEqual(followingRes.status, 400);
|
||||
assert.strictEqual(followersRes.status, 400);
|
||||
}));
|
||||
|
||||
it("ffVisibility が followers なユーザーのフォロー/フォロワーをフォロワーが見れる", async(async () => {
|
||||
await request(
|
||||
"/i/update",
|
||||
{
|
||||
ffVisibility: "followers",
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
await request(
|
||||
"/following/create",
|
||||
{
|
||||
userId: alice.id,
|
||||
},
|
||||
bob,
|
||||
);
|
||||
|
||||
const followingRes = await request(
|
||||
"/users/following",
|
||||
{
|
||||
userId: alice.id,
|
||||
},
|
||||
bob,
|
||||
);
|
||||
const followersRes = await request(
|
||||
"/users/followers",
|
||||
{
|
||||
userId: alice.id,
|
||||
},
|
||||
bob,
|
||||
);
|
||||
|
||||
assert.strictEqual(followingRes.status, 200);
|
||||
assert.strictEqual(Array.isArray(followingRes.body), true);
|
||||
assert.strictEqual(followersRes.status, 200);
|
||||
assert.strictEqual(Array.isArray(followersRes.body), true);
|
||||
}));
|
||||
|
||||
it("ffVisibility が private なユーザーのフォロー/フォロワーを自分で見れる", async(async () => {
|
||||
await request(
|
||||
"/i/update",
|
||||
{
|
||||
ffVisibility: "private",
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
const followingRes = await request(
|
||||
"/users/following",
|
||||
{
|
||||
userId: alice.id,
|
||||
},
|
||||
alice,
|
||||
);
|
||||
const followersRes = await request(
|
||||
"/users/followers",
|
||||
{
|
||||
userId: alice.id,
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
assert.strictEqual(followingRes.status, 200);
|
||||
assert.strictEqual(Array.isArray(followingRes.body), true);
|
||||
assert.strictEqual(followersRes.status, 200);
|
||||
assert.strictEqual(Array.isArray(followersRes.body), true);
|
||||
}));
|
||||
|
||||
it("ffVisibility が private なユーザーのフォロー/フォロワーを他人が見れない", async(async () => {
|
||||
await request(
|
||||
"/i/update",
|
||||
{
|
||||
ffVisibility: "private",
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
const followingRes = await request(
|
||||
"/users/following",
|
||||
{
|
||||
userId: alice.id,
|
||||
},
|
||||
bob,
|
||||
);
|
||||
const followersRes = await request(
|
||||
"/users/followers",
|
||||
{
|
||||
userId: alice.id,
|
||||
},
|
||||
bob,
|
||||
);
|
||||
|
||||
assert.strictEqual(followingRes.status, 400);
|
||||
assert.strictEqual(followersRes.status, 400);
|
||||
}));
|
||||
|
||||
describe("AP", () => {
|
||||
it("ffVisibility が public 以外ならばAPからは取得できない", async(async () => {
|
||||
{
|
||||
await request(
|
||||
"/i/update",
|
||||
{
|
||||
ffVisibility: "public",
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
const followingRes = await simpleGet(
|
||||
`/users/${alice.id}/following`,
|
||||
"application/activity+json",
|
||||
);
|
||||
const followersRes = await simpleGet(
|
||||
`/users/${alice.id}/followers`,
|
||||
"application/activity+json",
|
||||
);
|
||||
assert.strictEqual(followingRes.status, 200);
|
||||
assert.strictEqual(followersRes.status, 200);
|
||||
}
|
||||
{
|
||||
await request(
|
||||
"/i/update",
|
||||
{
|
||||
ffVisibility: "followers",
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
const followingRes = await simpleGet(
|
||||
`/users/${alice.id}/following`,
|
||||
"application/activity+json",
|
||||
).catch((res) => ({ status: res.statusCode }));
|
||||
const followersRes = await simpleGet(
|
||||
`/users/${alice.id}/followers`,
|
||||
"application/activity+json",
|
||||
).catch((res) => ({ status: res.statusCode }));
|
||||
assert.strictEqual(followingRes.status, 403);
|
||||
assert.strictEqual(followersRes.status, 403);
|
||||
}
|
||||
{
|
||||
await request(
|
||||
"/i/update",
|
||||
{
|
||||
ffVisibility: "private",
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
const followingRes = await simpleGet(
|
||||
`/users/${alice.id}/following`,
|
||||
"application/activity+json",
|
||||
).catch((res) => ({ status: res.statusCode }));
|
||||
const followersRes = await simpleGet(
|
||||
`/users/${alice.id}/followers`,
|
||||
"application/activity+json",
|
||||
).catch((res) => ({ status: res.statusCode }));
|
||||
assert.strictEqual(followingRes.status, 403);
|
||||
assert.strictEqual(followersRes.status, 403);
|
||||
}
|
||||
}));
|
||||
});
|
||||
});
|
|
@ -1,209 +0,0 @@
|
|||
import * as assert from "assert";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname } from "node:path";
|
||||
import { getFileInfo } from "../src/misc/get-file-info.js";
|
||||
import { async } from "./utils.js";
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
|
||||
describe("Get file info", () => {
|
||||
it("Empty file", async(async () => {
|
||||
const path = `${_dirname}/resources/emptyfile`;
|
||||
const info = (await getFileInfo(path, {
|
||||
skipSensitiveDetection: true,
|
||||
})) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 0,
|
||||
md5: "d41d8cd98f00b204e9800998ecf8427e",
|
||||
type: {
|
||||
mime: "application/octet-stream",
|
||||
ext: null,
|
||||
},
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
orientation: undefined,
|
||||
});
|
||||
}));
|
||||
|
||||
it("Generic JPEG", async(async () => {
|
||||
const path = `${_dirname}/resources/Lenna.jpg`;
|
||||
const info = (await getFileInfo(path, {
|
||||
skipSensitiveDetection: true,
|
||||
})) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 25360,
|
||||
md5: "091b3f259662aa31e2ffef4519951168",
|
||||
type: {
|
||||
mime: "image/jpeg",
|
||||
ext: "jpg",
|
||||
},
|
||||
width: 512,
|
||||
height: 512,
|
||||
orientation: undefined,
|
||||
});
|
||||
}));
|
||||
|
||||
it("Generic APNG", async(async () => {
|
||||
const path = `${_dirname}/resources/anime.png`;
|
||||
const info = (await getFileInfo(path, {
|
||||
skipSensitiveDetection: true,
|
||||
})) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 1868,
|
||||
md5: "08189c607bea3b952704676bb3c979e0",
|
||||
type: {
|
||||
mime: "image/apng",
|
||||
ext: "apng",
|
||||
},
|
||||
width: 256,
|
||||
height: 256,
|
||||
orientation: undefined,
|
||||
});
|
||||
}));
|
||||
|
||||
it("Generic AGIF", async(async () => {
|
||||
const path = `${_dirname}/resources/anime.gif`;
|
||||
const info = (await getFileInfo(path, {
|
||||
skipSensitiveDetection: true,
|
||||
})) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 2248,
|
||||
md5: "32c47a11555675d9267aee1a86571e7e",
|
||||
type: {
|
||||
mime: "image/gif",
|
||||
ext: "gif",
|
||||
},
|
||||
width: 256,
|
||||
height: 256,
|
||||
orientation: undefined,
|
||||
});
|
||||
}));
|
||||
|
||||
it("PNG with alpha", async(async () => {
|
||||
const path = `${_dirname}/resources/with-alpha.png`;
|
||||
const info = (await getFileInfo(path, {
|
||||
skipSensitiveDetection: true,
|
||||
})) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 3772,
|
||||
md5: "f73535c3e1e27508885b69b10cf6e991",
|
||||
type: {
|
||||
mime: "image/png",
|
||||
ext: "png",
|
||||
},
|
||||
width: 256,
|
||||
height: 256,
|
||||
orientation: undefined,
|
||||
});
|
||||
}));
|
||||
|
||||
it("Generic SVG", async(async () => {
|
||||
const path = `${_dirname}/resources/image.svg`;
|
||||
const info = (await getFileInfo(path, {
|
||||
skipSensitiveDetection: true,
|
||||
})) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 505,
|
||||
md5: "b6f52b4b021e7b92cdd04509c7267965",
|
||||
type: {
|
||||
mime: "image/svg+xml",
|
||||
ext: "svg",
|
||||
},
|
||||
width: 256,
|
||||
height: 256,
|
||||
orientation: undefined,
|
||||
});
|
||||
}));
|
||||
|
||||
it("SVG with XML definition", async(async () => {
|
||||
// https://github.com/misskey-dev/misskey/issues/4413
|
||||
const path = `${_dirname}/resources/with-xml-def.svg`;
|
||||
const info = (await getFileInfo(path, {
|
||||
skipSensitiveDetection: true,
|
||||
})) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 544,
|
||||
md5: "4b7a346cde9ccbeb267e812567e33397",
|
||||
type: {
|
||||
mime: "image/svg+xml",
|
||||
ext: "svg",
|
||||
},
|
||||
width: 256,
|
||||
height: 256,
|
||||
orientation: undefined,
|
||||
});
|
||||
}));
|
||||
|
||||
it("Dimension limit", async(async () => {
|
||||
const path = `${_dirname}/resources/25000x25000.png`;
|
||||
const info = (await getFileInfo(path, {
|
||||
skipSensitiveDetection: true,
|
||||
})) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 75933,
|
||||
md5: "268c5dde99e17cf8fe09f1ab3f97df56",
|
||||
type: {
|
||||
mime: "application/octet-stream", // do not treat as image
|
||||
ext: null,
|
||||
},
|
||||
width: 25000,
|
||||
height: 25000,
|
||||
orientation: undefined,
|
||||
});
|
||||
}));
|
||||
|
||||
it("Rotate JPEG", async(async () => {
|
||||
const path = `${_dirname}/resources/rotate.jpg`;
|
||||
const info = (await getFileInfo(path, {
|
||||
skipSensitiveDetection: true,
|
||||
})) as any;
|
||||
delete info.warnings;
|
||||
delete info.blurhash;
|
||||
delete info.sensitive;
|
||||
delete info.porn;
|
||||
assert.deepStrictEqual(info, {
|
||||
size: 12624,
|
||||
md5: "68d5b2d8d1d1acbbce99203e3ec3857e",
|
||||
type: {
|
||||
mime: "image/jpeg",
|
||||
ext: "jpg",
|
||||
},
|
||||
width: 512,
|
||||
height: 256,
|
||||
orientation: 8,
|
||||
});
|
||||
}));
|
||||
});
|
|
@ -1,37 +0,0 @@
|
|||
/**
|
||||
* ts-node/esmローダーに投げる前にpath mappingを解決する
|
||||
* 参考
|
||||
* - https://github.com/TypeStrong/ts-node/discussions/1450#discussioncomment-1806115
|
||||
* - https://nodejs.org/api/esm.html#loaders
|
||||
* ※ https://github.com/TypeStrong/ts-node/pull/1585 が取り込まれたらこのカスタムローダーは必要なくなる
|
||||
*/
|
||||
|
||||
import { resolve as resolveTs, load } from "ts-node/esm";
|
||||
import { loadConfig, createMatchPath } from "tsconfig-paths";
|
||||
import { pathToFileURL } from "url";
|
||||
|
||||
const tsconfig = loadConfig();
|
||||
const matchPath = createMatchPath(tsconfig.absoluteBaseUrl, tsconfig.paths);
|
||||
|
||||
export function resolve(specifier, ctx, defaultResolve) {
|
||||
let resolvedSpecifier;
|
||||
if (specifier.endsWith(".js")) {
|
||||
// maybe transpiled
|
||||
const specifierWithoutExtension = specifier.substring(
|
||||
0,
|
||||
specifier.length - ".js".length,
|
||||
);
|
||||
const matchedSpecifier = matchPath(specifierWithoutExtension);
|
||||
if (matchedSpecifier) {
|
||||
resolvedSpecifier = pathToFileURL(`${matchedSpecifier}.js`).href;
|
||||
}
|
||||
} else {
|
||||
const matchedSpecifier = matchPath(specifier);
|
||||
if (matchedSpecifier) {
|
||||
resolvedSpecifier = pathToFileURL(matchedSpecifier).href;
|
||||
}
|
||||
}
|
||||
return resolveTs(resolvedSpecifier ?? specifier, ctx, defaultResolve);
|
||||
}
|
||||
|
||||
export { load };
|
|
@ -1,127 +0,0 @@
|
|||
import * as assert from "assert";
|
||||
import * as mfm from "mfm-js";
|
||||
|
||||
import { toHtml } from "../src/mfm/to-html.js";
|
||||
import { fromHtml } from "../src/mfm/from-html.js";
|
||||
|
||||
describe("toHtml", () => {
|
||||
it("br", () => {
|
||||
const input = "foo\nbar\nbaz";
|
||||
const output = "<p><span>foo<br>bar<br>baz</span></p>";
|
||||
assert.equal(toHtml(mfm.parse(input)), output);
|
||||
});
|
||||
|
||||
it("br alt", () => {
|
||||
const input = "foo\r\nbar\rbaz";
|
||||
const output = "<p><span>foo<br>bar<br>baz</span></p>";
|
||||
assert.equal(toHtml(mfm.parse(input)), output);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fromHtml", () => {
|
||||
it("p", () => {
|
||||
assert.deepStrictEqual(fromHtml("<p>a</p><p>b</p>"), "a\n\nb");
|
||||
});
|
||||
|
||||
it("block element", () => {
|
||||
assert.deepStrictEqual(fromHtml("<div>a</div><div>b</div>"), "a\nb");
|
||||
});
|
||||
|
||||
it("inline element", () => {
|
||||
assert.deepStrictEqual(fromHtml("<ul><li>a</li><li>b</li></ul>"), "a\nb");
|
||||
});
|
||||
|
||||
it("block code", () => {
|
||||
assert.deepStrictEqual(
|
||||
fromHtml("<pre><code>a\nb</code></pre>"),
|
||||
"```\na\nb\n```",
|
||||
);
|
||||
});
|
||||
|
||||
it("inline code", () => {
|
||||
assert.deepStrictEqual(fromHtml("<code>a</code>"), "`a`");
|
||||
});
|
||||
|
||||
it("quote", () => {
|
||||
assert.deepStrictEqual(
|
||||
fromHtml("<blockquote>a\nb</blockquote>"),
|
||||
"> a\n> b",
|
||||
);
|
||||
});
|
||||
|
||||
it("br", () => {
|
||||
assert.deepStrictEqual(fromHtml("<p>abc<br><br/>d</p>"), "abc\n\nd");
|
||||
});
|
||||
|
||||
it("link with different text", () => {
|
||||
assert.deepStrictEqual(
|
||||
fromHtml('<p>a <a href="https://joinfirefish.org/b">c</a> d</p>'),
|
||||
"a [c](https://joinfirefish.org/b) d",
|
||||
);
|
||||
});
|
||||
|
||||
it("link with different text, but not encoded", () => {
|
||||
assert.deepStrictEqual(
|
||||
fromHtml('<p>a <a href="https://joinfirefish.org/ä">c</a> d</p>'),
|
||||
"a [c](<https://joinfirefish.org/ä>) d",
|
||||
);
|
||||
});
|
||||
|
||||
it("link with same text", () => {
|
||||
assert.deepStrictEqual(
|
||||
fromHtml(
|
||||
'<p>a <a href="https://joinfirefish.org/b">https://joinfirefish.org/b</a> d</p>',
|
||||
),
|
||||
"a https://joinfirefish.org/b d",
|
||||
);
|
||||
});
|
||||
|
||||
it("link with same text, but not encoded", () => {
|
||||
assert.deepStrictEqual(
|
||||
fromHtml(
|
||||
'<p>a <a href="https://joinfirefish.org/ä">https://joinfirefish.org/ä</a> d</p>',
|
||||
),
|
||||
"a <https://joinfirefish.org/ä> d",
|
||||
);
|
||||
});
|
||||
|
||||
it("link with no url", () => {
|
||||
assert.deepStrictEqual(
|
||||
fromHtml('<p>a <a href="b">c</a> d</p>'),
|
||||
"a [c](b) d",
|
||||
);
|
||||
});
|
||||
|
||||
it("link without href", () => {
|
||||
assert.deepStrictEqual(fromHtml("<p>a <a>c</a> d</p>"), "a c d");
|
||||
});
|
||||
|
||||
it("link without text", () => {
|
||||
assert.deepStrictEqual(
|
||||
fromHtml('<p>a <a href="https://joinfirefish.org/b"></a> d</p>'),
|
||||
"a https://joinfirefish.org/b d",
|
||||
);
|
||||
});
|
||||
|
||||
it("link without both", () => {
|
||||
assert.deepStrictEqual(fromHtml("<p>a <a></a> d</p>"), "a d");
|
||||
});
|
||||
|
||||
it("mention", () => {
|
||||
assert.deepStrictEqual(
|
||||
fromHtml(
|
||||
'<p>a <a href="https://joinfirefish.org/@user" class="u-url mention">@user</a> d</p>',
|
||||
),
|
||||
"a @user@joinfirefish.org d",
|
||||
);
|
||||
});
|
||||
|
||||
it("hashtag", () => {
|
||||
assert.deepStrictEqual(
|
||||
fromHtml('<p>a <a href="https://joinfirefish.org/tags/a">#a</a> d</p>', [
|
||||
"#a",
|
||||
]),
|
||||
"a #a d",
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,39 +0,0 @@
|
|||
import Resolver from "../../src/remote/activitypub/resolver.js";
|
||||
import { IObject } from "../../src/remote/activitypub/type.js";
|
||||
|
||||
type MockResponse = {
|
||||
type: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
export class MockResolver extends Resolver {
|
||||
private _rs = new Map<string, MockResponse>();
|
||||
public async _register(
|
||||
uri: string,
|
||||
content: string | Record<string, any>,
|
||||
type = "application/activity+json",
|
||||
) {
|
||||
this._rs.set(uri, {
|
||||
type,
|
||||
content: typeof content === "string" ? content : JSON.stringify(content),
|
||||
});
|
||||
}
|
||||
|
||||
public async resolve(value: string | IObject): Promise<IObject> {
|
||||
if (typeof value !== "string") return value;
|
||||
|
||||
const r = this._rs.get(value);
|
||||
|
||||
if (!r) {
|
||||
throw {
|
||||
name: "StatusError",
|
||||
statusCode: 404,
|
||||
message: "Not registed for mock",
|
||||
};
|
||||
}
|
||||
|
||||
const object = JSON.parse(r.content);
|
||||
|
||||
return object;
|
||||
}
|
||||
}
|
|
@ -1,176 +0,0 @@
|
|||
process.env.NODE_ENV = "test";
|
||||
|
||||
import * as assert from "assert";
|
||||
import * as childProcess from "child_process";
|
||||
import {
|
||||
async,
|
||||
signup,
|
||||
request,
|
||||
post,
|
||||
react,
|
||||
startServer,
|
||||
shutdownServer,
|
||||
waitFire,
|
||||
} from "./utils.js";
|
||||
|
||||
describe("Mute", () => {
|
||||
let p: childProcess.ChildProcess;
|
||||
|
||||
// alice mutes carol
|
||||
let alice: any;
|
||||
let bob: any;
|
||||
let carol: any;
|
||||
|
||||
before(async () => {
|
||||
p = await startServer();
|
||||
alice = await signup({ username: "alice" });
|
||||
bob = await signup({ username: "bob" });
|
||||
carol = await signup({ username: "carol" });
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await shutdownServer(p);
|
||||
});
|
||||
|
||||
it("ミュート作成", async(async () => {
|
||||
const res = await request(
|
||||
"/mute/create",
|
||||
{
|
||||
userId: carol.id,
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
assert.strictEqual(res.status, 204);
|
||||
}));
|
||||
|
||||
it("「自分宛ての投稿」にミュートしているユーザーの投稿が含まれない", async(async () => {
|
||||
const bobNote = await post(bob, { text: "@alice hi" });
|
||||
const carolNote = await post(carol, { text: "@alice hi" });
|
||||
|
||||
const res = await request("/notes/mentions", {}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(
|
||||
res.body.some((note: any) => note.id === bobNote.id),
|
||||
true,
|
||||
);
|
||||
assert.strictEqual(
|
||||
res.body.some((note: any) => note.id === carolNote.id),
|
||||
false,
|
||||
);
|
||||
}));
|
||||
|
||||
it("ミュートしているユーザーからメンションされても、hasUnreadMentions が true にならない", async(async () => {
|
||||
// 状態リセット
|
||||
await request("/i/read-all-unread-notes", {}, alice);
|
||||
|
||||
await post(carol, { text: "@alice hi" });
|
||||
|
||||
const res = await request("/i", {}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.body.hasUnreadMentions, false);
|
||||
}));
|
||||
|
||||
it("ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない", async () => {
|
||||
// 状態リセット
|
||||
await request("/i/read-all-unread-notes", {}, alice);
|
||||
|
||||
const fired = await waitFire(
|
||||
alice,
|
||||
"main",
|
||||
() => post(carol, { text: "@alice hi" }),
|
||||
(msg) => msg.type === "unreadMention",
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, false);
|
||||
});
|
||||
|
||||
it("ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない", async () => {
|
||||
// 状態リセット
|
||||
await request("/i/read-all-unread-notes", {}, alice);
|
||||
await request("/notifications/mark-all-as-read", {}, alice);
|
||||
|
||||
const fired = await waitFire(
|
||||
alice,
|
||||
"main",
|
||||
() => post(carol, { text: "@alice hi" }),
|
||||
(msg) => msg.type === "unreadNotification",
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, false);
|
||||
});
|
||||
|
||||
describe("Timeline", () => {
|
||||
it("タイムラインにミュートしているユーザーの投稿が含まれない", async(async () => {
|
||||
const aliceNote = await post(alice);
|
||||
const bobNote = await post(bob);
|
||||
const carolNote = await post(carol);
|
||||
|
||||
const res = await request("/notes/local-timeline", {}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(
|
||||
res.body.some((note: any) => note.id === aliceNote.id),
|
||||
true,
|
||||
);
|
||||
assert.strictEqual(
|
||||
res.body.some((note: any) => note.id === bobNote.id),
|
||||
true,
|
||||
);
|
||||
assert.strictEqual(
|
||||
res.body.some((note: any) => note.id === carolNote.id),
|
||||
false,
|
||||
);
|
||||
}));
|
||||
|
||||
it("タイムラインにミュートしているユーザーの投稿のRenoteが含まれない", async(async () => {
|
||||
const aliceNote = await post(alice);
|
||||
const carolNote = await post(carol);
|
||||
const bobNote = await post(bob, {
|
||||
renoteId: carolNote.id,
|
||||
});
|
||||
|
||||
const res = await request("/notes/local-timeline", {}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(
|
||||
res.body.some((note: any) => note.id === aliceNote.id),
|
||||
true,
|
||||
);
|
||||
assert.strictEqual(
|
||||
res.body.some((note: any) => note.id === bobNote.id),
|
||||
false,
|
||||
);
|
||||
assert.strictEqual(
|
||||
res.body.some((note: any) => note.id === carolNote.id),
|
||||
false,
|
||||
);
|
||||
}));
|
||||
});
|
||||
|
||||
describe("Notification", () => {
|
||||
it("通知にミュートしているユーザーの通知が含まれない(リアクション)", async(async () => {
|
||||
const aliceNote = await post(alice);
|
||||
await react(bob, aliceNote, "like");
|
||||
await react(carol, aliceNote, "like");
|
||||
|
||||
const res = await request("/i/notifications", {}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(
|
||||
res.body.some((notification: any) => notification.userId === bob.id),
|
||||
true,
|
||||
);
|
||||
assert.strictEqual(
|
||||
res.body.some((notification: any) => notification.userId === carol.id),
|
||||
false,
|
||||
);
|
||||
}));
|
||||
});
|
||||
});
|
|
@ -1,517 +0,0 @@
|
|||
process.env.NODE_ENV = "test";
|
||||
|
||||
import * as assert from "assert";
|
||||
import * as childProcess from "child_process";
|
||||
import { Note } from "../src/models/entities/note.js";
|
||||
import {
|
||||
async,
|
||||
signup,
|
||||
request,
|
||||
post,
|
||||
uploadUrl,
|
||||
startServer,
|
||||
shutdownServer,
|
||||
initTestDb,
|
||||
api,
|
||||
} from "./utils.js";
|
||||
|
||||
describe("Note", () => {
|
||||
let p: childProcess.ChildProcess;
|
||||
let Notes: any;
|
||||
|
||||
let alice: any;
|
||||
let bob: any;
|
||||
|
||||
before(async () => {
|
||||
p = await startServer();
|
||||
const connection = await initTestDb(true);
|
||||
Notes = connection.getRepository(Note);
|
||||
alice = await signup({ username: "alice" });
|
||||
bob = await signup({ username: "bob" });
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await shutdownServer(p);
|
||||
});
|
||||
|
||||
it("投稿できる", async(async () => {
|
||||
const post = {
|
||||
text: "test",
|
||||
};
|
||||
|
||||
const res = await request("/notes/create", post, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(
|
||||
typeof res.body === "object" && !Array.isArray(res.body),
|
||||
true,
|
||||
);
|
||||
assert.strictEqual(res.body.createdNote.text, post.text);
|
||||
}));
|
||||
|
||||
it("ファイルを添付できる", async(async () => {
|
||||
const file = await uploadUrl(
|
||||
alice,
|
||||
"https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg",
|
||||
);
|
||||
|
||||
const res = await request(
|
||||
"/notes/create",
|
||||
{
|
||||
fileIds: [file.id],
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(
|
||||
typeof res.body === "object" && !Array.isArray(res.body),
|
||||
true,
|
||||
);
|
||||
assert.deepStrictEqual(res.body.createdNote.fileIds, [file.id]);
|
||||
}));
|
||||
|
||||
it("他人のファイルは無視", async(async () => {
|
||||
const file = await uploadUrl(
|
||||
bob,
|
||||
"https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg",
|
||||
);
|
||||
|
||||
const res = await request(
|
||||
"/notes/create",
|
||||
{
|
||||
text: "test",
|
||||
fileIds: [file.id],
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(
|
||||
typeof res.body === "object" && !Array.isArray(res.body),
|
||||
true,
|
||||
);
|
||||
assert.deepStrictEqual(res.body.createdNote.fileIds, []);
|
||||
}));
|
||||
|
||||
it("存在しないファイルは無視", async(async () => {
|
||||
const res = await request(
|
||||
"/notes/create",
|
||||
{
|
||||
text: "test",
|
||||
fileIds: ["000000000000000000000000"],
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(
|
||||
typeof res.body === "object" && !Array.isArray(res.body),
|
||||
true,
|
||||
);
|
||||
assert.deepStrictEqual(res.body.createdNote.fileIds, []);
|
||||
}));
|
||||
|
||||
it("不正なファイルIDは無視", async(async () => {
|
||||
const res = await request(
|
||||
"/notes/create",
|
||||
{
|
||||
fileIds: ["kyoppie"],
|
||||
},
|
||||
alice,
|
||||
);
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(
|
||||
typeof res.body === "object" && !Array.isArray(res.body),
|
||||
true,
|
||||
);
|
||||
assert.deepStrictEqual(res.body.createdNote.fileIds, []);
|
||||
}));
|
||||
|
||||
it("返信できる", async(async () => {
|
||||
const bobPost = await post(bob, {
|
||||
text: "foo",
|
||||
});
|
||||
|
||||
const alicePost = {
|
||||
text: "bar",
|
||||
replyId: bobPost.id,
|
||||
};
|
||||
|
||||
const res = await request("/notes/create", alicePost, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(
|
||||
typeof res.body === "object" && !Array.isArray(res.body),
|
||||
true,
|
||||
);
|
||||
assert.strictEqual(res.body.createdNote.text, alicePost.text);
|
||||
assert.strictEqual(res.body.createdNote.replyId, alicePost.replyId);
|
||||
assert.strictEqual(res.body.createdNote.reply.text, bobPost.text);
|
||||
}));
|
||||
|
||||
it("renoteできる", async(async () => {
|
||||
const bobPost = await post(bob, {
|
||||
text: "test",
|
||||
});
|
||||
|
||||
const alicePost = {
|
||||
renoteId: bobPost.id,
|
||||
};
|
||||
|
||||
const res = await request("/notes/create", alicePost, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(
|
||||
typeof res.body === "object" && !Array.isArray(res.body),
|
||||
true,
|
||||
);
|
||||
assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId);
|
||||
assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
|
||||
}));
|
||||
|
||||
it("引用renoteできる", async(async () => {
|
||||
const bobPost = await post(bob, {
|
||||
text: "test",
|
||||
});
|
||||
|
||||
const alicePost = {
|
||||
text: "test",
|
||||
renoteId: bobPost.id,
|
||||
};
|
||||
|
||||
const res = await request("/notes/create", alicePost, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(
|
||||
typeof res.body === "object" && !Array.isArray(res.body),
|
||||
true,
|
||||
);
|
||||
assert.strictEqual(res.body.createdNote.text, alicePost.text);
|
||||
assert.strictEqual(res.body.createdNote.renoteId, alicePost.renoteId);
|
||||
assert.strictEqual(res.body.createdNote.renote.text, bobPost.text);
|
||||
}));
|
||||
|
||||
it("文字数ぎりぎりで怒られない", async(async () => {
|
||||
const post = {
|
||||
text: "!".repeat(3000),
|
||||
};
|
||||
const res = await request("/notes/create", post, alice);
|
||||
assert.strictEqual(res.status, 200);
|
||||
}));
|
||||
|
||||
it("文字数オーバーで怒られる", async(async () => {
|
||||
const post = {
|
||||
text: "!".repeat(3001),
|
||||
};
|
||||
const res = await request("/notes/create", post, alice);
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it("存在しないリプライ先で怒られる", async(async () => {
|
||||
const post = {
|
||||
text: "test",
|
||||
replyId: "000000000000000000000000",
|
||||
};
|
||||
const res = await request("/notes/create", post, alice);
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it("存在しないrenote対象で怒られる", async(async () => {
|
||||
const post = {
|
||||
renoteId: "000000000000000000000000",
|
||||
};
|
||||
const res = await request("/notes/create", post, alice);
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it("不正なリプライ先IDで怒られる", async(async () => {
|
||||
const post = {
|
||||
text: "test",
|
||||
replyId: "foo",
|
||||
};
|
||||
const res = await request("/notes/create", post, alice);
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it("不正なrenote対象IDで怒られる", async(async () => {
|
||||
const post = {
|
||||
renoteId: "foo",
|
||||
};
|
||||
const res = await request("/notes/create", post, alice);
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it("存在しないユーザーにメンションできる", async(async () => {
|
||||
const post = {
|
||||
text: "@ghost yo",
|
||||
};
|
||||
|
||||
const res = await request("/notes/create", post, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(
|
||||
typeof res.body === "object" && !Array.isArray(res.body),
|
||||
true,
|
||||
);
|
||||
assert.strictEqual(res.body.createdNote.text, post.text);
|
||||
}));
|
||||
|
||||
it("同じユーザーに複数メンションしても内部的にまとめられる", async(async () => {
|
||||
const post = {
|
||||
text: "@bob @bob @bob yo",
|
||||
};
|
||||
|
||||
const res = await request("/notes/create", post, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(
|
||||
typeof res.body === "object" && !Array.isArray(res.body),
|
||||
true,
|
||||
);
|
||||
assert.strictEqual(res.body.createdNote.text, post.text);
|
||||
|
||||
const noteDoc = await Notes.findOneBy({ id: res.body.createdNote.id });
|
||||
assert.deepStrictEqual(noteDoc.mentions, [bob.id]);
|
||||
}));
|
||||
|
||||
describe("notes/create", () => {
|
||||
it("投票を添付できる", async(async () => {
|
||||
const res = await request(
|
||||
"/notes/create",
|
||||
{
|
||||
text: "test",
|
||||
poll: {
|
||||
choices: ["foo", "bar"],
|
||||
},
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(
|
||||
typeof res.body === "object" && !Array.isArray(res.body),
|
||||
true,
|
||||
);
|
||||
assert.strictEqual(res.body.createdNote.poll != null, true);
|
||||
}));
|
||||
|
||||
it("投票の選択肢が無くて怒られる", async(async () => {
|
||||
const res = await request(
|
||||
"/notes/create",
|
||||
{
|
||||
poll: {},
|
||||
},
|
||||
alice,
|
||||
);
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it("投票の選択肢が無くて怒られる (空の配列)", async(async () => {
|
||||
const res = await request(
|
||||
"/notes/create",
|
||||
{
|
||||
poll: {
|
||||
choices: [],
|
||||
},
|
||||
},
|
||||
alice,
|
||||
);
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it("投票の選択肢が1つで怒られる", async(async () => {
|
||||
const res = await request(
|
||||
"/notes/create",
|
||||
{
|
||||
poll: {
|
||||
choices: ["Strawberry Pasta"],
|
||||
},
|
||||
},
|
||||
alice,
|
||||
);
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it("投票できる", async(async () => {
|
||||
const { body } = await request(
|
||||
"/notes/create",
|
||||
{
|
||||
text: "test",
|
||||
poll: {
|
||||
choices: ["sakura", "izumi", "ako"],
|
||||
},
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
const res = await request(
|
||||
"/notes/polls/vote",
|
||||
{
|
||||
noteId: body.createdNote.id,
|
||||
choice: 1,
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
assert.strictEqual(res.status, 204);
|
||||
}));
|
||||
|
||||
it("複数投票できない", async(async () => {
|
||||
const { body } = await request(
|
||||
"/notes/create",
|
||||
{
|
||||
text: "test",
|
||||
poll: {
|
||||
choices: ["sakura", "izumi", "ako"],
|
||||
},
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
await request(
|
||||
"/notes/polls/vote",
|
||||
{
|
||||
noteId: body.createdNote.id,
|
||||
choice: 0,
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
const res = await request(
|
||||
"/notes/polls/vote",
|
||||
{
|
||||
noteId: body.createdNote.id,
|
||||
choice: 2,
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
|
||||
it("許可されている場合は複数投票できる", async(async () => {
|
||||
const { body } = await request(
|
||||
"/notes/create",
|
||||
{
|
||||
text: "test",
|
||||
poll: {
|
||||
choices: ["sakura", "izumi", "ako"],
|
||||
multiple: true,
|
||||
},
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
await request(
|
||||
"/notes/polls/vote",
|
||||
{
|
||||
noteId: body.createdNote.id,
|
||||
choice: 0,
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
await request(
|
||||
"/notes/polls/vote",
|
||||
{
|
||||
noteId: body.createdNote.id,
|
||||
choice: 1,
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
const res = await request(
|
||||
"/notes/polls/vote",
|
||||
{
|
||||
noteId: body.createdNote.id,
|
||||
choice: 2,
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
assert.strictEqual(res.status, 204);
|
||||
}));
|
||||
|
||||
it("締め切られている場合は投票できない", async(async () => {
|
||||
const { body } = await request(
|
||||
"/notes/create",
|
||||
{
|
||||
text: "test",
|
||||
poll: {
|
||||
choices: ["sakura", "izumi", "ako"],
|
||||
expiredAfter: 1,
|
||||
},
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
await new Promise((x) => setTimeout(x, 2));
|
||||
|
||||
const res = await request(
|
||||
"/notes/polls/vote",
|
||||
{
|
||||
noteId: body.createdNote.id,
|
||||
choice: 1,
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
assert.strictEqual(res.status, 400);
|
||||
}));
|
||||
});
|
||||
|
||||
describe("notes/delete", () => {
|
||||
it("delete a reply", async(async () => {
|
||||
const mainNoteRes = await api(
|
||||
"notes/create",
|
||||
{
|
||||
text: "main post",
|
||||
},
|
||||
alice,
|
||||
);
|
||||
const replyOneRes = await api(
|
||||
"notes/create",
|
||||
{
|
||||
text: "reply one",
|
||||
replyId: mainNoteRes.body.createdNote.id,
|
||||
},
|
||||
alice,
|
||||
);
|
||||
const replyTwoRes = await api(
|
||||
"notes/create",
|
||||
{
|
||||
text: "reply two",
|
||||
replyId: mainNoteRes.body.createdNote.id,
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
const deleteOneRes = await api(
|
||||
"notes/delete",
|
||||
{
|
||||
noteId: replyOneRes.body.createdNote.id,
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
assert.strictEqual(deleteOneRes.status, 204);
|
||||
let mainNote = await Notes.findOneBy({
|
||||
id: mainNoteRes.body.createdNote.id,
|
||||
});
|
||||
assert.strictEqual(mainNote.repliesCount, 1);
|
||||
|
||||
const deleteTwoRes = await api(
|
||||
"notes/delete",
|
||||
{
|
||||
noteId: replyTwoRes.body.createdNote.id,
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
assert.strictEqual(deleteTwoRes.status, 204);
|
||||
mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id });
|
||||
assert.strictEqual(mainNote.repliesCount, 0);
|
||||
}));
|
||||
});
|
||||
});
|
|
@ -1,18 +0,0 @@
|
|||
import * as assert from "assert";
|
||||
import { just, nothing } from "../../src/prelude/maybe.js";
|
||||
|
||||
describe("just", () => {
|
||||
it("has a value", () => {
|
||||
assert.deepStrictEqual(just(3).isJust(), true);
|
||||
});
|
||||
|
||||
it("has the inverse called get", () => {
|
||||
assert.deepStrictEqual(just(3).get(), 3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("nothing", () => {
|
||||
it("has no value", () => {
|
||||
assert.deepStrictEqual(nothing().isJust(), false);
|
||||
});
|
||||
});
|
|
@ -1,13 +0,0 @@
|
|||
import * as assert from "assert";
|
||||
import { query } from "../../src/prelude/url.js";
|
||||
|
||||
describe("url", () => {
|
||||
it("query", () => {
|
||||
const s = query({
|
||||
foo: "ふぅ",
|
||||
bar: "b a r",
|
||||
baz: undefined,
|
||||
});
|
||||
assert.deepStrictEqual(s, "foo=%E3%81%B5%E3%81%85&bar=b%20a%20r");
|
||||
});
|
||||
});
|
|
@ -1,83 +0,0 @@
|
|||
/*
|
||||
import * as assert from 'assert';
|
||||
|
||||
import { toDbReaction } from '../src/misc/reaction-lib.js';
|
||||
|
||||
describe('toDbReaction', async () => {
|
||||
it('既存の文字列リアクションはそのまま', async () => {
|
||||
assert.strictEqual(await toDbReaction('like'), 'like');
|
||||
});
|
||||
|
||||
it('Unicodeプリンは寿司化不能とするため文字列化しない', async () => {
|
||||
assert.strictEqual(await toDbReaction('🍮'), '🍮');
|
||||
});
|
||||
|
||||
it('プリン以外の既存のリアクションは文字列化する like', async () => {
|
||||
assert.strictEqual(await toDbReaction('👍'), 'like');
|
||||
});
|
||||
|
||||
it('プリン以外の既存のリアクションは文字列化する love', async () => {
|
||||
assert.strictEqual(await toDbReaction('❤️'), 'love');
|
||||
});
|
||||
|
||||
it('プリン以外の既存のリアクションは文字列化する love 異体字セレクタなし', async () => {
|
||||
assert.strictEqual(await toDbReaction('❤'), 'love');
|
||||
});
|
||||
|
||||
it('プリン以外の既存のリアクションは文字列化する laugh', async () => {
|
||||
assert.strictEqual(await toDbReaction('😆'), 'laugh');
|
||||
});
|
||||
|
||||
it('プリン以外の既存のリアクションは文字列化する hmm', async () => {
|
||||
assert.strictEqual(await toDbReaction('🤔'), 'hmm');
|
||||
});
|
||||
|
||||
it('プリン以外の既存のリアクションは文字列化する surprise', async () => {
|
||||
assert.strictEqual(await toDbReaction('😮'), 'surprise');
|
||||
});
|
||||
|
||||
it('プリン以外の既存のリアクションは文字列化する congrats', async () => {
|
||||
assert.strictEqual(await toDbReaction('🎉'), 'congrats');
|
||||
});
|
||||
|
||||
it('プリン以外の既存のリアクションは文字列化する angry', async () => {
|
||||
assert.strictEqual(await toDbReaction('💢'), 'angry');
|
||||
});
|
||||
|
||||
it('プリン以外の既存のリアクションは文字列化する confused', async () => {
|
||||
assert.strictEqual(await toDbReaction('😥'), 'confused');
|
||||
});
|
||||
|
||||
it('プリン以外の既存のリアクションは文字列化する rip', async () => {
|
||||
assert.strictEqual(await toDbReaction('😇'), 'rip');
|
||||
});
|
||||
|
||||
it('それ以外はUnicodeのまま', async () => {
|
||||
assert.strictEqual(await toDbReaction('🍅'), '🍅');
|
||||
});
|
||||
|
||||
it('異体字セレクタ除去', async () => {
|
||||
assert.strictEqual(await toDbReaction('㊗️'), '㊗');
|
||||
});
|
||||
|
||||
it('異体字セレクタ除去 必要なし', async () => {
|
||||
assert.strictEqual(await toDbReaction('㊗'), '㊗');
|
||||
});
|
||||
|
||||
it('fallback - undefined', async () => {
|
||||
assert.strictEqual(await toDbReaction(undefined), 'like');
|
||||
});
|
||||
|
||||
it('fallback - null', async () => {
|
||||
assert.strictEqual(await toDbReaction(null), 'like');
|
||||
});
|
||||
|
||||
it('fallback - empty', async () => {
|
||||
assert.strictEqual(await toDbReaction(''), 'like');
|
||||
});
|
||||
|
||||
it('fallback - unknown', async () => {
|
||||
assert.strictEqual(await toDbReaction('unknown'), 'like');
|
||||
});
|
||||
});
|
||||
*/
|
Before Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 463 KiB |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 1.8 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="#FF40A4" d="M128 80c-16 4-20 24-20 48v16c0 8-8 16-20.3 8 4.3 20 24.3 28 40.3 24s20-24 20-48v-16c0-8 8-16 20.3-8C164 84 144 76 128 80"/><path fill="#FFBF40" d="M192 80c-16 4-20 24-20 48v16c0 8-8 16-20.3 8 4.3 20 24.3 28 40.3 24s20-24 20-48v-16c0-8 8-16 20.3-8C228 84 208 76 192 80"/><path fill="#408EFF" d="M64 80c-16 4-20 24-20 48v16c0 8-8 16-20.3 8C28 172 48 180 64 176s20-24 20-48v-16c0-8 8-16 20.3-8C100 84 80 76 64 80"/></svg>
|
Before Width: | Height: | Size: 504 B |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 3.7 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><path fill="#FF40A4" d="M128 80c-16 4-20 24-20 48v16c0 8-8 16-20.3 8 4.3 20 24.3 28 40.3 24s20-24 20-48v-16c0-8 8-16 20.3-8C164 84 144 76 128 80"/><path fill="#FFBF40" d="M192 80c-16 4-20 24-20 48v16c0 8-8 16-20.3 8 4.3 20 24.3 28 40.3 24s20-24 20-48v-16c0-8 8-16 20.3-8C228 84 208 76 192 80"/><path fill="#408EFF" d="M64 80c-16 4-20 24-20 48v16c0 8-8 16-20.3 8C28 172 48 180 64 176s20-24 20-48v-16c0-8 8-16 20.3-8C100 84 80 76 64 80"/></svg>
|
Before Width: | Height: | Size: 504 B |
|
@ -1,766 +0,0 @@
|
|||
process.env.NODE_ENV = "test";
|
||||
|
||||
import * as assert from "assert";
|
||||
import * as childProcess from "child_process";
|
||||
import { Following } from "../src/models/entities/following.js";
|
||||
import {
|
||||
connectStream,
|
||||
signup,
|
||||
api,
|
||||
post,
|
||||
startServer,
|
||||
shutdownServer,
|
||||
initTestDb,
|
||||
waitFire,
|
||||
} from "./utils.js";
|
||||
|
||||
describe("Streaming", () => {
|
||||
let p: childProcess.ChildProcess;
|
||||
let Followings: any;
|
||||
|
||||
const follow = async (follower: any, followee: any) => {
|
||||
await Followings.save({
|
||||
id: "a",
|
||||
createdAt: new Date(),
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
followerHost: follower.host,
|
||||
followerInbox: null,
|
||||
followerSharedInbox: null,
|
||||
followeeHost: followee.host,
|
||||
followeeInbox: null,
|
||||
followeeSharedInbox: null,
|
||||
});
|
||||
};
|
||||
|
||||
describe("Streaming", () => {
|
||||
// Local users
|
||||
let ayano: any;
|
||||
let kyoko: any;
|
||||
let chitose: any;
|
||||
|
||||
// Remote users
|
||||
let akari: any;
|
||||
let chinatsu: any;
|
||||
|
||||
let kyokoNote: any;
|
||||
let list: any;
|
||||
|
||||
before(async () => {
|
||||
p = await startServer();
|
||||
const connection = await initTestDb(true);
|
||||
Followings = connection.getRepository(Following);
|
||||
|
||||
ayano = await signup({ username: "ayano" });
|
||||
kyoko = await signup({ username: "kyoko" });
|
||||
chitose = await signup({ username: "chitose" });
|
||||
|
||||
akari = await signup({ username: "akari", host: "example.com" });
|
||||
chinatsu = await signup({ username: "chinatsu", host: "example.com" });
|
||||
|
||||
kyokoNote = await post(kyoko, { text: "foo" });
|
||||
|
||||
// Follow: ayano => kyoko
|
||||
await api("following/create", { userId: kyoko.id }, ayano);
|
||||
|
||||
// Follow: ayano => akari
|
||||
await follow(ayano, akari);
|
||||
|
||||
// List: chitose => ayano, kyoko
|
||||
list = await api(
|
||||
"users/lists/create",
|
||||
{
|
||||
name: "my list",
|
||||
},
|
||||
chitose,
|
||||
).then((x) => x.body);
|
||||
|
||||
await api(
|
||||
"users/lists/push",
|
||||
{
|
||||
listId: list.id,
|
||||
userId: ayano.id,
|
||||
},
|
||||
chitose,
|
||||
);
|
||||
|
||||
await api(
|
||||
"users/lists/push",
|
||||
{
|
||||
listId: list.id,
|
||||
userId: kyoko.id,
|
||||
},
|
||||
chitose,
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await shutdownServer(p);
|
||||
});
|
||||
|
||||
describe("Events", () => {
|
||||
it("mention event", async () => {
|
||||
const fired = await waitFire(
|
||||
kyoko,
|
||||
"main", // kyoko:main
|
||||
() => post(ayano, { text: "foo @kyoko bar" }), // ayano mention => kyoko
|
||||
(msg) => msg.type === "mention" && msg.body.userId === ayano.id, // wait ayano
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
it("renote event", async () => {
|
||||
const fired = await waitFire(
|
||||
kyoko,
|
||||
"main", // kyoko:main
|
||||
() => post(ayano, { renoteId: kyokoNote.id }), // ayano renote
|
||||
(msg) => msg.type === "renote" && msg.body.renoteId === kyokoNote.id, // wait renote
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Home Timeline", () => {
|
||||
it("自分の投稿が流れる", async () => {
|
||||
const fired = await waitFire(
|
||||
ayano,
|
||||
"homeTimeline", // ayano:Home
|
||||
() => api("notes/create", { text: "foo" }, ayano), // ayano posts
|
||||
(msg) => msg.type === "note" && msg.body.text === "foo",
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
it("フォローしているユーザーの投稿が流れる", async () => {
|
||||
const fired = await waitFire(
|
||||
ayano,
|
||||
"homeTimeline", // ayano:home
|
||||
() => api("notes/create", { text: "foo" }, kyoko), // kyoko posts
|
||||
(msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
it("フォローしていないユーザーの投稿は流れない", async () => {
|
||||
const fired = await waitFire(
|
||||
kyoko,
|
||||
"homeTimeline", // kyoko:home
|
||||
() => api("notes/create", { text: "foo" }, ayano), // ayano posts
|
||||
(msg) => msg.type === "note" && msg.body.userId === ayano.id, // wait ayano
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, false);
|
||||
});
|
||||
|
||||
it("フォローしているユーザーのダイレクト投稿が流れる", async () => {
|
||||
const fired = await waitFire(
|
||||
ayano,
|
||||
"homeTimeline", // ayano:home
|
||||
() =>
|
||||
api(
|
||||
"notes/create",
|
||||
{
|
||||
text: "foo",
|
||||
visibility: "specified",
|
||||
visibleUserIds: [ayano.id],
|
||||
},
|
||||
kyoko,
|
||||
), // kyoko dm => ayano
|
||||
(msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
it("フォローしているユーザーでも自分が指定されていないダイレクト投稿は流れない", async () => {
|
||||
const fired = await waitFire(
|
||||
ayano,
|
||||
"homeTimeline", // ayano:home
|
||||
() =>
|
||||
api(
|
||||
"notes/create",
|
||||
{
|
||||
text: "foo",
|
||||
visibility: "specified",
|
||||
visibleUserIds: [chitose.id],
|
||||
},
|
||||
kyoko,
|
||||
), // kyoko dm => chitose
|
||||
(msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, false);
|
||||
});
|
||||
}); // Home
|
||||
|
||||
describe("Local Timeline", () => {
|
||||
it("自分の投稿が流れる", async () => {
|
||||
const fired = await waitFire(
|
||||
ayano,
|
||||
"localTimeline", // ayano:Local
|
||||
() => api("notes/create", { text: "foo" }, ayano), // ayano posts
|
||||
(msg) => msg.type === "note" && msg.body.text === "foo",
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
it("フォローしていないローカルユーザーの投稿が流れる", async () => {
|
||||
const fired = await waitFire(
|
||||
ayano,
|
||||
"localTimeline", // ayano:Local
|
||||
() => api("notes/create", { text: "foo" }, chitose), // chitose posts
|
||||
(msg) => msg.type === "note" && msg.body.userId === chitose.id, // wait chitose
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
it("リモートユーザーの投稿は流れない", async () => {
|
||||
const fired = await waitFire(
|
||||
ayano,
|
||||
"localTimeline", // ayano:Local
|
||||
() => api("notes/create", { text: "foo" }, chinatsu), // chinatsu posts
|
||||
(msg) => msg.type === "note" && msg.body.userId === chinatsu.id, // wait chinatsu
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, false);
|
||||
});
|
||||
|
||||
it("フォローしてたとしてもリモートユーザーの投稿は流れない", async () => {
|
||||
const fired = await waitFire(
|
||||
ayano,
|
||||
"localTimeline", // ayano:Local
|
||||
() => api("notes/create", { text: "foo" }, akari), // akari posts
|
||||
(msg) => msg.type === "note" && msg.body.userId === akari.id, // wait akari
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, false);
|
||||
});
|
||||
|
||||
it("ホーム指定の投稿は流れない", async () => {
|
||||
const fired = await waitFire(
|
||||
ayano,
|
||||
"localTimeline", // ayano:Local
|
||||
() => api("notes/create", { text: "foo", visibility: "home" }, kyoko), // kyoko home posts
|
||||
(msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, false);
|
||||
});
|
||||
|
||||
it("フォローしているローカルユーザーのダイレクト投稿は流れない", async () => {
|
||||
const fired = await waitFire(
|
||||
ayano,
|
||||
"localTimeline", // ayano:Local
|
||||
() =>
|
||||
api(
|
||||
"notes/create",
|
||||
{
|
||||
text: "foo",
|
||||
visibility: "specified",
|
||||
visibleUserIds: [ayano.id],
|
||||
},
|
||||
kyoko,
|
||||
), // kyoko DM => ayano
|
||||
(msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, false);
|
||||
});
|
||||
|
||||
it("フォローしていないローカルユーザーのフォロワー宛て投稿は流れない", async () => {
|
||||
const fired = await waitFire(
|
||||
ayano,
|
||||
"localTimeline", // ayano:Local
|
||||
() =>
|
||||
api(
|
||||
"notes/create",
|
||||
{ text: "foo", visibility: "followers" },
|
||||
chitose,
|
||||
),
|
||||
(msg) => msg.type === "note" && msg.body.userId === chitose.id, // wait chitose
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Recommended Timeline", () => {
|
||||
it("自分の投稿が流れる", async () => {
|
||||
const fired = await waitFire(
|
||||
ayano,
|
||||
"recommendedTimeline", // ayano:Local
|
||||
() => api("notes/create", { text: "foo" }, ayano), // ayano posts
|
||||
(msg) => msg.type === "note" && msg.body.text === "foo",
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
it("フォローしていないローカルユーザーの投稿が流れる", async () => {
|
||||
const fired = await waitFire(
|
||||
ayano,
|
||||
"recommendedTimeline", // ayano:Local
|
||||
() => api("notes/create", { text: "foo" }, chitose), // chitose posts
|
||||
(msg) => msg.type === "note" && msg.body.userId === chitose.id, // wait chitose
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
it("リモートユーザーの投稿は流れない", async () => {
|
||||
const fired = await waitFire(
|
||||
ayano,
|
||||
"recommendedTimeline", // ayano:Local
|
||||
() => api("notes/create", { text: "foo" }, chinatsu), // chinatsu posts
|
||||
(msg) => msg.type === "note" && msg.body.userId === chinatsu.id, // wait chinatsu
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, false);
|
||||
});
|
||||
|
||||
it("フォローしてたとしてもリモートユーザーの投稿は流れない", async () => {
|
||||
const fired = await waitFire(
|
||||
ayano,
|
||||
"recommendedTimeline", // ayano:Local
|
||||
() => api("notes/create", { text: "foo" }, akari), // akari posts
|
||||
(msg) => msg.type === "note" && msg.body.userId === akari.id, // wait akari
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, false);
|
||||
});
|
||||
|
||||
it("ホーム指定の投稿は流れない", async () => {
|
||||
const fired = await waitFire(
|
||||
ayano,
|
||||
"recommendedTimeline", // ayano:Local
|
||||
() => api("notes/create", { text: "foo", visibility: "home" }, kyoko), // kyoko home posts
|
||||
(msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, false);
|
||||
});
|
||||
|
||||
it("フォローしているローカルユーザーのダイレクト投稿は流れない", async () => {
|
||||
const fired = await waitFire(
|
||||
ayano,
|
||||
"recommendedTimeline", // ayano:Local
|
||||
() =>
|
||||
api(
|
||||
"notes/create",
|
||||
{
|
||||
text: "foo",
|
||||
visibility: "specified",
|
||||
visibleUserIds: [ayano.id],
|
||||
},
|
||||
kyoko,
|
||||
), // kyoko DM => ayano
|
||||
(msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, false);
|
||||
});
|
||||
|
||||
it("フォローしていないローカルユーザーのフォロワー宛て投稿は流れない", async () => {
|
||||
const fired = await waitFire(
|
||||
ayano,
|
||||
"recommendedTimeline", // ayano:Local
|
||||
() =>
|
||||
api(
|
||||
"notes/create",
|
||||
{ text: "foo", visibility: "followers" },
|
||||
chitose,
|
||||
),
|
||||
(msg) => msg.type === "note" && msg.body.userId === chitose.id, // wait chitose
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Hybrid Timeline", () => {
|
||||
it("自分の投稿が流れる", async () => {
|
||||
const fired = await waitFire(
|
||||
ayano,
|
||||
"hybridTimeline", // ayano:Hybrid
|
||||
() => api("notes/create", { text: "foo" }, ayano), // ayano posts
|
||||
(msg) => msg.type === "note" && msg.body.text === "foo",
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
it("フォローしていないローカルユーザーの投稿が流れる", async () => {
|
||||
const fired = await waitFire(
|
||||
ayano,
|
||||
"hybridTimeline", // ayano:Hybrid
|
||||
() => api("notes/create", { text: "foo" }, chitose), // chitose posts
|
||||
(msg) => msg.type === "note" && msg.body.userId === chitose.id, // wait chitose
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
it("フォローしているリモートユーザーの投稿が流れる", async () => {
|
||||
const fired = await waitFire(
|
||||
ayano,
|
||||
"hybridTimeline", // ayano:Hybrid
|
||||
() => api("notes/create", { text: "foo" }, akari), // akari posts
|
||||
(msg) => msg.type === "note" && msg.body.userId === akari.id, // wait akari
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
it("フォローしていないリモートユーザーの投稿は流れない", async () => {
|
||||
const fired = await waitFire(
|
||||
ayano,
|
||||
"hybridTimeline", // ayano:Hybrid
|
||||
() => api("notes/create", { text: "foo" }, chinatsu), // chinatsu posts
|
||||
(msg) => msg.type === "note" && msg.body.userId === chinatsu.id, // wait chinatsu
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, false);
|
||||
});
|
||||
|
||||
it("フォローしているユーザーのダイレクト投稿が流れる", async () => {
|
||||
const fired = await waitFire(
|
||||
ayano,
|
||||
"hybridTimeline", // ayano:Hybrid
|
||||
() =>
|
||||
api(
|
||||
"notes/create",
|
||||
{
|
||||
text: "foo",
|
||||
visibility: "specified",
|
||||
visibleUserIds: [ayano.id],
|
||||
},
|
||||
kyoko,
|
||||
),
|
||||
(msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
it("フォローしているユーザーのホーム投稿が流れる", async () => {
|
||||
const fired = await waitFire(
|
||||
ayano,
|
||||
"hybridTimeline", // ayano:Hybrid
|
||||
() => api("notes/create", { text: "foo", visibility: "home" }, kyoko),
|
||||
(msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
it("フォローしていないローカルユーザーのホーム投稿は流れない", async () => {
|
||||
const fired = await waitFire(
|
||||
ayano,
|
||||
"hybridTimeline", // ayano:Hybrid
|
||||
() =>
|
||||
api("notes/create", { text: "foo", visibility: "home" }, chitose),
|
||||
(msg) => msg.type === "note" && msg.body.userId === chitose.id,
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, false);
|
||||
});
|
||||
|
||||
it("フォローしていないローカルユーザーのフォロワー宛て投稿は流れない", () =>
|
||||
async () => {
|
||||
const fired = await waitFire(
|
||||
ayano,
|
||||
"hybridTimeline", // ayano:Hybrid
|
||||
() =>
|
||||
api(
|
||||
"notes/create",
|
||||
{ text: "foo", visibility: "followers" },
|
||||
chitose,
|
||||
),
|
||||
(msg) => msg.type === "note" && msg.body.userId === chitose.id,
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Global Timeline", () => {
|
||||
it("フォローしていないローカルユーザーの投稿が流れる", () => async () => {
|
||||
const fired = await waitFire(
|
||||
ayano,
|
||||
"globalTimeline", // ayano:Global
|
||||
() => api("notes/create", { text: "foo" }, chitose), // chitose posts
|
||||
(msg) => msg.type === "note" && msg.body.userId === chitose.id, // wait chitose
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
it("フォローしていないリモートユーザーの投稿が流れる", () => async () => {
|
||||
const fired = await waitFire(
|
||||
ayano,
|
||||
"globalTimeline", // ayano:Global
|
||||
() => api("notes/create", { text: "foo" }, chinatsu), // chinatsu posts
|
||||
(msg) => msg.type === "note" && msg.body.userId === chinatsu.id, // wait chinatsu
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
it("ホーム投稿は流れない", () => async () => {
|
||||
const fired = await waitFire(
|
||||
ayano,
|
||||
"globalTimeline", // ayano:Global
|
||||
() => api("notes/create", { text: "foo", visibility: "home" }, kyoko), // kyoko posts
|
||||
(msg) => msg.type === "note" && msg.body.userId === kyoko.id, // wait kyoko
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("UserList Timeline", () => {
|
||||
it("リストに入れているユーザーの投稿が流れる", () => async () => {
|
||||
const fired = await waitFire(
|
||||
chitose,
|
||||
"userList",
|
||||
() => api("notes/create", { text: "foo" }, ayano),
|
||||
(msg) => msg.type === "note" && msg.body.userId === ayano.id,
|
||||
{ listId: list.id },
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
it("リストに入れていないユーザーの投稿は流れない", () => async () => {
|
||||
const fired = await waitFire(
|
||||
chitose,
|
||||
"userList",
|
||||
() => api("notes/create", { text: "foo" }, chinatsu),
|
||||
(msg) => msg.type === "note" && msg.body.userId === chinatsu.id,
|
||||
{ listId: list.id },
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, false);
|
||||
});
|
||||
|
||||
// #4471
|
||||
it("リストに入れているユーザーのダイレクト投稿が流れる", () =>
|
||||
async () => {
|
||||
const fired = await waitFire(
|
||||
chitose,
|
||||
"userList",
|
||||
() =>
|
||||
api(
|
||||
"notes/create",
|
||||
{
|
||||
text: "foo",
|
||||
visibility: "specified",
|
||||
visibleUserIds: [chitose.id],
|
||||
},
|
||||
ayano,
|
||||
),
|
||||
(msg) => msg.type === "note" && msg.body.userId === ayano.id,
|
||||
{ listId: list.id },
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, true);
|
||||
});
|
||||
|
||||
// #4335
|
||||
it("リストに入れているがフォローはしてないユーザーのフォロワー宛て投稿は流れない", () =>
|
||||
async () => {
|
||||
const fired = await waitFire(
|
||||
chitose,
|
||||
"userList",
|
||||
() =>
|
||||
api(
|
||||
"notes/create",
|
||||
{ text: "foo", visibility: "followers" },
|
||||
kyoko,
|
||||
),
|
||||
(msg) => msg.type === "note" && msg.body.userId === kyoko.id,
|
||||
{ listId: list.id },
|
||||
);
|
||||
|
||||
assert.strictEqual(fired, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Hashtag Timeline", () => {
|
||||
it("指定したハッシュタグの投稿が流れる", () =>
|
||||
new Promise<void>(async (done) => {
|
||||
const ws = await connectStream(
|
||||
chitose,
|
||||
"hashtag",
|
||||
({ type, body }) => {
|
||||
if (type == "note") {
|
||||
assert.deepStrictEqual(body.text, "#foo");
|
||||
ws.close();
|
||||
done();
|
||||
}
|
||||
},
|
||||
{
|
||||
q: [["foo"]],
|
||||
},
|
||||
);
|
||||
|
||||
post(chitose, {
|
||||
text: "#foo",
|
||||
});
|
||||
}));
|
||||
|
||||
it("指定したハッシュタグの投稿が流れる (AND)", () =>
|
||||
new Promise<void>(async (done) => {
|
||||
let fooCount = 0;
|
||||
let barCount = 0;
|
||||
let fooBarCount = 0;
|
||||
|
||||
const ws = await connectStream(
|
||||
chitose,
|
||||
"hashtag",
|
||||
({ type, body }) => {
|
||||
if (type == "note") {
|
||||
if (body.text === "#foo") fooCount++;
|
||||
if (body.text === "#bar") barCount++;
|
||||
if (body.text === "#foo #bar") fooBarCount++;
|
||||
}
|
||||
},
|
||||
{
|
||||
q: [["foo", "bar"]],
|
||||
},
|
||||
);
|
||||
|
||||
post(chitose, {
|
||||
text: "#foo",
|
||||
});
|
||||
|
||||
post(chitose, {
|
||||
text: "#bar",
|
||||
});
|
||||
|
||||
post(chitose, {
|
||||
text: "#foo #bar",
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
assert.strictEqual(fooCount, 0);
|
||||
assert.strictEqual(barCount, 0);
|
||||
assert.strictEqual(fooBarCount, 1);
|
||||
ws.close();
|
||||
done();
|
||||
}, 3000);
|
||||
}));
|
||||
|
||||
it("指定したハッシュタグの投稿が流れる (OR)", () =>
|
||||
new Promise<void>(async (done) => {
|
||||
let fooCount = 0;
|
||||
let barCount = 0;
|
||||
let fooBarCount = 0;
|
||||
let piyoCount = 0;
|
||||
|
||||
const ws = await connectStream(
|
||||
chitose,
|
||||
"hashtag",
|
||||
({ type, body }) => {
|
||||
if (type == "note") {
|
||||
if (body.text === "#foo") fooCount++;
|
||||
if (body.text === "#bar") barCount++;
|
||||
if (body.text === "#foo #bar") fooBarCount++;
|
||||
if (body.text === "#piyo") piyoCount++;
|
||||
}
|
||||
},
|
||||
{
|
||||
q: [["foo"], ["bar"]],
|
||||
},
|
||||
);
|
||||
|
||||
post(chitose, {
|
||||
text: "#foo",
|
||||
});
|
||||
|
||||
post(chitose, {
|
||||
text: "#bar",
|
||||
});
|
||||
|
||||
post(chitose, {
|
||||
text: "#foo #bar",
|
||||
});
|
||||
|
||||
post(chitose, {
|
||||
text: "#piyo",
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
assert.strictEqual(fooCount, 1);
|
||||
assert.strictEqual(barCount, 1);
|
||||
assert.strictEqual(fooBarCount, 1);
|
||||
assert.strictEqual(piyoCount, 0);
|
||||
ws.close();
|
||||
done();
|
||||
}, 3000);
|
||||
}));
|
||||
|
||||
it("指定したハッシュタグの投稿が流れる (AND + OR)", () =>
|
||||
new Promise<void>(async (done) => {
|
||||
let fooCount = 0;
|
||||
let barCount = 0;
|
||||
let fooBarCount = 0;
|
||||
let piyoCount = 0;
|
||||
let waaaCount = 0;
|
||||
|
||||
const ws = await connectStream(
|
||||
chitose,
|
||||
"hashtag",
|
||||
({ type, body }) => {
|
||||
if (type == "note") {
|
||||
if (body.text === "#foo") fooCount++;
|
||||
if (body.text === "#bar") barCount++;
|
||||
if (body.text === "#foo #bar") fooBarCount++;
|
||||
if (body.text === "#piyo") piyoCount++;
|
||||
if (body.text === "#waaa") waaaCount++;
|
||||
}
|
||||
},
|
||||
{
|
||||
q: [["foo", "bar"], ["piyo"]],
|
||||
},
|
||||
);
|
||||
|
||||
post(chitose, {
|
||||
text: "#foo",
|
||||
});
|
||||
|
||||
post(chitose, {
|
||||
text: "#bar",
|
||||
});
|
||||
|
||||
post(chitose, {
|
||||
text: "#foo #bar",
|
||||
});
|
||||
|
||||
post(chitose, {
|
||||
text: "#piyo",
|
||||
});
|
||||
|
||||
post(chitose, {
|
||||
text: "#waaa",
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
assert.strictEqual(fooCount, 0);
|
||||
assert.strictEqual(barCount, 0);
|
||||
assert.strictEqual(fooBarCount, 1);
|
||||
assert.strictEqual(piyoCount, 1);
|
||||
assert.strictEqual(waaaCount, 0);
|
||||
ws.close();
|
||||
done();
|
||||
}, 3000);
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,161 +0,0 @@
|
|||
process.env.NODE_ENV = "test";
|
||||
|
||||
import * as assert from "assert";
|
||||
import * as childProcess from "child_process";
|
||||
import {
|
||||
async,
|
||||
signup,
|
||||
request,
|
||||
post,
|
||||
react,
|
||||
connectStream,
|
||||
startServer,
|
||||
shutdownServer,
|
||||
} from "./utils.js";
|
||||
|
||||
describe("Note thread mute", () => {
|
||||
let p: childProcess.ChildProcess;
|
||||
|
||||
let alice: any;
|
||||
let bob: any;
|
||||
let carol: any;
|
||||
|
||||
before(async () => {
|
||||
p = await startServer();
|
||||
alice = await signup({ username: "alice" });
|
||||
bob = await signup({ username: "bob" });
|
||||
carol = await signup({ username: "carol" });
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await shutdownServer(p);
|
||||
});
|
||||
|
||||
it("notes/mentions にミュートしているスレッドの投稿が含まれない", async(async () => {
|
||||
const bobNote = await post(bob, { text: "@alice @carol root note" });
|
||||
const aliceReply = await post(alice, {
|
||||
replyId: bobNote.id,
|
||||
text: "@bob @carol child note",
|
||||
});
|
||||
|
||||
await request("/notes/thread-muting/create", { noteId: bobNote.id }, alice);
|
||||
|
||||
const carolReply = await post(carol, {
|
||||
replyId: bobNote.id,
|
||||
text: "@bob @alice child note",
|
||||
});
|
||||
const carolReplyWithoutMention = await post(carol, {
|
||||
replyId: aliceReply.id,
|
||||
text: "child note",
|
||||
});
|
||||
|
||||
const res = await request("/notes/mentions", {}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(
|
||||
res.body.some((note: any) => note.id === bobNote.id),
|
||||
false,
|
||||
);
|
||||
assert.strictEqual(
|
||||
res.body.some((note: any) => note.id === carolReply.id),
|
||||
false,
|
||||
);
|
||||
assert.strictEqual(
|
||||
res.body.some((note: any) => note.id === carolReplyWithoutMention.id),
|
||||
false,
|
||||
);
|
||||
}));
|
||||
|
||||
it("ミュートしているスレッドからメンションされても、hasUnreadMentions が true にならない", async(async () => {
|
||||
// 状態リセット
|
||||
await request("/i/read-all-unread-notes", {}, alice);
|
||||
|
||||
const bobNote = await post(bob, { text: "@alice @carol root note" });
|
||||
|
||||
await request("/notes/thread-muting/create", { noteId: bobNote.id }, alice);
|
||||
|
||||
const carolReply = await post(carol, {
|
||||
replyId: bobNote.id,
|
||||
text: "@bob @alice child note",
|
||||
});
|
||||
|
||||
const res = await request("/i", {}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(res.body.hasUnreadMentions, false);
|
||||
}));
|
||||
|
||||
it("ミュートしているスレッドからメンションされても、ストリームに unreadMention イベントが流れてこない", () =>
|
||||
new Promise(async (done) => {
|
||||
// 状態リセット
|
||||
await request("/i/read-all-unread-notes", {}, alice);
|
||||
|
||||
const bobNote = await post(bob, { text: "@alice @carol root note" });
|
||||
|
||||
await request(
|
||||
"/notes/thread-muting/create",
|
||||
{ noteId: bobNote.id },
|
||||
alice,
|
||||
);
|
||||
|
||||
let fired = false;
|
||||
|
||||
const ws = await connectStream(alice, "main", async ({ type, body }) => {
|
||||
if (type === "unreadMention") {
|
||||
if (body === bobNote.id) return;
|
||||
fired = true;
|
||||
}
|
||||
});
|
||||
|
||||
const carolReply = await post(carol, {
|
||||
replyId: bobNote.id,
|
||||
text: "@bob @alice child note",
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
assert.strictEqual(fired, false);
|
||||
ws.close();
|
||||
done();
|
||||
}, 5000);
|
||||
}));
|
||||
|
||||
it("i/notifications にミュートしているスレッドの通知が含まれない", async(async () => {
|
||||
const bobNote = await post(bob, { text: "@alice @carol root note" });
|
||||
const aliceReply = await post(alice, {
|
||||
replyId: bobNote.id,
|
||||
text: "@bob @carol child note",
|
||||
});
|
||||
|
||||
await request("/notes/thread-muting/create", { noteId: bobNote.id }, alice);
|
||||
|
||||
const carolReply = await post(carol, {
|
||||
replyId: bobNote.id,
|
||||
text: "@bob @alice child note",
|
||||
});
|
||||
const carolReplyWithoutMention = await post(carol, {
|
||||
replyId: aliceReply.id,
|
||||
text: "child note",
|
||||
});
|
||||
|
||||
const res = await request("/i/notifications", {}, alice);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(
|
||||
res.body.some(
|
||||
(notification: any) => notification.note.id === carolReply.id,
|
||||
),
|
||||
false,
|
||||
);
|
||||
assert.strictEqual(
|
||||
res.body.some(
|
||||
(notification: any) =>
|
||||
notification.note.id === carolReplyWithoutMention.id,
|
||||
),
|
||||
false,
|
||||
);
|
||||
|
||||
// NOTE: bobの投稿はスレッドミュート前に行われたため通知に含まれていてもよい
|
||||
}));
|
||||
});
|
|
@ -1,41 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"noEmitOnError": false,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedParameters": false,
|
||||
"noUnusedLocals": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"declaration": false,
|
||||
"sourceMap": true,
|
||||
"target": "es2021",
|
||||
"module": "es2020",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"removeComments": false,
|
||||
"noLib": false,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["../src/*"]
|
||||
},
|
||||
"typeRoots": [
|
||||
"../node_modules/@types",
|
||||
"../src/@types"
|
||||
],
|
||||
"lib": [
|
||||
"esnext"
|
||||
]
|
||||
},
|
||||
"compileOnSave": false,
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
]
|
||||
}
|
|
@ -1,98 +0,0 @@
|
|||
process.env.NODE_ENV = "test";
|
||||
|
||||
import * as assert from "assert";
|
||||
import * as childProcess from "child_process";
|
||||
import {
|
||||
async,
|
||||
signup,
|
||||
request,
|
||||
post,
|
||||
uploadUrl,
|
||||
startServer,
|
||||
shutdownServer,
|
||||
} from "./utils.js";
|
||||
|
||||
describe("users/notes", () => {
|
||||
let p: childProcess.ChildProcess;
|
||||
|
||||
let alice: any;
|
||||
let jpgNote: any;
|
||||
let pngNote: any;
|
||||
let jpgPngNote: any;
|
||||
|
||||
before(async () => {
|
||||
p = await startServer();
|
||||
alice = await signup({ username: "alice" });
|
||||
const jpg = await uploadUrl(
|
||||
alice,
|
||||
"https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg",
|
||||
);
|
||||
const png = await uploadUrl(
|
||||
alice,
|
||||
"https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.png",
|
||||
);
|
||||
jpgNote = await post(alice, {
|
||||
fileIds: [jpg.id],
|
||||
});
|
||||
pngNote = await post(alice, {
|
||||
fileIds: [png.id],
|
||||
});
|
||||
jpgPngNote = await post(alice, {
|
||||
fileIds: [jpg.id, png.id],
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await shutdownServer(p);
|
||||
});
|
||||
|
||||
it("ファイルタイプ指定 (jpg)", async(async () => {
|
||||
const res = await request(
|
||||
"/users/notes",
|
||||
{
|
||||
userId: alice.id,
|
||||
fileType: ["image/jpeg"],
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.length, 2);
|
||||
assert.strictEqual(
|
||||
res.body.some((note: any) => note.id === jpgNote.id),
|
||||
true,
|
||||
);
|
||||
assert.strictEqual(
|
||||
res.body.some((note: any) => note.id === jpgPngNote.id),
|
||||
true,
|
||||
);
|
||||
}));
|
||||
|
||||
it("ファイルタイプ指定 (jpg or png)", async(async () => {
|
||||
const res = await request(
|
||||
"/users/notes",
|
||||
{
|
||||
userId: alice.id,
|
||||
fileType: ["image/jpeg", "image/png"],
|
||||
},
|
||||
alice,
|
||||
);
|
||||
|
||||
assert.strictEqual(res.status, 200);
|
||||
assert.strictEqual(Array.isArray(res.body), true);
|
||||
assert.strictEqual(res.body.length, 3);
|
||||
assert.strictEqual(
|
||||
res.body.some((note: any) => note.id === jpgNote.id),
|
||||
true,
|
||||
);
|
||||
assert.strictEqual(
|
||||
res.body.some((note: any) => note.id === pngNote.id),
|
||||
true,
|
||||
);
|
||||
assert.strictEqual(
|
||||
res.body.some((note: any) => note.id === jpgPngNote.id),
|
||||
true,
|
||||
);
|
||||
}));
|
||||
});
|
|
@ -1,403 +0,0 @@
|
|||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname } from "node:path";
|
||||
import * as childProcess from "child_process";
|
||||
import * as http from "node:http";
|
||||
import { SIGKILL } from "constants";
|
||||
import WebSocket from "ws";
|
||||
import * as misskey from "firefish-js";
|
||||
import fetch from "node-fetch";
|
||||
import FormData from "form-data";
|
||||
import { DataSource } from "typeorm";
|
||||
import loadConfig from "../src/config/load.js";
|
||||
import { entities } from "../src/db/postgre.js";
|
||||
import got from "got";
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
|
||||
const config = loadConfig();
|
||||
export const port = config.port;
|
||||
|
||||
export const async = (fn: Function) => (done: Function) => {
|
||||
fn().then(
|
||||
() => {
|
||||
done();
|
||||
},
|
||||
(err: Error) => {
|
||||
done(err);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const api = async (endpoint: string, params: any, me?: any) => {
|
||||
endpoint = endpoint.replace(/^\//, "");
|
||||
|
||||
const auth = me
|
||||
? {
|
||||
i: me.token,
|
||||
}
|
||||
: {};
|
||||
|
||||
const res = await got<string>(`http://localhost:${port}/api/${endpoint}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(Object.assign(auth, params)),
|
||||
retry: {
|
||||
limit: 0,
|
||||
},
|
||||
hooks: {
|
||||
beforeError: [
|
||||
(error) => {
|
||||
const { response } = error;
|
||||
if (response && response.body) console.warn(response.body);
|
||||
return error;
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const status = res.statusCode;
|
||||
const body = res.statusCode !== 204 ? await JSON.parse(res.body) : null;
|
||||
|
||||
return {
|
||||
status,
|
||||
body,
|
||||
};
|
||||
};
|
||||
|
||||
export const request = async (
|
||||
endpoint: string,
|
||||
params: any,
|
||||
me?: any,
|
||||
): Promise<{ body: any; status: number }> => {
|
||||
const auth = me
|
||||
? {
|
||||
i: me.token,
|
||||
}
|
||||
: {};
|
||||
|
||||
const res = await fetch(`http://localhost:${port}/api${endpoint}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(Object.assign(auth, params)),
|
||||
});
|
||||
|
||||
const status = res.status;
|
||||
const body = res.status !== 204 ? await res.json().catch() : null;
|
||||
|
||||
return {
|
||||
body,
|
||||
status,
|
||||
};
|
||||
};
|
||||
|
||||
export const signup = async (params?: any): Promise<any> => {
|
||||
const q = Object.assign(
|
||||
{
|
||||
username: "test",
|
||||
password: "test",
|
||||
},
|
||||
params,
|
||||
);
|
||||
|
||||
const res = await api("signup", q);
|
||||
|
||||
return res.body;
|
||||
};
|
||||
|
||||
export const post = async (
|
||||
user: any,
|
||||
params?: misskey.Endpoints["notes/create"]["req"],
|
||||
): Promise<misskey.entities.Note> => {
|
||||
const q = Object.assign(
|
||||
{
|
||||
text: "test",
|
||||
},
|
||||
params,
|
||||
);
|
||||
|
||||
const res = await api("notes/create", q, user);
|
||||
|
||||
return res.body ? res.body.createdNote : null;
|
||||
};
|
||||
|
||||
export const react = async (
|
||||
user: any,
|
||||
note: any,
|
||||
reaction: string,
|
||||
): Promise<any> => {
|
||||
await api(
|
||||
"notes/reactions/create",
|
||||
{
|
||||
noteId: note.id,
|
||||
reaction: reaction,
|
||||
},
|
||||
user,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Upload file
|
||||
* @param user User
|
||||
* @param _path Optional, absolute path or relative from ./resources/
|
||||
*/
|
||||
export const uploadFile = async (user: any, _path?: string): Promise<any> => {
|
||||
const absPath =
|
||||
_path == null
|
||||
? `${_dirname}/resources/Lenna.jpg`
|
||||
: path.isAbsolute(_path)
|
||||
? _path
|
||||
: `${_dirname}/resources/${_path}`;
|
||||
|
||||
const formData = new FormData() as any;
|
||||
formData.append("i", user.token);
|
||||
formData.append("file", fs.createReadStream(absPath));
|
||||
formData.append("force", "true");
|
||||
|
||||
const res = await got<string>(
|
||||
`http://localhost:${port}/api/drive/files/create`,
|
||||
{
|
||||
method: "POST",
|
||||
body: formData,
|
||||
retry: {
|
||||
limit: 0,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const body = res.statusCode !== 204 ? await JSON.parse(res.body) : null;
|
||||
|
||||
return body;
|
||||
};
|
||||
|
||||
export const uploadUrl = async (user: any, url: string) => {
|
||||
let file: any;
|
||||
|
||||
const ws = await connectStream(user, "main", (msg) => {
|
||||
if (msg.type === "driveFileCreated") {
|
||||
file = msg.body;
|
||||
}
|
||||
});
|
||||
|
||||
await api(
|
||||
"drive/files/upload-from-url",
|
||||
{
|
||||
url,
|
||||
force: true,
|
||||
},
|
||||
user,
|
||||
);
|
||||
|
||||
await sleep(5000);
|
||||
ws.close();
|
||||
|
||||
return file;
|
||||
};
|
||||
|
||||
export function connectStream(
|
||||
user: any,
|
||||
channel: string,
|
||||
listener: (message: Record<string, any>) => any,
|
||||
params?: any,
|
||||
): Promise<WebSocket> {
|
||||
return new Promise((res, rej) => {
|
||||
const ws = new WebSocket(
|
||||
`ws://localhost:${port}/streaming?i=${user.token}`,
|
||||
);
|
||||
|
||||
ws.on("open", () => {
|
||||
ws.on("message", (data) => {
|
||||
const msg = JSON.parse(data.toString());
|
||||
if (msg.type === "channel" && msg.body.id === "a") {
|
||||
listener(msg.body);
|
||||
} else if (msg.type === "connected" && msg.body.id === "a") {
|
||||
res(ws);
|
||||
}
|
||||
});
|
||||
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "connect",
|
||||
body: {
|
||||
channel: channel,
|
||||
id: "a",
|
||||
pong: true,
|
||||
params: params,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export const waitFire = async (
|
||||
user: any,
|
||||
channel: string,
|
||||
trgr: () => any,
|
||||
cond: (msg: Record<string, any>) => boolean,
|
||||
params?: any,
|
||||
) => {
|
||||
return new Promise<boolean>(async (res, rej) => {
|
||||
let timer: NodeJS.Timeout;
|
||||
|
||||
let ws: WebSocket;
|
||||
try {
|
||||
ws = await connectStream(
|
||||
user,
|
||||
channel,
|
||||
(msg) => {
|
||||
if (cond(msg)) {
|
||||
ws.close();
|
||||
if (timer) clearTimeout(timer);
|
||||
res(true);
|
||||
}
|
||||
},
|
||||
params,
|
||||
);
|
||||
} catch (e) {
|
||||
rej(e);
|
||||
}
|
||||
|
||||
if (!ws!) return;
|
||||
|
||||
timer = setTimeout(() => {
|
||||
ws.close();
|
||||
res(false);
|
||||
}, 3000);
|
||||
|
||||
try {
|
||||
await trgr();
|
||||
} catch (e) {
|
||||
ws.close();
|
||||
if (timer) clearTimeout(timer);
|
||||
rej(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const simpleGet = async (
|
||||
path: string,
|
||||
accept = "*/*",
|
||||
): Promise<{ status?: number; type?: string; location?: string }> => {
|
||||
// node-fetchだと3xxを取れない
|
||||
return await new Promise((resolve, reject) => {
|
||||
const req = http.request(
|
||||
`http://localhost:${port}${path}`,
|
||||
{
|
||||
headers: {
|
||||
Accept: accept,
|
||||
},
|
||||
},
|
||||
(res) => {
|
||||
if (res.statusCode! >= 400) {
|
||||
reject(res);
|
||||
} else {
|
||||
resolve({
|
||||
status: res.statusCode,
|
||||
type: res.headers["content-type"],
|
||||
location: res.headers.location,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
req.end();
|
||||
});
|
||||
};
|
||||
|
||||
export function launchServer(
|
||||
callbackSpawnedProcess: (p: childProcess.ChildProcess) => void,
|
||||
moreProcess: () => Promise<void> = async () => {},
|
||||
) {
|
||||
return (done: (err?: Error) => any) => {
|
||||
const p = childProcess.spawn("node", [_dirname + "/../index.js"], {
|
||||
stdio: ["inherit", "inherit", "inherit", "ipc"],
|
||||
env: { NODE_ENV: "test", PATH: process.env.PATH },
|
||||
});
|
||||
callbackSpawnedProcess(p);
|
||||
p.on("message", (message) => {
|
||||
if (message === "ok")
|
||||
moreProcess()
|
||||
.then(() => done())
|
||||
.catch((e) => done(e));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export async function initTestDb(justBorrow = false, initEntities?: any[]) {
|
||||
if (process.env.NODE_ENV !== "test") throw "NODE_ENV is not a test";
|
||||
|
||||
const db = new DataSource({
|
||||
type: "postgres",
|
||||
host: config.db.host,
|
||||
port: config.db.port,
|
||||
username: config.db.user,
|
||||
password: config.db.pass,
|
||||
database: config.db.db,
|
||||
synchronize: true && !justBorrow,
|
||||
dropSchema: true && !justBorrow,
|
||||
entities: initEntities || entities,
|
||||
});
|
||||
|
||||
await db.initialize();
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
export function startServer(
|
||||
timeout = 60 * 1000,
|
||||
): Promise<childProcess.ChildProcess> {
|
||||
return new Promise((res, rej) => {
|
||||
const t = setTimeout(() => {
|
||||
p.kill(SIGKILL);
|
||||
rej("timeout to start");
|
||||
}, timeout);
|
||||
|
||||
const p = childProcess.spawn("node", [_dirname + "/../built/index.js"], {
|
||||
stdio: ["inherit", "inherit", "inherit", "ipc"],
|
||||
env: { NODE_ENV: "test", PATH: process.env.PATH },
|
||||
});
|
||||
|
||||
p.on("error", (e) => rej(e));
|
||||
|
||||
p.on("message", (message) => {
|
||||
if (message === "ok") {
|
||||
clearTimeout(t);
|
||||
res(p);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function shutdownServer(
|
||||
p: childProcess.ChildProcess,
|
||||
timeout = 20 * 1000,
|
||||
) {
|
||||
return new Promise((res, rej) => {
|
||||
const t = setTimeout(() => {
|
||||
p.kill(SIGKILL);
|
||||
res("force exit");
|
||||
}, timeout);
|
||||
|
||||
p.once("exit", () => {
|
||||
clearTimeout(t);
|
||||
res("exited");
|
||||
});
|
||||
|
||||
p.kill();
|
||||
});
|
||||
}
|
||||
|
||||
export function sleep(msec: number) {
|
||||
return new Promise<void>((res) => {
|
||||
setTimeout(() => {
|
||||
res();
|
||||
}, msec);
|
||||
});
|
||||
}
|
15
packages/client/.vscode/settings.json
vendored
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"path-intellisense.mappings": {
|
||||
"@": "${workspaceRoot}/packages/client/src/"
|
||||
},
|
||||
"files.exclude": {
|
||||
"**/.git": true,
|
||||
"**/.svn": true,
|
||||
"**/.hg": true,
|
||||
"**/CVS": true,
|
||||
"**/.DS_Store": true,
|
||||
"**/Thumbs.db": true,
|
||||
"**/_client_dist_": true
|
||||
}
|
||||
}
|