Compare commits

..

93 Commits

Author SHA1 Message Date
syuilo
e0fc8cbf8f Merge branch 'develop' 2023-02-09 18:12:04 +09:00
syuilo
f9d1bc340e 13.5.4 2023-02-09 18:11:48 +09:00
syuilo
0b269e79fd i/notificationsのレートリミットを緩和 2023-02-09 18:11:11 +09:00
syuilo
6159cfd138 enhance(client): improve api error handling 2023-02-09 18:07:51 +09:00
syuilo
6a5bbd335b Update CHANGELOG.md 2023-02-09 18:03:04 +09:00
syuilo
39e269db8c Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-02-09 18:01:15 +09:00
syuilo
70fe23a3ce fix(client): validate url to improve security 2023-02-09 18:01:12 +09:00
KOKO
a6a8a7fb85 fix: dateの初期値が正常に入らない時がある (#9827)
* fix: dateの初期値が正常に入らない時がある

* feat: datettime-localをとれるように

* chore: いらない差分を戻す
2023-02-09 17:54:30 +09:00
syuilo
6641b13b4c Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-02-09 17:54:14 +09:00
syuilo
5136b05c9b New translations ja-JP.yml (Spanish) (#9839) 2023-02-09 17:53:21 +09:00
syuilo
803c2144f4 Update about-misskey.vue 2023-02-09 17:44:18 +09:00
syuilo
b69a079514 lint 2023-02-09 17:36:16 +09:00
syuilo
2aa800cd55 Update about-misskey.vue 2023-02-09 17:34:45 +09:00
tamaina
6e61a36d05 i/notificationsのレートリミットを緩和
SubwayTooterのバグ対策でレートリミットを設定していたが、通常の使い方でも引っかかることもあるため緩和
2023-02-09 08:32:42 +00:00
tamaina
f80bf1fb1c perf: renderBaseでCache-Controlを300秒から30秒に 2023-02-09 08:19:12 +00:00
syuilo
d465e85239 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-02-09 17:03:21 +09:00
tamaina
deed25a2ff Fix #9842 2023-02-09 08:00:45 +00:00
tamaina
a486716520 perf: renderBaseでCache-Controlを15秒から300秒に 2023-02-09 07:49:39 +00:00
syuilo
2361e11e98 Update about-misskey.vue 2023-02-09 16:42:22 +09:00
syuilo
cd1f2adca7 🎨 2023-02-09 13:21:11 +09:00
syuilo
a558767b7a Merge branch 'develop' 2023-02-09 11:54:49 +09:00
syuilo
399ce9b999 13.5.3 2023-02-09 11:54:41 +09:00
syuilo
a94a0b5b0b New Crowdin updates (#9838)
* New translations ja-JP.yml (Romanian)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Slovak)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Vietnamese)

* New translations ja-JP.yml (Indonesian)

* New translations ja-JP.yml (Bengali)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Japanese, Kansai)
2023-02-09 11:52:08 +09:00
syuilo
76faec2115 refactor: fix types 2023-02-09 11:46:08 +09:00
syuilo
33c4e57994 refactor: fix types 2023-02-09 11:42:55 +09:00
syuilo
bc23496998 refactor: fix types 2023-02-09 11:31:40 +09:00
syuilo
d35ad95c18 refactor: fix types 2023-02-09 11:03:40 +09:00
syuilo
5facd11592 refactor: fix types 2023-02-09 11:02:37 +09:00
syuilo
e1e885d6b2 refactor: fix types 2023-02-09 10:55:15 +09:00
syuilo
5b6695114f refactor: fix types 2023-02-09 10:50:53 +09:00
syuilo
71dd7f89e9 clean up 2023-02-09 10:47:03 +09:00
syuilo
21331e53fe refactor: fix types 2023-02-09 10:46:01 +09:00
syuilo
7afee5977f feat(client): add channel column to deck 2023-02-09 10:35:28 +09:00
syuilo
d195b0dec7 refactor(client): use css modules 2023-02-09 10:11:33 +09:00
syuilo
8a95e850ad Update ROADMAP.md 2023-02-09 09:54:30 +09:00
syuilo
a4d74d7d7e 🎨 2023-02-09 09:48:35 +09:00
syuilo
256e0db36d 多分 #9815 2023-02-09 09:33:46 +09:00
syuilo
d593c1358a 🎨 2023-02-09 09:32:39 +09:00
syuilo
1ff14d81c1 update deps 2023-02-09 09:25:31 +09:00
syuilo
4369d12eec Merge branch 'develop' 2023-02-08 20:17:24 +09:00
syuilo
91cc033eb5 13.5.2 2023-02-08 20:17:13 +09:00
syuilo
57543e6b44 fix(client): ログイントークンの再生成が出来ない
Fix #9822
2023-02-08 20:12:44 +09:00
syuilo
a1b8cd15c4 fix(client): register_note_view_interruptor not working
Fix #9826
2023-02-08 20:11:53 +09:00
syuilo
73f06e591a revert: 650187deaf 2023-02-08 20:07:19 +09:00
tamaina
6f7cfa82b5 fix(client): 通知のノート表示で_nowrapが効いていない問題を修正
Fix #9834
2023-02-08 09:50:34 +00:00
syuilo
ff97a003d1 Merge branch 'develop' 2023-02-08 18:14:27 +09:00
syuilo
53c92e3e23 13.5.1 2023-02-08 18:14:17 +09:00
syuilo
13d13bc2f6 fix broken component 2023-02-08 18:13:45 +09:00
syuilo
03744a25ed Merge branch 'develop' 2023-02-08 18:03:28 +09:00
syuilo
eac3bf8bff 13.5.0 2023-02-08 18:03:13 +09:00
syuilo
2e1fbb5b16 New Crowdin updates (#9812)
* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Lao)

* New translations ja-JP.yml (Lao)

* New translations ja-JP.yml (Chinese Traditional)
2023-02-08 18:00:45 +09:00
Masaya Suzuki
98b3517d36 package.json内のscriptsでbackendのpackage.json内のscriptsを実行する (#9833) 2023-02-08 18:00:20 +09:00
파링
dee662705e fix docker health check (#9810)
* fix(healthcheck): use default commands instead of yq

this removes yq command and uses grep and awk to get port

* fix: use correct config file

* fix: install curl in runner instead of builder

* fix: remove unused packages
2023-02-08 17:59:10 +09:00
syuilo
0da0cc80b9 fix(server): validate url from ap to improve security 2023-02-08 17:50:23 +09:00
syuilo
650187deaf perf(client): do not render custom emojis in user names
#9778
2023-02-08 17:48:02 +09:00
syuilo
2e565cac2c enhance(client): use VuePlyr
Close #9797

Co-Authored-By: Rox Squires <rox@roxsquires.gay>
2023-02-08 17:05:36 +09:00
syuilo
ac7537278c enhance(client): tweak medialist style 2023-02-08 16:54:51 +09:00
itiradi
f9a2e98831 fix(mfm): default degree not used in rotate (#9831) 2023-02-08 08:20:27 +09:00
tamaina
54f789bd55 fix(server): DriveFileEntityService.getPublicUrl調整
- 外部MediaProxyではビデオのサムネイルを生成できないので外部に投げない
- thumbnailUrlが存在しない場合、画像の場合はプロキシで圧縮させる
2023-02-07 14:24:15 +00:00
syuilo
5ac9d13516 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-02-07 19:59:00 +09:00
syuilo
2be1a39d13 fix(server): validate urls from ap to improve security 2023-02-07 19:58:58 +09:00
Masaya Suzuki
f3c5edc852 fix: postgre -> postgres (#9814) 2023-02-07 19:50:38 +09:00
tamaina
30704e6de8 update CHANGELOG 2023-02-06 12:13:43 +00:00
tamaina
41932ac409 MkEmojiPickerでも Fix #9598 2023-02-06 12:05:33 +00:00
tamaina
9843c596d8 disableShowingAnimatedImagesのデフォルト値をprefers-reduced-motionにする
Resolve #9821
Related to #6501
2023-02-06 11:29:48 +00:00
syuilo
baf65bfa69 Merge branch 'develop' 2023-02-05 20:55:51 +09:00
syuilo
6501f80fc7 13.4.0 2023-02-05 20:55:42 +09:00
syuilo
b037f6566b Add Thai to language selection
Resolve #9809
2023-02-05 20:53:40 +09:00
syuilo
0ec8ebeba3 fix(client): tweak notification style
Fix #8633
2023-02-05 20:47:27 +09:00
syuilo
af1c9251fc chore(client): add type check 2023-02-05 20:38:33 +09:00
Masaya Suzuki
4ad399c593 fix: テスト実行時のDB立ち上げコマンド修正 (#9804) 2023-02-05 20:34:22 +09:00
syuilo
55a9646f23 New Crowdin updates (#9798)
* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Thai)

* New translations ja-JP.yml (Lao)

* New translations ja-JP.yml (Lao)
2023-02-05 20:32:19 +09:00
syuilo
46017f5725 Update CHANGELOG.md 2023-02-05 20:32:12 +09:00
Caipira
c20ce12f86 enhance(client): add webhook delete button (#9806) 2023-02-05 20:31:38 +09:00
syuilo
1e28db2396 Update CHANGELOG.md 2023-02-05 20:30:46 +09:00
syuilo
5f3640c7fd fix(client): validate input response in aiscript 2023-02-05 20:29:10 +09:00
syuilo
d65e5f6794 単なるラッキーの獲得確立を調整 2023-02-05 14:38:21 +09:00
syuilo
e67d7bc0ea tweak animation 2023-02-05 14:35:00 +09:00
syuilo
1139632f95 fix(server): 自分のノートをお気に入りに登録しても実績解除される問題を修正 2023-02-05 14:30:07 +09:00
syuilo
b51a8c3f82 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-02-05 14:25:39 +09:00
syuilo
0d7256678e fix(server): validate filename and emoji name to improve security 2023-02-05 14:25:37 +09:00
Masaya Suzuki
eea33d07fd fix: aptのキャッシュを削除しないようにする (#9803) 2023-02-05 14:15:59 +09:00
Masaya Suzuki
f599337320 DockleのCI追加 (#9568)
* Dockerイメージ検査のCI追加

* Add cp

* step分離

* step分離

* rm depends_on

* Dockle実行時に必要なイメージタグ付与処理をCI内で行う

* 末尾に移動

* Add comment

* .git削除処理をビルドステージに移動

* docker-compose.yml作成処理追加

* aptのキャッシュ削除処理追加

* ヘルスチェック用スクリプト追加

* yqインストール処理修正

* Add ca-certificates

* yqインストール処理をビルドステージに移動

* インデントを揃える

* インデントをタブに変更
2023-02-05 14:04:02 +09:00
Takuya Yoshida
7df019db0e BuildX設定漏れ修正 (#9741)
* BuildX設定漏れ

* Update .github/workflows/docker-develop.yml

Co-authored-by: Masaya Suzuki <15100604+massongit@users.noreply.github.com>

---------

Co-authored-by: Masaya Suzuki <15100604+massongit@users.noreply.github.com>
2023-02-05 14:03:26 +09:00
futchitwo
04f92bd688 feat: timeline page for non-login users (#9795) 2023-02-05 14:02:54 +09:00
MeiMei
505ecf6c1f Deny UNIX domain socket (#9802)
* Deny UNIX domain socket

* got v12ならこれが使える?
2023-02-05 13:51:59 +09:00
Masaya Suzuki
c9ec08704e fix: インラインコードを折り返して表示する (#9801) 2023-02-05 13:33:21 +09:00
syuilo
6a3039f7b7 feat: ロールにアイコンを設定してユーザー名の横に表示できるように
Resolve #9761
2023-02-05 10:37:03 +09:00
tamaina
868c8fffb3 update CHANGELOG.md 2023-02-04 15:05:13 +00:00
tamaina
faed3b438e fix(server): clean up file in FileServer 2023-02-04 13:46:19 +00:00
syuilo
6c982629ea Merge branch 'develop' 2023-02-04 19:19:57 +09:00
syuilo
110bbbc7dc 13.3.4 2023-02-04 19:19:48 +09:00
syuilo
4ad0345f20 fix(server): cannot follow user 2023-02-04 19:19:30 +09:00
123 changed files with 1506 additions and 980 deletions

View File

@@ -16,9 +16,15 @@ files/
misskey-assets/
fluent-emojis/
.pnp.*
# .yarn関連
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
.idea/
packages/*/.vscode/
packages/backend/test/docker-compose.yml

3
.dockleignore Normal file
View File

@@ -0,0 +1,3 @@
DKL-DI-0005
DKL-DI-0006
DKL-LI-0003

View File

@@ -14,6 +14,8 @@ jobs:
steps:
- name: Check out the repo
uses: actions/checkout@v3.3.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.3.0
- name: Docker meta
id: meta
uses: docker/metadata-action@v4

30
.github/workflows/dockle.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
---
name: Dockle
on:
push:
branches:
- master
- develop
pull_request:
jobs:
dockle:
runs-on: ubuntu-latest
env:
DOCKER_CONTENT_TRUST: 1
steps:
- uses: actions/checkout@v3.2.0
- run: |
curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v0.4.10/dockle_0.4.10_Linux-64bit.deb"
sudo dpkg -i dockle.deb
- run: |
cp .config/docker_example.env .config/docker.env
cp ./docker-compose.yml.example ./docker-compose.yml
- run: |
docker compose up -d web
docker tag "$(docker compose images web | awk 'OFS=":" {print $4}' | tail -n +2)" misskey-web:latest
- run: |
cmd="dockle --exit-code 1 misskey-web:latest ${image_name}"
echo "> ${cmd}"
eval "${cmd}"

View File

@@ -8,6 +8,62 @@
You should also include the user name that made the change.
-->
## 13.5.4 (2023/02/09)
### Improvements
- Server: UIのHTMLートなどの特別なページを除くのキャッシュ時間を15秒から30秒に
- i/notificationsのレートリミットを緩和
### Bugfixes
- fix(client): validate url to improve security
- fix(client): dateの初期値が正常に入らない時がある
## 13.5.3 (2023/02/09)
### Improvements
- Client: デッキにチャンネルカラムを追加
## 13.5.2 (2023/02/08)
### Changes
- Revert: perf(client): do not render custom emojis in user names
### Bugfixes
- Client: register_note_view_interruptor not working
- Client: ログイントークンの再生成が出来ない
## 13.5.0 (2023/02/08)
### Changes
- perf(client): do not render custom emojis in user names
### Improvements
- Client: disableShowingAnimatedImagesのデフォルト値をprefers-reduced-motionにする
- enhance(client): tweak medialist style
### Bugfixes
- fix docker health check
- Client: MkEmojiPickerでもChromeで検索ダイアログで変換確定するとそのまま検索されてしまうのを修正
- fix(mfm): default degree not used in rotate
- fix(server): validate urls from ap to improve security
## 13.4.0 (2023/02/05)
### Improvements
- ロールにアイコンを設定してユーザー名の横に表示できるように
- feat: timeline page for non-login users
- 実績の単なるラッキーの獲得確立を調整
- Add Thai language support
### Bugfixes
- fix(server): 自分のノートをお気に入りに登録しても実績解除される問題を修正
- fix(server): clean up file in FileServer
- fix(server): Deny UNIX domain socket
- fix(server): validate filename and emoji name to improve security
- fix(client): validate input response in aiscript
- fix(client): add webhook delete button
- fix(client): tweak notification style
- fix(client): インラインコードを折り返して表示する
## 13.3.3 (2023/02/04)

View File

@@ -121,7 +121,7 @@ cp .github/misskey/test.yml .config/
```
Prepare DB/Redis for testing.
```
docker-compose -f packages/backend/test/docker-compose.yml up
docker compose -f packages/backend/test/docker-compose.yml up
```
Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.

View File

@@ -29,6 +29,7 @@ ARG NODE_ENV=production
RUN git submodule update --init
RUN pnpm build
RUN rm -rf .git/
FROM node:${NODE_VERSION}-slim AS runner
@@ -41,10 +42,12 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache \
&& apt-get update \
&& apt-get install -y --no-install-recommends \
ffmpeg tini \
ffmpeg tini curl \
&& corepack enable \
&& groupadd -g "${GID}" misskey \
&& useradd -l -u "${UID}" -g "${GID}" -m -d /misskey misskey
&& useradd -l -u "${UID}" -g "${GID}" -m -d /misskey misskey \
&& find / -type f -perm /u+s -ignore_readdir_race -exec chmod u-s {} \; \
&& find / -type f -perm /g+s -ignore_readdir_race -exec chmod g-s {} \;
USER misskey
WORKDIR /misskey
@@ -58,5 +61,6 @@ COPY --chown=misskey:misskey --from=builder /misskey/fluent-emojis /misskey/flue
COPY --chown=misskey:misskey . ./
ENV NODE_ENV=production
HEALTHCHECK --interval=5s --retries=20 CMD ["/bin/bash", "/misskey/healthcheck.sh"]
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["pnpm", "run", "migrateandstart"]

View File

@@ -6,16 +6,13 @@ Also, the later tasks are more indefinite and are subject to change as developme
This is the phase we are at now. We need to make a high-maintenance environment that can withstand future development.
- Make the number of type errors zero (backend)
- Probably need to switch some libraries to others that make it difficult to reduce type errors
- e.g. koa to fastify https://github.com/misskey-dev/misskey/issues/7537
- Improve CI
- Fix tests
- mocha, jest, etc. do not support the combination of `TypeScript + ESM + Path alias`, and the tests currently do not work.
- Fix random test failures - https://github.com/misskey-dev/misskey/issues/7985 and https://github.com/misskey-dev/misskey/issues/7986
- Add more tests
- May need to implement a mechanism that allows for DI
- ~~May need to implement a mechanism that allows for DI~~ → Done ✔️
- https://github.com/misskey-dev/misskey/pull/9085
- Measure coverage
- ~~Measure coverage~~ → Done ✔️
- https://github.com/misskey-dev/misskey/pull/9081
- Improve documentation
- Refactoring

4
healthcheck.sh Normal file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
PORT=$(grep '^port:' /misskey/.config/default.yml | awk 'NR==1{print $2; exit}')
curl -s -S -o /dev/null "http://localhost:${PORT}"

View File

@@ -1345,5 +1345,6 @@ _deck:
tl: "الخيط الزمني"
antenna: "الهوائيات"
list: "القوائم"
channel: "القنوات"
mentions: "الإشارات"
direct: "مباشرة"

View File

@@ -1441,5 +1441,6 @@ _deck:
tl: "টাইমলাইন"
antenna: "অ্যান্টেনা"
list: "লিস্ট"
channel: "চ্যানেলগুলি"
mentions: "উল্লেখসমূহ"
direct: "ডাইরেক্ট নোটগুলি"

View File

@@ -804,4 +804,5 @@ _deck:
tl: "Časová osa"
antenna: "Antény"
list: "Seznamy"
channel: "Kanály"
mentions: "Zmínění"

View File

@@ -1195,6 +1195,9 @@ _role:
baseRole: "Rollenvorlage"
useBaseValue: "Wert der Rollenvorlage verwenden"
chooseRoleToAssign: "Zuzuweisende Rolle auswählen"
iconUrl: "Icon-URL"
asBadge: "Als Abzeichen anzeigen"
descriptionOfAsBadge: "Ist dies aktiviert, so wird das Icon dieser Rolle an der Seite der Namen von Benutzern mit dieser Rolle angezeigt."
canEditMembersByModerator: "Moderatoren können Benutzern diese Rolle zuweisen"
descriptionOfCanEditMembersByModerator: "Wenn aktiviert, so können Moderatoren und Adminstratoren anderen Benutzern diese Rolle zuweisen bzw. diese Zuweisung aufheben. Wenn deaktiviert, so ist es nur Administratoren möglich, Zuweisungen dieser Rolle zu verwalten."
priority: "Priorität"
@@ -1866,5 +1869,6 @@ _deck:
tl: "Chronik"
antenna: "Antennen"
list: "Listen"
channel: "Kanäle"
mentions: "Erwähnungen"
direct: "Direktnachrichten"

View File

@@ -1195,6 +1195,9 @@ _role:
baseRole: "Role template"
useBaseValue: "Use role template value"
chooseRoleToAssign: "Select the role to assign"
iconUrl: "Icon URL"
asBadge: "Show as badge"
descriptionOfAsBadge: "This role's icon will be displayed next to the username of users with this role if turned on."
canEditMembersByModerator: "Allow moderators to edit the list of members for this role"
descriptionOfCanEditMembersByModerator: "When turned on, moderators as well as administrators will be able to assign and unassign users to this role. When turned off, only administrators will be able to assign users."
priority: "Priority"
@@ -1866,5 +1869,6 @@ _deck:
tl: "Timeline"
antenna: "Antennas"
list: "List"
channel: "Channels"
mentions: "Mentions"
direct: "Direct notes"

View File

@@ -129,6 +129,7 @@ unblockConfirm: "¿Quiere dejar de bloquear esta cuenta?"
suspendConfirm: "¿Quiere suspender esta cuenta?"
unsuspendConfirm: "¿Quiere dejar de suspender esta cuenta?"
selectList: "Seleccione una lista"
selectChannel: "Seleccionar canal"
selectAntenna: "Seleccionar antena"
selectWidget: "Seleccionar widget"
editWidgets: "Editar widgets"
@@ -1195,6 +1196,9 @@ _role:
baseRole: "Rol base"
useBaseValue: "Usar los valores del rol base"
chooseRoleToAssign: "Selecciona el rol para asignar"
iconUrl: "URL del ícono"
asBadge: "Mostrar como emblema"
descriptionOfAsBadge: "Este ícono de rol se mostrará a lado del nombre de usuario cuando este rol se encuentre activo."
canEditMembersByModerator: "Permitir a los moderadores editar los miembros"
descriptionOfCanEditMembersByModerator: "Si se activa, los moderadores, al igual que los administradores, serán capaces de asignar/quitar usuarios a éste rol. Si se desactiva, sólo los administradores podrán hacerlo."
priority: "Prioridad"
@@ -1866,5 +1870,6 @@ _deck:
tl: "Linea de tiempo"
antenna: "Antenas"
list: "Listas"
channel: "Canal"
mentions: "Menciones"
direct: "Mensaje directo"

View File

@@ -1541,5 +1541,6 @@ _deck:
tl: "Fil"
antenna: "Antennes"
list: "Listes"
channel: "Canaux"
mentions: "Mentions"
direct: "Direct"

View File

@@ -1673,5 +1673,6 @@ _deck:
tl: "Linimasa"
antenna: "Antena"
list: "Daftar"
channel: "Kanal"
mentions: "Sebutan"
direct: "Langsung"

View File

@@ -34,6 +34,7 @@ const languages = [
'pt-PT',
'ru-RU',
'sk-SK',
'th-TH',
'ug-CN',
'uk-UA',
'vi-VN',

View File

@@ -1044,7 +1044,7 @@ _achievements:
flavor: "Grazie per aver usato Misskey!"
_noteClipped1:
title: "Devo clippare!"
description: "Ho raccolto in Clip la prima Nota"
description: "Hai raccolto la tua prima Nota in una Clip"
_noteFavorited1:
title: "Guarda le stelle"
description: "Aggiungi una Nota ai preferiti per la prima volta"
@@ -1080,7 +1080,7 @@ _achievements:
title: "Follow me!"
description: "Hai ottenuto 10 profili Follower"
_followers50:
title: "Follower a frotte"
title: "Un gregge di Follower"
description: "Hai ottenuto 50 Follower"
_followers100:
title: "Popolare"
@@ -1108,7 +1108,7 @@ _achievements:
title: "Caccia al tesoro"
description: "Hai trovato un tesoro nascosto"
_client30min:
title: "Piccola pausa"
title: "Piccola grande pausa"
description: "Hai passato più di 30 minuti su Misskey"
_noteDeletedWithin1min:
title: "Ooops!"
@@ -1134,7 +1134,7 @@ _achievements:
title: "Hello, world!"
description: "Hai scritto «Hello world» nel blocco appunti"
_open3windows:
title: "Finestrato"
title: "Apri le finestre!"
description: "Hai aperto almeno 3 finestre contemporaneamente"
_driveFolderCircularReference:
title: "Riferimento circolare"
@@ -1170,7 +1170,7 @@ _achievements:
_cookieClicked:
title: "Clicca il biscotto"
description: "Hai giocato a cliccare il cookie"
flavor: "Hai autorizzato i cookie?"
flavor: "È il sito giusto?"
_brainDiver:
title: "Brain Diver"
description: "Pubblica un link a Brain Diver"
@@ -1195,6 +1195,9 @@ _role:
baseRole: "Ruolo di base"
useBaseValue: "Eredita dal ruolo base"
chooseRoleToAssign: "Seleziona il ruolo da assegnare"
iconUrl: "URL dell'icona"
asBadge: "Mostra come badge"
descriptionOfAsBadge: "Se indicato, accanto al nome utente viene visualizzata l'icona del ruolo."
canEditMembersByModerator: "Anche i Moderatori assegnano profili a questo ruolo"
descriptionOfCanEditMembersByModerator: "Se disattivo, potranno farlo solamente gli Amministratori."
priority: "Priorità"
@@ -1866,5 +1869,6 @@ _deck:
tl: "Timeline"
antenna: "Antenne"
list: "Liste"
channel: "Canale"
mentions: "Menzioni"
direct: "Diretta"

View File

@@ -129,6 +129,7 @@ unblockConfirm: "ブロック解除しますか?"
suspendConfirm: "凍結しますか?"
unsuspendConfirm: "解凍しますか?"
selectList: "リストを選択"
selectChannel: "チャンネルを選択"
selectAntenna: "アンテナを選択"
selectWidget: "ウィジェットを選択"
editWidgets: "ウィジェットを編集"
@@ -939,6 +940,8 @@ cannotPerformTemporaryDescription: "操作回数が制限を超過するため
preset: "プリセット"
selectFromPresets: "プリセットから選択"
achievements: "実績"
gotInvalidResponseError: "サーバーの応答が無効です"
gotInvalidResponseErrorDescription: "サーバーがダウンまたはメンテナンスしている可能性があります。しばらくしてから再度お試しください。"
_achievements:
earnedAt: "獲得日時"
@@ -1148,7 +1151,7 @@ _achievements:
description: "ここをクリックした"
_justPlainLucky:
title: "単なるラッキー"
description: "10秒ごとに0.01%の確率で獲得"
description: "10秒ごとに0.005%の確率で獲得"
_setNameToSyuilo:
title: "神様コンプレックス"
description: "名前を syuilo に設定した"
@@ -1184,7 +1187,7 @@ _role:
description: "ロールの説明"
permission: "ロールの権限"
descriptionOfPermission: "<b>モデレーター</b>は基本的なモデレーションに関する操作を行えます。\n<b>管理者</b>はインスタンスの全ての設定を変更できます。"
assignTarget: "アサインターゲット"
assignTarget: "アサイン"
descriptionOfAssignTarget: "<b>マニュアル</b>は誰がこのロールに含まれるかを手動で管理します。\n<b>コンディショナル</b>は条件を設定し、それに合致するユーザーが自動で含まれるようになります。"
manual: "マニュアル"
conditional: "コンディショナル"
@@ -1197,6 +1200,9 @@ _role:
baseRole: "ベースロール"
useBaseValue: "ベースロールの値を使用"
chooseRoleToAssign: "アサインするロールを選択"
iconUrl: "アイコン画像のURL"
asBadge: "バッジとして表示"
descriptionOfAsBadge: "オンにすると、ユーザー名の横にロールのアイコンが表示されます。"
canEditMembersByModerator: "モデレーターのメンバー編集を許可"
descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。"
priority: "優先度"
@@ -1919,5 +1925,6 @@ _deck:
tl: "タイムライン"
antenna: "アンテナ"
list: "リスト"
channel: "チャンネル"
mentions: "あなた宛て"
direct: "ダイレクト"

View File

@@ -1628,5 +1628,6 @@ _deck:
tl: "タイムライン"
antenna: "アンテナ"
list: "リスト"
channel: "チャンネル"
mentions: "あんた宛て"
direct: "ダイレクト"

View File

@@ -1866,5 +1866,6 @@ _deck:
tl: "타임라인"
antenna: "안테나"
list: "리스트"
channel: "채널"
mentions: "받은 멘션"
direct: "다이렉트"

72
locales/lo-LA.yml Normal file
View File

@@ -0,0 +1,72 @@
---
_lang_: "ພາສາລາວ"
headlineMisskey: "ເຊື່ອມຕໍ່ເຄືອຂ່າຍໂດຍຫມາຍເຫດ"
introMisskey: "ຍິນດີຕ້ອນຮັບ! Misskey ເປັນແຫຼ່ງເປີດ, ການບໍລິການ microblogging ກະຈາຍ\nສ້າງ \"ບັນທຶກ\" ເພື່ອແບ່ງປັນຄວາມຄິດຂອງທ່ານກັບທຸກໆຄົນທີ່ຢູ່ອ້ອມຮອບທ່ານ 📡\nດ້ວຍ \"ປະຕິກິລິຍາ\", ທ່ານຍັງສາມາດສະແດງຄວາມຮູ້ສຶກຂອງທ່ານຢ່າງໄວວາກ່ຽວກັບບັນທຶກຂອງທຸກໆຄົນ 👍\nມາສຳຫຼວດໂລກໃໝ່! 🚀"
poweredByMisskeyDescription: "{name} ແມ່ນສ່ວນໜຶ່ງຂອງການບໍລິການທີ່ຂັບເຄື່ອນໂດຍແພລດຟອມ open source. <b>Misskey</b> (ເອີ້ນວ່າ \"Misskey instance\")"
monthAndDay: "{ເດືອນ}/{ມື້}"
search: "ຄົ້ນຫາ"
notifications: "ການແຈ້ງເຕືອນ"
username: "ຊື່ຜູ້ໃຊ້"
password: "ລະຫັດຜ່ານ"
forgotPassword: "ລືມລະຫັດຜ່ານ"
fetchingAsApObject: "ກຳລັງດຶງຂໍ້ມູນຈາກ fediverse..."
ok: "ຕົກ​ລົງ"
gotIt: "ເຂົ້າໃຈແລ້ວ!"
cancel: "ຍົກເລີກ"
noThankYou: "ບໍ່​ແມ່ນ​ຕອນ​ນີ້"
enterUsername: "ປ້ອນຊື່ຜູ້ໃຊ້"
renotedBy: "Renoted ໂດຍ {ຜູ້ໃຊ້}"
noNotes: "ບໍ່ມີຫມາຍເຫດ"
noNotifications: "ບໍ່ມີການແຈ້ງເຕືອນ"
instance: "ອີນສະແຕນ"
settings: "ກຳນົດຄ່າ"
basicSettings: "ການຕັ້ງຄ່າພື້ນຖານ"
otherSettings: "ການຕັ້ງຄ່າອື່ນໆ"
openInWindow: "ເປີດຢູ່ໃນປ່ອງຢ້ຽມ"
profile: "ໂພຼຟາຍ"
timeline: "​ເສັ້ນກຳ​ນົດ​ເວ​ລາ​"
noAccountDescription: "ຜູ້ໃຊ້ນີ້ຍັງບໍ່ໄດ້ຂຽນໃນຊີວະປະຫວັດຂອງເຂົາເຈົ້າເທື່ອ"
login: "ເຂົ້າ​ສູ່​ລະ​ບົບ"
loggingIn: "ກຳລັງເຂົ້າສູ່ລະບົບ..."
logout: "ອອກ​ຈາກ​ລະ​ບົບ"
signup: "ລົງ​ທະ​ບຽນ"
uploading: "ການອັບໂຫຼດ..."
save: "ບັນທຶກ"
users: "ຜູ້ໃຊ້ຕ່າງໆ"
addUser: "ເພີ່ມຜູ້ໃຊ້"
favorite: "ເພີ່ມໃສ່ລາຍການທີ່ມັກ"
favorites: "ລາຍການທີ່ມັກ"
unfavorite: "ລຶບອອກຈາກລາຍການທີ່ມັກ"
favorited: "ເພີ່ມໃສ່ລາຍການທີ່ມັກແລ້ວ"
alreadyFavorited: "ເພີ່ມເຂົ້າໃນລາຍການທີ່ມັກແລ້ວ."
cantFavorite: "ບໍ່ສາມາດເພີ່ມໃສ່ລາຍການທີ່ມັກໄດ້."
pin: "ປັກໝຸດໄປຫາໂປຣໄຟລ໌"
unpin: "ຖອດປັກໝຸດອອກຈາກໂປຣໄຟລ໌"
copyContent: "ຄັດລອກເນື້ອຫາ"
copyLink: "ສຳເນົາລິ້ງ"
delete: "ລຶບ"
deleteAndEdit: "ລົບ​ແລະ​ແກ້​ໄຂ​"
deleteAndEditConfirm: "ເຈົ້າ​ແນ່​ໃຈ​ບໍ່? ທີ່ທ່ານຕ້ອງການທີ່ຈະລຶບບັນທຶກນີ້ແລະແກ້ໄຂມັນ ທ່ານອາດຈະສູນເສຍການໂຕ້ຕອບ, ບັນທຶກ, ແລະການຕອບກັບທັງໝົດ"
addToList: "ເພີ່ມໃສ່ລາຍຊື່"
sendMessage: "ສົ່ງຂໍ້ຄວາມ"
pinned: "ປັກໝຸດໄປຫາໂປຣໄຟລ໌"
instances: "ອີນສະແຕນ"
remove: "ລຶບ"
smtpUser: "ຊື່ຜູ້ໃຊ້"
smtpPass: "ລະຫັດຜ່ານ"
user: "ຜູ້ໃຊ້ຕ່າງໆ"
searchByGoogle: "ຄົ້ນຫາ"
_mfm:
search: "ຄົ້ນຫາ"
_sfx:
notification: "ການແຈ້ງເຕືອນ"
_widgets:
profile: "ໂພຼຟາຍ"
notifications: "ການແຈ້ງເຕືອນ"
timeline: "​ເສັ້ນກຳ​ນົດ​ເວ​ລາ​"
_profile:
username: "ຊື່ຜູ້ໃຊ້"
_deck:
_columns:
notifications: "ການແຈ້ງເຕືອນ"
tl: "​ເສັ້ນກຳ​ນົດ​ເວ​ລາ​"

View File

@@ -1438,5 +1438,6 @@ _deck:
tl: "Oś czasu"
antenna: "Anteny"
list: "Listy"
channel: "Kanały"
mentions: "Wspomnienia"
direct: "Bezpośredni"

View File

@@ -721,4 +721,5 @@ _deck:
tl: "Cronologie"
antenna: "Antene"
list: "Liste"
channel: "Canale"
mentions: "Mențiuni"

View File

@@ -1845,5 +1845,6 @@ _deck:
tl: "Лента"
antenna: "Антенны"
list: "Списки"
channel: "Каналы"
mentions: "Упоминания"
direct: "Личное"

View File

@@ -1545,5 +1545,6 @@ _deck:
tl: "Časová os"
antenna: "Antény"
list: "Zoznam"
channel: "Kanály"
mentions: "Zmienky"
direct: "Priame poznámky"

View File

@@ -1147,7 +1147,7 @@ _achievements:
description: "คุณได้คลิกที่นี่"
_justPlainLucky:
title: "แค่ลัคกี้ธรรมดา"
description: "มีโอกาสที่จะได้รับด้วยความน่าจะเป็นไปได้ 0.01% ทุก ๆ 10 วินาที"
description: "มีโอกาสที่จะได้รับด้วยความน่าจะเป็นไปได้ 0.005% ทุก ๆ 10 วินาที"
_setNameToSyuilo:
title: "พระเจ้าคอมเพล็กซ์"
description: "ตั้งชื่อของคุณเป็น \"syuilo\""
@@ -1182,7 +1182,7 @@ _role:
description: "คำอธิบายบทบาท"
permission: "สิทธิ์ตามบทบาท"
descriptionOfPermission: "<b>ผู้ดูแลกลั่นกรองเนื้อหา</b> สามารถดำเนินการดูแลขั้นพื้นฐานได้นะ\n<b>ผู้ดูแลระบบ</b> สามารถเปลี่ยนการตั้งค่าทั้งหมดของอินสแตนซ์ได้นะ"
assignTarget: "กำหนดเป้าหมาย"
assignTarget: "มอบหมาย"
descriptionOfAssignTarget: "<b>แมนนวล</b> เพื่อเปลี่ยนผู้ที่เป็นส่วนหนึ่งของบทบาทนี้และใครที่ไม่ใช่ด้วยตนเอง\n<b>เงื่อนไข</b> เพื่อให้ผู้ใช้ได้รับการกำหนดและนำออกจากบทบาทนี้โดยอัตโนมัติตามเงื่อนไขชุดหนึ่ง"
manual: "ปรับเอง"
conditional: "มีเงื่อนไข"
@@ -1195,6 +1195,9 @@ _role:
baseRole: "บทบาทพื้นฐาน"
useBaseValue: "ใช้บทบาทพื้นฐานเริ่มต้น"
chooseRoleToAssign: "เลือกบทบาทที่ต้องการกำหนด"
iconUrl: "ไอคอน URL"
asBadge: "แสดงเป็นตรา"
descriptionOfAsBadge: "ไอคอนของบทบาทนี้จะปรากฏถัดจากชื่อผู้ใช้ของผู้ใช้งานด้วยบทบาทนี้ถ้าหากเปิดใช้งาน"
canEditMembersByModerator: "อนุญาตให้ผู้ดูแลแก้ไขสมาชิก"
descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ ผู้ดูแลนอกเหนือจากผู้ดูแลระบบแล้ว จะสามารถกำหนดและยกเลิกการมอบหมายบทบาทนี้ให้กับผู้ใช้ได้ เมื่อปิด เฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถกำหนดผู้ใช้ได้นะ"
priority: "ลำดับความสำคัญ"
@@ -1866,5 +1869,6 @@ _deck:
tl: "ไทม์ไลน์"
antenna: "เสาอากาศ"
list: "รายการ"
channel: "แชนแนล"
mentions: "พูดถึง"
direct: "ไดเร็ค"

View File

@@ -1382,8 +1382,8 @@ _tutorial:
step1_1: "Ласкаво просимо!"
step1_2: "Ця сторінка має назву \"стрічка подій\". На ній з'являються записи користувачів на яких ви підписані."
step1_3: "Наразі ваша стрічка порожня, оскільки ви ще не написали жодної нотатки і не підписані на інших."
step2_1: "Перш ніж зробити запис або підписатись на когось, спочатку заповніть свій обліковий запис."
step2_2: "Надання деякої інформації про себе дозволить іншим користувачам підписатись на вас."
step2_1: "Перш ніж зробити запис або підписатись на когось, заповніть свій профіль."
step2_2: "Надання деякої інформації про себе допоможе іншим користувачам вирішити підписатись на вас."
step3_1: "Ви успішно налаштували свій обліковий запис?"
step3_2: "Наступним кроком є написання нотатки. Це можна зробити, натиснувши зображення олівця на екрані."
step3_3: "Після написання вмісту ви можете опублікувати його, натиснувши кнопку у верхньому правому куті форми."
@@ -1689,5 +1689,6 @@ _deck:
tl: "Стрічка"
antenna: "Антени"
list: "Списки"
channel: "Канали"
mentions: "Згадки"
direct: "Особисте"

View File

@@ -1520,5 +1520,6 @@ _deck:
tl: "Bảng tin"
antenna: "Trạm phát sóng"
list: "Danh sách"
channel: "Kênh"
mentions: "Lượt nhắc"
direct: "Nhắn riêng"

View File

@@ -1195,6 +1195,9 @@ _role:
baseRole: "基本角色"
useBaseValue: "使用基本角色的值"
chooseRoleToAssign: "选择要分配的角色"
iconUrl: "图标URL"
asBadge: "作为徽章显示"
descriptionOfAsBadge: "开启后,用户名旁边将会出现角色图标。"
canEditMembersByModerator: "允许监察者编辑成员"
descriptionOfCanEditMembersByModerator: "如果选中,监察者和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。"
priority: "优先级"
@@ -1866,5 +1869,6 @@ _deck:
tl: "时间线"
antenna: "天线"
list: "列表"
channel: "频道"
mentions: "提及"
direct: "指定用户"

View File

@@ -326,7 +326,7 @@ connectService: "己連結"
disconnectService: "己斷開 "
enableLocalTimeline: "開啟本地時間軸"
enableGlobalTimeline: "啟用全域時間軸"
disablingTimelinesInfo: "為了方便,即使您關閉了時間線功能,管理員和審員仍可以繼續使用。"
disablingTimelinesInfo: "為了方便,即使您關閉了時間線功能,管理員和審員仍可以繼續使用。"
registration: "註冊"
enableRegistration: "開啟新使用者註冊"
invite: "邀請"
@@ -389,8 +389,8 @@ aboutMisskey: "關於 Misskey"
administrator: "管理員"
token: "權杖"
twoStepAuthentication: "兩階段驗證"
moderator: "審員"
moderation: "監察"
moderator: "審員"
moderation: "審查"
nUsersMentioned: "提到了{n}"
securityKey: "安全金鑰"
securityKeyName: "金鑰名稱"
@@ -607,7 +607,7 @@ testEmail: "測試郵件發送"
wordMute: "被靜音的文字"
regexpError: "正規表達式錯誤"
regexpErrorDescription: "{tab} 靜音文字的第 {line} 行的正規表達式有錯誤:"
instanceMute: "實例的靜音"
instanceMute: "被靜音的實例"
userSaysSomething: "{name}說了什麼"
makeActive: "啟用"
display: "檢視"
@@ -1181,7 +1181,7 @@ _role:
name: "角色名稱"
description: "角色描述 "
permission: "角色的權限"
descriptionOfPermission: "<b>審員</b>執行與審相關的基本操作。\n<b>管理員</b>能變更實例的全部設定"
descriptionOfPermission: "<b>審員</b>執行與審相關的基本操作。\n<b>管理員</b>能變更實例的全部設定"
assignTarget: "指派目標"
descriptionOfAssignTarget: "<b>手動</b>是以手動管理這個角色包含的人員。\n<b>符合條件</b>是設定條件以自動包含符合條件的使用者。"
manual: "手動"
@@ -1195,8 +1195,11 @@ _role:
baseRole: "基本角色"
useBaseValue: "使用基本角色的值"
chooseRoleToAssign: "選擇要指派的角色"
canEditMembersByModerator: "允許編輯監察員的成員"
descriptionOfCanEditMembersByModerator: "如果開啟,管理員與監察員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。"
iconUrl: "圖示的URL"
asBadge: "顯示為徽章"
descriptionOfAsBadge: "開啟的話,角色圖示會顯示在用戶名旁邊。"
canEditMembersByModerator: "允許編輯審查員的成員"
descriptionOfCanEditMembersByModerator: "如果開啟,管理員與審查員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。"
priority: "優先級"
_priority:
low: "低"
@@ -1233,7 +1236,7 @@ _role:
or: "~或~"
not: "~否"
_sensitiveMediaDetection:
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審。 伺服器的負荷會稍微增加。"
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審。 伺服器的負荷會稍微增加。"
sensitivity: "檢測敏感度"
sensitivityDescription: "敏感度低時,誤檢測(偽陽性)會減少。敏感度高時,漏檢(偽陰性)會減少。"
setSensitiveFlagAutomatically: "設定 NSFW 旗標"
@@ -1866,5 +1869,6 @@ _deck:
tl: "時間軸"
antenna: "天線"
list: "清單"
channel: "頻道"
mentions: "提及"
direct: "指定使用者"

View File

@@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "13.3.3",
"version": "13.5.4",
"codename": "nasubi",
"repository": {
"type": "git",
@@ -19,7 +19,7 @@
"start": "cd packages/backend && node ./built/boot/index.js",
"start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/index.js",
"init": "pnpm migrate",
"migrate": "cd packages/backend && pnpm typeorm migration:run -d ormconfig.js",
"migrate": "cd packages/backend && pnpm migrate",
"migrateandstart": "pnpm migrate && pnpm start",
"gulp": "pnpm exec gulp build",
"watch": "pnpm dev",
@@ -28,8 +28,8 @@
"cy:open": "pnpm cypress open --browser --e2e --config-file=cypress.config.ts",
"cy:run": "pnpm cypress run",
"e2e": "pnpm start-server-and-test start:test http://localhost:61812 cy:run",
"jest": "cd packages/backend && pnpm cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --runInBand",
"jest-and-coverage": "cd packages/backend && pnpm cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --runInBand",
"jest": "cd packages/backend && pnpm jest",
"jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage",
"test": "pnpm jest",
"test-and-coverage": "pnpm jest-and-coverage",
"format": "pnpm exec gulp format",
@@ -54,8 +54,8 @@
"devDependencies": {
"@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1",
"@typescript-eslint/eslint-plugin": "5.50.0",
"@typescript-eslint/parser": "5.50.0",
"@typescript-eslint/eslint-plugin": "5.51.0",
"@typescript-eslint/parser": "5.51.0",
"cross-env": "7.0.3",
"cypress": "12.5.1",
"eslint": "8.33.0",

View File

@@ -0,0 +1,13 @@
export class roleIconBadge1675557528704 {
name = 'roleIconBadge1675557528704'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "role" ADD "iconUrl" character varying(512)`);
await queryRunner.query(`ALTER TABLE "role" ADD "asBadge" boolean NOT NULL DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "asBadge"`);
await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "iconUrl"`);
}
}

View File

@@ -1,6 +1,6 @@
import { DataSource } from 'typeorm';
import { loadConfig } from './built/config.js';
import { entities } from './built/postgre.js';
import { entities } from './built/postgres.js';
const config = loadConfig();

View File

@@ -23,9 +23,9 @@
"@tensorflow/tfjs-node": "4.2.0"
},
"dependencies": {
"@bull-board/api": "4.11.0",
"@bull-board/fastify": "4.11.0",
"@bull-board/ui": "4.11.0",
"@bull-board/api": "4.11.1",
"@bull-board/fastify": "4.11.1",
"@bull-board/ui": "4.11.1",
"@discordapp/twemoji": "14.0.2",
"@fastify/accepts": "4.1.0",
"@fastify/cookie": "8.3.0",
@@ -34,9 +34,9 @@
"@fastify/multipart": "7.4.0",
"@fastify/static": "6.8.0",
"@fastify/view": "7.4.1",
"@nestjs/common": "9.3.1",
"@nestjs/core": "9.3.1",
"@nestjs/testing": "9.3.1",
"@nestjs/common": "9.3.7",
"@nestjs/core": "9.3.7",
"@nestjs/testing": "9.3.7",
"@peertube/http-signature": "1.7.0",
"@sinonjs/fake-timers": "10.0.2",
"accepts": "1.3.8",
@@ -46,7 +46,7 @@
"aws-sdk": "2.1295.0",
"bcryptjs": "2.4.3",
"blurhash": "2.0.4",
"bull": "4.10.2",
"bull": "4.10.3",
"cacheable-lookup": "6.1.0",
"cbor": "8.1.0",
"chalk": "5.2.0",
@@ -90,7 +90,7 @@
"promise-limit": "2.7.0",
"pug": "3.0.2",
"punycode": "2.3.0",
"pureimage": "0.3.15",
"pureimage": "0.3.17",
"qrcode": "1.5.1",
"random-seed": "0.3.0",
"ratelimiter": "3.4.1",
@@ -111,12 +111,12 @@
"stringz": "2.1.0",
"summaly": "2.7.0",
"systeminformation": "5.17.8",
"tinycolor2": "1.5.2",
"tinycolor2": "1.6.0",
"tmp": "0.2.1",
"tsc-alias": "1.8.2",
"tsconfig-paths": "4.1.2",
"twemoji-parser": "14.0.0",
"typeorm": "0.3.11",
"typeorm": "0.3.12",
"typescript": "4.9.5",
"ulid": "2.3.0",
"unzipper": "0.10.11",
@@ -128,10 +128,10 @@
"xev": "3.0.2"
},
"devDependencies": {
"@jest/globals": "29.4.1",
"@jest/globals": "29.4.2",
"@redocly/openapi-core": "1.0.0-beta.123",
"@swc/cli": "0.1.61",
"@swc/core": "1.3.32",
"@swc/core": "1.3.34",
"@swc/jest": "0.2.24",
"@types/accepts": "1.3.5",
"@types/archiver": "5.3.1",
@@ -145,11 +145,11 @@
"@types/ioredis": "4.28.10",
"@types/jest": "29.4.0",
"@types/js-yaml": "4.0.5",
"@types/jsdom": "20.0.1",
"@types/jsdom": "21.1.0",
"@types/jsonld": "1.5.8",
"@types/jsrsasign": "10.5.5",
"@types/mime-types": "2.1.1",
"@types/node": "18.11.18",
"@types/node": "18.13.0",
"@types/node-fetch": "3.0.3",
"@types/nodemailer": "6.4.7",
"@types/oauth": "0.9.1",
@@ -174,13 +174,13 @@
"@types/web-push": "3.3.2",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.50.0",
"@typescript-eslint/parser": "5.50.0",
"@typescript-eslint/eslint-plugin": "5.51.0",
"@typescript-eslint/parser": "5.51.0",
"cross-env": "7.0.3",
"eslint": "8.33.0",
"eslint-plugin-import": "2.27.5",
"execa": "6.1.0",
"jest": "29.4.1",
"jest-mock": "29.4.1"
"jest": "29.4.2",
"jest-mock": "29.4.2"
}
}

View File

@@ -4,7 +4,7 @@ import { DataSource } from 'typeorm';
import { createRedisConnection } from '@/redis.js';
import { DI } from './di-symbols.js';
import { loadConfig } from './config.js';
import { createPostgreDataSource } from './postgre.js';
import { createPostgresDataSource } from './postgres.js';
import { RepositoryModule } from './models/RepositoryModule.js';
import type { Provider, OnApplicationShutdown } from '@nestjs/common';
@@ -18,7 +18,7 @@ const $config: Provider = {
const $db: Provider = {
provide: DI.db,
useFactory: async (config) => {
const db = createPostgreDataSource(config);
const db = createPostgresDataSource(config);
return await db.initialize();
},
inject: [DI.config],

View File

@@ -5,7 +5,7 @@ import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { CreateNotificationService } from '@/core/CreateNotificationService.js';
const ACHIEVEMENT_TYPES = [
export const ACHIEVEMENT_TYPES = [
'notes1',
'notes10',
'notes100',

View File

@@ -60,6 +60,7 @@ export class DownloadService {
retry: {
limit: 0,
},
enableUnixSockets: false,
}).on('response', (res: Got.Response) => {
if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !this.config.proxy && res.ip) {
if (this.isPrivateIp(res.ip)) {

View File

@@ -1,10 +1,10 @@
import { Inject, Injectable } from '@nestjs/common';
import { Brackets } from 'typeorm';
import { Brackets, ObjectLiteral } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { User } from '@/models/entities/User.js';
import type { UserProfilesRepository, FollowingsRepository, ChannelFollowingsRepository, MutedNotesRepository, BlockingsRepository, NoteThreadMutingsRepository, MutingsRepository } from '@/models/index.js';
import type { SelectQueryBuilder } from 'typeorm';
import { bindThis } from '@/decorators.js';
import type { SelectQueryBuilder } from 'typeorm';
@Injectable()
export class QueryService {
@@ -32,7 +32,7 @@ export class QueryService {
) {
}
public makePaginationQuery<T>(q: SelectQueryBuilder<T>, sinceId?: string, untilId?: string, sinceDate?: number, untilDate?: number): SelectQueryBuilder<T> {
public makePaginationQuery<T extends ObjectLiteral>(q: SelectQueryBuilder<T>, sinceId?: string, untilId?: string, sinceDate?: number, untilDate?: number): SelectQueryBuilder<T> {
if (sinceId && untilId) {
q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId });
q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId });

View File

@@ -202,6 +202,19 @@ export class RoleService implements OnApplicationShutdown {
return [...assignedRoles, ...matchedCondRoles];
}
/**
* 指定ユーザーのバッジロール一覧取得
*/
@bindThis
public async getUserBadgeRoles(userId: User['id']) {
const assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId }));
const assignedRoleIds = assigns.map(x => x.roleId);
const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
const assignedBadgeRoles = roles.filter(r => r.asBadge && assignedRoleIds.includes(r.id));
// コンディショナルロールも含めるのは負荷高そうだから一旦無し
return assignedBadgeRoles;
}
@bindThis
public async getUserPolicies(userId: User['id'] | null): Promise<RolePolicies> {
const meta = await this.metaService.fetch();

View File

@@ -89,8 +89,8 @@ export class UserFollowingService {
await this.userBlockingService.unblock(follower, followee);
} else {
// それ以外は単純に例外
if (blocking != null) throw new IdentifiableError('710e8fb0-b8c3-4922-be49-d5d93d8e6a6e', 'blocking');
if (blocked != null) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'blocked');
if (blocking) throw new IdentifiableError('710e8fb0-b8c3-4922-be49-d5d93d8e6a6e', 'blocking');
if (blocked) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'blocked');
}
const followeeProfile = await this.userProfilesRepository.findOneByOrFail({ userId: followee.id });
@@ -353,8 +353,8 @@ export class UserFollowingService {
this.userBlockingService.checkBlocked(followee.id, follower.id),
]);
if (blocking != null) throw new Error('blocking');
if (blocked != null) throw new Error('blocked');
if (blocking) throw new Error('blocking');
if (blocked) throw new Error('blocked');
const followRequest = await this.followRequestsRepository.insert({
id: this.idService.genId(),

View File

@@ -48,6 +48,10 @@ export class ApImageService {
throw new Error('invalid image: url not privided');
}
if (!image.url.startsWith('https://')) {
throw new Error('invalid image: unexpected shcema of url: ' + image.url);
}
this.logger.info(`Creating the Image: ${image.url}`);
const instance = await this.metaService.fetch();

View File

@@ -1,8 +1,7 @@
import { forwardRef, Inject, Injectable } from '@nestjs/common';
import promiseLimit from 'promise-limit';
import { DI } from '@/di-symbols.js';
import type { MessagingMessagesRepository, PollsRepository, EmojisRepository } from '@/models/index.js';
import type { UsersRepository } from '@/models/index.js';
import type { MessagingMessagesRepository, PollsRepository, EmojisRepository, UsersRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
import type { CacheableRemoteUser } from '@/models/entities/User.js';
import type { Note } from '@/models/entities/Note.js';
@@ -18,6 +17,7 @@ import { PollService } from '@/core/PollService.js';
import { StatusError } from '@/misc/status-error.js';
import { UtilityService } from '@/core/UtilityService.js';
import { MessagingService } from '@/core/MessagingService.js';
import { bindThis } from '@/decorators.js';
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
import { ApLoggerService } from '../ApLoggerService.js';
@@ -32,7 +32,6 @@ import { ApQuestionService } from './ApQuestionService.js';
import { ApImageService } from './ApImageService.js';
import type { Resolver } from '../ApResolverService.js';
import type { IObject, IPost } from '../type.js';
import { bindThis } from '@/decorators.js';
@Injectable()
export class ApNoteService {
@@ -133,6 +132,16 @@ export class ApNoteService {
const note: IPost = object;
this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
if (note.id && !note.id.startsWith('https://')) {
throw new Error('unexpected shcema of note.id: ' + note.id);
}
const url = getOneApHrefNullable(note.url);
if (url && !url.startsWith('https://')) {
throw new Error('unexpected shcema of note url: ' + url);
}
this.logger.info(`Creating the Note: ${note.id}`);
@@ -307,7 +316,7 @@ export class ApNoteService {
apEmojis,
poll,
uri: note.id,
url: getOneApHrefNullable(note.url),
url: url,
}, silent);
}

View File

@@ -252,6 +252,12 @@ export class ApPersonService implements OnModuleInit {
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
const url = getOneApHrefNullable(person.url);
if (url && !url.startsWith('https://')) {
throw new Error('unexpected shcema of person url: ' + url);
}
// Create user
let user: IRemoteUser;
try {
@@ -283,7 +289,7 @@ export class ApPersonService implements OnModuleInit {
await transactionalEntityManager.save(new UserProfile({
userId: user.id,
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
url: getOneApHrefNullable(person.url),
url: url,
fields,
birthday: bday ? bday[0] : null,
location: person['vcard:Address'] ?? null,
@@ -425,6 +431,12 @@ export class ApPersonService implements OnModuleInit {
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
const url = getOneApHrefNullable(person.url);
if (url && !url.startsWith('https://')) {
throw new Error('unexpected shcema of person url: ' + url);
}
const updates = {
lastFetchedAt: new Date(),
inbox: person.inbox,
@@ -459,7 +471,7 @@ export class ApPersonService implements OnModuleInit {
}
await this.userProfilesRepository.update({ userId: exist.id }, {
url: getOneApHrefNullable(person.url),
url: url,
fields,
description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
birthday: bday ? bday[0] : null,

View File

@@ -20,6 +20,7 @@ type PackOptions = {
withUser?: boolean,
};
import { bindThis } from '@/decorators.js';
import { isMimeImage } from '@/misc/is-mime-image.js';
@Injectable()
export class DriveFileEntityService {
@@ -82,7 +83,9 @@ export class DriveFileEntityService {
// リモートかつメディアプロキシ
if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) {
return proxiedUrl(file.uri);
if (!(mode === 'static' && file.type.startsWith('video'))) {
return proxiedUrl(file.uri);
}
}
// リモートかつ期限切れはローカルプロキシを試みる
@@ -91,20 +94,19 @@ export class DriveFileEntityService {
if (key && !key.match('/')) { // 古いものはここにオブジェクトストレージキーが入ってるので除外
const url = `${this.config.url}/files/${key}`;
if (mode === 'avatar') return proxiedUrl(url);
if (mode === 'avatar') return proxiedUrl(file.uri);
return url;
}
}
const isImage = file.type && ['image/png', 'image/apng', 'image/gif', 'image/jpeg', 'image/webp', 'image/avif', 'image/svg+xml'].includes(file.type);
if (mode === 'static') {
return file.thumbnailUrl ?? (isImage ? (file.webpublicUrl ?? file.url) : null);
}
const url = file.webpublicUrl ?? file.url;
if (mode === 'avatar') return proxiedUrl(url);
if (mode === 'static') {
return file.thumbnailUrl ?? (isMimeImage(file.type, 'sharp-convertible-image') ? proxiedUrl(url) : null);
}
if (mode === 'avatar') {
return proxiedUrl(url);
}
return url;
}

View File

@@ -56,11 +56,13 @@ export class RoleEntityService {
name: role.name,
description: role.description,
color: role.color,
iconUrl: role.iconUrl,
target: role.target,
condFormula: role.condFormula,
isPublic: role.isPublic,
isAdministrator: role.isAdministrator,
isModerator: role.isModerator,
asBadge: role.asBadge,
canEditMembersByModerator: role.canEditMembersByModerator,
policies: policies,
usersCount: assigns.length,

View File

@@ -415,6 +415,11 @@ export class UserEntityService implements OnModuleInit {
} : undefined) : undefined,
emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
onlineStatus: this.getOnlineStatus(user),
// パフォーマンス上の理由でローカルユーザーのみ
badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then(rs => rs.map(r => ({
name: r.name,
iconUrl: r.iconUrl,
}))) : undefined,
...(opts.detail ? {
url: profile!.url,
@@ -454,6 +459,7 @@ export class UserEntityService implements OnModuleInit {
id: role.id,
name: role.name,
color: role.color,
iconUrl: role.iconUrl,
description: role.description,
isModerator: role.isModerator,
isAdministrator: role.isAdministrator,

View File

@@ -5,7 +5,7 @@
* The getter will return a .bind version of the function
* and memoize the result against a symbol on the instance
*/
export function bindThis(target, key, descriptor) {
export function bindThis(target: any, key: string, descriptor: any) {
let fn = descriptor.value;
if (typeof fn !== 'function') {
@@ -34,7 +34,7 @@ export function bindThis(target, key, descriptor) {
});
return boundFn;
},
set(value) {
set(value: any) {
fn = value;
},
};

View File

@@ -45,7 +45,7 @@ export default class Logger {
}
const time = dateFormat(new Date(), 'HH:mm:ss');
const worker = cluster.isPrimary ? '*' : cluster.worker.id;
const worker = cluster.isPrimary ? '*' : cluster.worker!.id;
const l =
level === 'error' ? important ? chalk.bgRed.white('ERR ') : chalk.red('ERR ') :
level === 'warning' ? chalk.yellow('WARN') :

View File

@@ -51,7 +51,7 @@ export function genIdenticon(seed: string, stream: WriteStream): Promise<void> {
bg.addColorStop(0, bgColors[0]);
bg.addColorStop(1, bgColors[1]);
ctx.fillStyle = bg;
ctx.fillStyle = bg as any;
ctx.beginPath();
ctx.fillRect(0, 0, size, size);

View File

@@ -11,10 +11,9 @@ export class I18n<T extends Record<string, any>> {
// string にしているのは、ドット区切りでのパス指定を許可するため
// なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも
@bindThis
public t(key: string, args?: Record<string, any>): string {
try {
let str = key.split('.').reduce((o, i) => o[i], this.locale) as string;
let str = key.split('.').reduce((o, i) => o[i], this.locale as any) as string;
if (args) {
for (const [k, v] of Object.entries(args)) {

View File

@@ -102,6 +102,11 @@ export class Role {
})
public color: string | null;
@Column('varchar', {
length: 512, nullable: true,
})
public iconUrl: string | null;
@Column('enum', {
enum: ['manual', 'conditional'],
default: 'manual',
@@ -118,6 +123,12 @@ export class Role {
})
public isPublic: boolean;
// trueの場合ユーザー名の横にバッジとして表示
@Column('boolean', {
default: false,
})
public asBadge: boolean;
@Column('boolean', {
default: false,
})

View File

@@ -197,7 +197,7 @@ export const entities = [
const log = process.env.NODE_ENV !== 'production';
export function createPostgreDataSource(config: Config) {
export function createPostgresDataSource(config: Config) {
return new DataSource({
type: 'postgres',
host: config.db.host,

View File

@@ -12,9 +12,9 @@ import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js';
import { createTemp, createTempDir } from '@/misc/create-temp.js';
import { DownloadService } from '@/core/DownloadService.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type Bull from 'bull';
import { bindThis } from '@/decorators.js';
@Injectable()
export class ExportCustomEmojisProcessorService {
@@ -82,6 +82,10 @@ export class ExportCustomEmojisProcessorService {
});
for (const emoji of customEmojis) {
if (!/^[a-zA-Z0-9_]+$/.test(emoji.name)) {
this.logger.error(`invalid emoji name: ${emoji.name}`);
continue;
}
const ext = mime.extension(emoji.type ?? 'image/png');
const fileName = emoji.name + (ext ? '.' + ext : '');
const emojiPath = path + '/' + fileName;

View File

@@ -81,6 +81,10 @@ export class ImportCustomEmojisProcessorService {
for (const record of meta.emojis) {
if (!record.downloaded) continue;
if (!/^[a-zA-Z0-9_]+?([a-zA-Z0-9\.]+)?$/.test(record.fileName)) {
this.logger.error(`invalid filename: ${record.fileName}`);
continue;
}
const emojiInfo = record.emoji;
const emojiPath = outputPath + '/' + record.fileName;
await this.emojisRepository.delete({

View File

@@ -1,3 +1,4 @@
import { IncomingMessage } from 'node:http';
import { Inject, Injectable } from '@nestjs/common';
import fastifyAccepts from '@fastify/accepts';
import httpSignature from '@peertube/http-signature';
@@ -19,6 +20,7 @@ import { QueryService } from '@/core/QueryService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { IActivity } from '@/core/activitypub/type.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
import type { FindOptionsWhere } from 'typeorm';
@@ -97,7 +99,8 @@ export class ActivityPubServerService {
return;
}
this.queueService.inbox(request.body, signature);
// TODO: request.bodyのバリデーション
this.queueService.inbox(request.body as IActivity, signature);
reply.code(202);
}
@@ -413,20 +416,21 @@ export class ActivityPubServerService {
@bindThis
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
fastify.addConstraintStrategy({
// addConstraintStrategy の型定義がおかしいため
(fastify.addConstraintStrategy as any)({
name: 'apOrHtml',
storage() {
const store = {};
const store = {} as any;
return {
get(key) {
get(key: string) {
return store[key] ?? null;
},
set(key, value) {
set(key: string, value: any) {
store[key] = value;
},
};
},
deriveConstraint(request, ctx) {
deriveConstraint(request: IncomingMessage) {
const accepted = accepts(request).type(['html', ACTIVITY_JSON, LD_JSON]);
const isAp = typeof accepted === 'string' && !accepted.match(/html/);
return isAp ? 'ap' : 'html';
@@ -536,6 +540,7 @@ export class ActivityPubServerService {
return (this.apRendererService.renderActivity(this.apRendererService.renderKey(user, keypair)));
} else {
reply.code(400);
return;
}
});

View File

@@ -146,6 +146,8 @@ export class FileServerService {
const url = new URL(`${this.config.mediaProxy}/static.webp`);
url.searchParams.set('url', file.url);
url.searchParams.set('static', '1');
file.cleanup();
return await reply.redirect(301, url.toString());
} else if (file.mime.startsWith('video/')) {
image = await this.videoProcessingService.generateVideoThumbnail(file.path);
@@ -158,6 +160,8 @@ export class FileServerService {
const url = new URL(`${this.config.mediaProxy}/svg.webp`);
url.searchParams.set('url', file.url);
file.cleanup();
return await reply.redirect(301, url.toString());
}
}

View File

@@ -166,6 +166,7 @@ export class ServerService {
return 'Verify succeeded!';
} else {
reply.code(404);
return;
}
});

View File

@@ -34,7 +34,7 @@ export class RateLimiterService {
const min = (): void => {
const minIntervalLimiter = new Limiter({
id: `${actor}:${limitation.key}:min`,
duration: limitation.minInterval * factor,
duration: limitation.minInterval! * factor,
max: 1,
db: this.redisClient,
});
@@ -62,8 +62,8 @@ export class RateLimiterService {
const max = (): void => {
const limiter = new Limiter({
id: `${actor}:${limitation.key}`,
duration: limitation.duration * factor,
max: limitation.max / factor,
duration: limitation.duration! * factor,
max: limitation.max! / factor,
db: this.redisClient,
});

View File

@@ -10,9 +10,9 @@ import { getIpHash } from '@/misc/get-ip-hash.js';
import type { ILocalUser } from '@/models/entities/User.js';
import { IdService } from '@/core/IdService.js';
import { TwoFactorAuthenticationService } from '@/core/TwoFactorAuthenticationService.js';
import { bindThis } from '@/decorators.js';
import { RateLimiterService } from './RateLimiterService.js';
import { SigninService } from './SigninService.js';
import { bindThis } from '@/decorators.js';
import type { FastifyRequest, FastifyReply } from 'fastify';
@Injectable()
@@ -131,7 +131,7 @@ export class SigninApiService {
createdAt: new Date(),
userId: user.id,
ip: request.ip,
headers: request.headers,
headers: request.headers as any,
success: false,
});

View File

@@ -25,7 +25,7 @@ export class SigninService {
}
@bindThis
public signin(request: FastifyRequest, reply: FastifyReply, user: ILocalUser, redirect = false) {
public signin(request: FastifyRequest, reply: FastifyReply, user: ILocalUser) {
setImmediate(async () => {
// Append signin history
const record = await this.signinsRepository.insert({
@@ -33,7 +33,7 @@ export class SigninService {
createdAt: new Date(),
userId: user.id,
ip: request.ip,
headers: request.headers,
headers: request.headers as any,
success: true,
}).then(x => this.signinsRepository.findOneByOrFail(x.identifiers[0]));
@@ -41,25 +41,11 @@ export class SigninService {
this.globalEventService.publishMainStream(user.id, 'signin', await this.signinEntityService.pack(record));
});
if (redirect) {
//#region Cookie
reply.setCookie('igi', user.token!, {
path: '/',
// SEE: https://github.com/koajs/koa/issues/974
// When using a SSL proxy it should be configured to add the "X-Forwarded-Proto: https" header
secure: this.config.url.startsWith('https'),
httpOnly: false,
});
//#endregion
reply.redirect(this.config.url);
} else {
reply.code(200);
return {
id: user.id,
i: user.token,
};
}
reply.code(200);
return {
id: user.id,
i: user.token,
};
}
}

View File

@@ -146,6 +146,7 @@ export class SignupApiService {
`To complete signup, please click this link: ${link}`);
reply.code(204);
return;
} else {
try {
const { account, secret } = await this.signupService.signup({
@@ -162,7 +163,7 @@ export class SignupApiService {
token: secret,
};
} catch (err) {
throw new FastifyReplyError(400, err);
throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString());
}
}
}
@@ -195,7 +196,7 @@ export class SignupApiService {
return this.signinService.signin(request, reply, account as ILocalUser);
} catch (err) {
throw new FastifyReplyError(400, err);
throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString());
}
}
}

View File

@@ -19,11 +19,13 @@ export const paramDef = {
name: { type: 'string' },
description: { type: 'string' },
color: { type: 'string', nullable: true },
target: { type: 'string' },
iconUrl: { type: 'string', nullable: true },
target: { type: 'string', enum: ['manual', 'conditional'] },
condFormula: { type: 'object' },
isPublic: { type: 'boolean' },
isModerator: { type: 'boolean' },
isAdministrator: { type: 'boolean' },
asBadge: { type: 'boolean' },
canEditMembersByModerator: { type: 'boolean' },
policies: {
type: 'object',
@@ -33,11 +35,13 @@ export const paramDef = {
'name',
'description',
'color',
'iconUrl',
'target',
'condFormula',
'isPublic',
'isModerator',
'isAdministrator',
'asBadge',
'canEditMembersByModerator',
'policies',
],
@@ -64,11 +68,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
name: ps.name,
description: ps.description,
color: ps.color,
iconUrl: ps.iconUrl,
target: ps.target,
condFormula: ps.condFormula,
isPublic: ps.isPublic,
isAdministrator: ps.isAdministrator,
isModerator: ps.isModerator,
asBadge: ps.asBadge,
canEditMembersByModerator: ps.canEditMembersByModerator,
policies: ps.policies,
}).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0]));

View File

@@ -27,11 +27,13 @@ export const paramDef = {
name: { type: 'string' },
description: { type: 'string' },
color: { type: 'string', nullable: true },
target: { type: 'string' },
iconUrl: { type: 'string', nullable: true },
target: { type: 'string', enum: ['manual', 'conditional'] },
condFormula: { type: 'object' },
isPublic: { type: 'boolean' },
isModerator: { type: 'boolean' },
isAdministrator: { type: 'boolean' },
asBadge: { type: 'boolean' },
canEditMembersByModerator: { type: 'boolean' },
policies: {
type: 'object',
@@ -42,11 +44,13 @@ export const paramDef = {
'name',
'description',
'color',
'iconUrl',
'target',
'condFormula',
'isPublic',
'isModerator',
'isAdministrator',
'asBadge',
'canEditMembersByModerator',
'policies',
],
@@ -73,11 +77,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
name: ps.name,
description: ps.description,
color: ps.color,
iconUrl: ps.iconUrl,
target: ps.target,
condFormula: ps.condFormula,
isPublic: ps.isPublic,
isModerator: ps.isModerator,
isAdministrator: ps.isAdministrator,
asBadge: ps.asBadge,
canEditMembersByModerator: ps.canEditMembersByModerator,
policies: ps.policies,
});

View File

@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { getJsonSchema } from '@/core/chart/core.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import PerUserPvChart from '@/core/chart/charts/per-user-pv.js';
import { schema } from '@/core/chart/charts/entities/per-user-notes.js';
import { schema } from '@/core/chart/charts/entities/per-user-pv.js';
export const meta = {
tags: ['charts', 'users'],

View File

@@ -27,7 +27,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
return {
params: Object.entries(ep.params.properties ?? {}).map(([k, v]) => ({
name: k,
type: v.type.charAt(0).toUpperCase() + v.type.slice(1),
type: v.type ? v.type.charAt(0).toUpperCase() + v.type.slice(1) : 'string',
})),
};
});

View File

@@ -1,7 +1,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { AchievementService } from '@/core/AchievementService.js';
import { AchievementService, ACHIEVEMENT_TYPES } from '@/core/AchievementService.js';
export const meta = {
requireCredential: true,
@@ -10,7 +10,7 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {
name: { type: 'string' },
name: { type: 'string', enum: ACHIEVEMENT_TYPES },
},
required: ['name'],
} as const;

View File

@@ -15,8 +15,8 @@ export const meta = {
requireCredential: true,
limit: {
duration: 60000,
max: 15,
duration: 30000,
max: 30,
},
kind: 'read:notifications',

View File

@@ -5,8 +5,8 @@ import { IdService } from '@/core/IdService.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { GetterService } from '@/server/api/GetterService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
import { AchievementService } from '@/core/AchievementService.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['notes', 'favorites'],
@@ -79,7 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
userId: me.id,
});
if (note.userHost == null) {
if (note.userHost == null && note.userId !== me.id) {
this.achievementService.create(note.userId, 'myNoteFavorited1');
}
});

View File

@@ -155,7 +155,7 @@ export class ClientServerService {
});
serverAdapter.setBasePath(bullBoardPath);
fastify.register(serverAdapter.registerPlugin(), { prefix: bullBoardPath });
(fastify.register as any)(serverAdapter.registerPlugin(), { prefix: bullBoardPath });
//#endregion
fastify.register(fastifyView, {
@@ -337,7 +337,7 @@ export class ClientServerService {
const renderBase = async (reply: FastifyReply) => {
const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=15');
reply.header('Cache-Control', 'public, max-age=30');
return await reply.view('base', {
img: meta.bannerUrl,
title: meta.name ?? 'Misskey',
@@ -372,6 +372,7 @@ export class ClientServerService {
return feed.atom1();
} else {
reply.code(404);
return;
}
});
@@ -384,6 +385,7 @@ export class ClientServerService {
return feed.rss2();
} else {
reply.code(404);
return;
}
});
@@ -396,6 +398,7 @@ export class ClientServerService {
return feed.json1();
} else {
reply.code(404);
return;
}
});

View File

@@ -35,7 +35,8 @@ html
link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg')
link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg')
link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg')
link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css')
//- https://github.com/misskey-dev/misskey/issues/9842
link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.2.0')
link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
if !config.clientManifestExists

View File

@@ -11,7 +11,7 @@ import FormData from 'form-data';
import { DataSource } from 'typeorm';
import got, { RequestError } from 'got';
import loadConfig from '../src/config/load.js';
import { entities } from '../src/postgre.js';
import { entities } from '@/postgres.js';
import type * as misskey from 'misskey-js';
const _filename = fileURLToPath(import.meta.url);

View File

@@ -12,7 +12,7 @@
"@rollup/plugin-json": "6.0.0",
"@rollup/pluginutils": "5.0.2",
"@syuilo/aiscript": "0.12.4",
"@tabler/icons-webfont": "2.1.2",
"@tabler/icons-webfont": "2.2.0",
"@vitejs/plugin-vue": "4.0.0",
"@vue/compiler-sfc": "3.2.47",
"autobind-decorator": "2.4.0",
@@ -23,7 +23,7 @@
"canvas-confetti": "1.6.0",
"chart.js": "4.2.0",
"chartjs-adapter-date-fns": "3.0.0",
"chartjs-chart-matrix": "1.3.0",
"chartjs-chart-matrix": "2.0.1",
"chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.0.0",
"compare-versions": "5.0.1",
@@ -44,7 +44,7 @@
"punycode": "2.3.0",
"querystring": "0.2.1",
"rndstr": "1.0.0",
"rollup": "3.12.1",
"rollup": "3.14.0",
"s-age": "1.1.2",
"sanitize-html": "2.9.0",
"sass": "1.58.0",
@@ -55,7 +55,7 @@
"textarea-caret": "3.1.0",
"three": "0.149.0",
"throttle-debounce": "5.0.0",
"tinycolor2": "1.5.2",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.2",
"tsconfig-paths": "4.1.2",
"twemoji-parser": "14.0.0",
@@ -74,7 +74,7 @@
"@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1",
"@types/matter-js": "0.18.2",
"@types/node": "18.11.18",
"@types/node": "18.13.0",
"@types/punycode": "2.1.0",
"@types/sanitize-html": "2.8.0",
"@types/seedrandom": "3.0.4",
@@ -83,8 +83,8 @@
"@types/uuid": "9.0.0",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.4",
"@typescript-eslint/eslint-plugin": "5.50.0",
"@typescript-eslint/parser": "5.50.0",
"@typescript-eslint/eslint-plugin": "5.51.0",
"@typescript-eslint/parser": "5.51.0",
"@vue/runtime-core": "3.2.47",
"cross-env": "7.0.3",
"cypress": "12.5.1",

View File

@@ -61,8 +61,6 @@ export async function signout() {
} catch (err) {}
//#endregion
document.cookie = 'igi=; path=/';
if (accounts.length > 0) login(accounts[0].token);
else unisonReload('/');
}

View File

@@ -1,6 +1,6 @@
<!-- eslint-disable vue/no-v-html -->
<template>
<code v-if="inline" :class="`language-${prismLang}`" v-html="html"></code>
<code v-if="inline" :class="`language-${prismLang}`" style="overflow-wrap: anywhere;" v-html="html"></code>
<pre v-else :class="`language-${prismLang}`"><code :class="`language-${prismLang}`" v-html="html"></code></pre>
</template>

View File

@@ -18,7 +18,7 @@
</div>
</Transition>
<div class="container">
<img ref="imgEl" :src="imgUrl" style="display: none;" @load="onImageLoad">
<img ref="imgEl" :src="imgUrl" style="display: none;" crossorigin="anonymous" @load="onImageLoad">
</div>
</div>
</template>

View File

@@ -107,19 +107,19 @@ export default defineComponent({
return () => h(
defaultStore.state.animation ? TransitionGroup : 'div',
{
class: {
[$style['date-separated-list']]: true,
[$style['date-separated-list-nogap']]: props.noGap,
[$style['reversed']]: props.reversed,
[$style['direction-down']]: props.direction === 'down',
[$style['direction-up']]: props.direction === 'up',
},
...(defaultStore.state.animation ? {
name: 'list',
tag: 'div',
onBeforeLeave,
onLeaveCanceled,
} : {}),
class: {
[$style['date-separated-list']]: true,
[$style['date-separated-list-nogap']]: props.noGap,
[$style['reversed']]: props.reversed,
[$style['direction-down']]: props.direction === 'down',
[$style['direction-up']]: props.direction === 'up',
},
...(defaultStore.state.animation ? {
name: 'list',
tag: 'div',
onBeforeLeave,
onLeaveCanceled,
} : {}),
},
{ default: renderChildren });
},
@@ -139,18 +139,10 @@ export default defineComponent({
transition: none !important;
}
> .list-leave-active,
> .list-enter-active {
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
}
> .list-leave-from,
> .list-leave-to,
> .list-leave-active {
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
position: absolute !important;
}
> *:empty {
display: none;
}

View File

@@ -1,6 +1,6 @@
<template>
<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer, asWindow }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
<input ref="searchEl" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @input="input()" @paste.stop="paste" @keyup.enter="done()">
<input ref="searchEl" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" @input="input()" @paste.stop="paste" @keydown.stop.prevent.enter="onEnter">
<div ref="emojisEl" class="emojis">
<section class="result">
<div v-if="searchResultCustom.length > 0" class="body">
@@ -327,6 +327,11 @@ function paste(event: ClipboardEvent): void {
}
}
function onEnter(ev: KeyboardEvent) {
if (ev.isComposing || ev.key === 'Process' || ev.keyCode === 229) return;
done();
}
function done(query?: string): boolean | void {
if (query == null) query = q.value;
if (query == null || typeof query !== 'string') return;

View File

@@ -42,7 +42,7 @@ import { i18n } from '@/i18n';
const props = defineProps<{
modelValue: string | number;
type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search';
type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search' | 'datetime-local';
required?: boolean;
readonly?: boolean;
disabled?: boolean;

View File

@@ -6,15 +6,14 @@
<span>{{ $ts.clickToShow }}</span>
</div>
<div v-else-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" class="audio">
<audio
ref="audioEl"
class="audio"
:src="media.url"
:title="media.name"
controls
preload="metadata"
@volumechange="volumechange"
/>
<VuePlyr :options="{ volume: 0.5 }">
<audio controls preload="metadata">
<source
:src="media.url"
:type="media.type"
/>
</audio>
</VuePlyr>
</div>
<a
v-else class="download"
@@ -31,7 +30,9 @@
<script lang="ts" setup>
import { onMounted } from 'vue';
import * as misskey from 'misskey-js';
import VuePlyr from 'vue-plyr';
import { ColdDeviceStorage } from '@/store';
import 'vue-plyr/dist/vue-plyr.css';
const props = withDefaults(defineProps<{
media: misskey.entities.DriveFile;
@@ -55,7 +56,11 @@ onMounted(() => {
width: 100%;
border-radius: 4px;
margin-top: 4px;
overflow: hidden;
overflow: clip;
--plyr-color-main: var(--accent);
--plyr-audio-controls-background: var(--bg);
--plyr-audio-controls-color: var(--accentLighten);
> .download,
> .sensitive {
@@ -93,10 +98,8 @@ onMounted(() => {
}
> .audio {
.audio {
display: block;
width: 100%;
}
border-radius: 8px;
overflow: clip;
}
}
</style>

View File

@@ -1,22 +1,23 @@
<template>
<div v-if="hide" class="qjewsnkg" @click="hide = false">
<ImgWithBlurhash class="bg" :hash="image.blurhash" :title="image.comment" :alt="image.comment"/>
<div class="text">
<div class="wrapper">
<div v-if="hide" :class="$style.hidden" @click="hide = false">
<ImgWithBlurhash style="filter: brightness(0.5);" :hash="image.blurhash" :title="image.comment" :alt="image.comment"/>
<div :class="$style.hiddenText">
<div :class="$style.hiddenTextWrapper">
<b style="display: block;"><i class="ti ti-alert-triangle"></i> {{ $ts.sensitive }}</b>
<span style="display: block;">{{ $ts.clickToShow }}</span>
</div>
</div>
</div>
<div v-else class="gqnyydlz">
<div v-else :class="$style.visible">
<a
:class="$style.imageContainer"
:href="image.url"
:title="image.name"
>
<ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.comment || image.name" :title="image.comment || image.name" :cover="false"/>
<div v-if="image.type === 'image/gif'" class="gif">GIF</div>
<div v-if="image.type === 'image/gif'" :class="$style.gif">GIF</div>
</a>
<button v-tooltip="$ts.hide" class="_button hide" @click="hide = true"><i class="ti ti-eye-off"></i></button>
<button v-tooltip="$ts.hide" :class="$style.hide" class="_button" @click="hide = true"><i class="ti ti-eye-off"></i></button>
</div>
</template>
@@ -49,82 +50,77 @@ watch(() => props.image, () => {
});
</script>
<style lang="scss" scoped>
.qjewsnkg {
<style lang="scss" module>
.hidden {
position: relative;
> .bg {
filter: brightness(0.5);
}
> .text {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 1;
display: flex;
justify-content: center;
align-items: center;
> .wrapper {
display: table-cell;
text-align: center;
font-size: 0.8em;
color: #fff;
}
}
}
.gqnyydlz {
.hiddenText {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 1;
display: flex;
justify-content: center;
align-items: center;
}
.hiddenTextWrapper {
display: table-cell;
text-align: center;
font-size: 0.8em;
color: #fff;
}
.visible {
position: relative;
//box-shadow: 0 0 0 1px var(--divider) inset;
background: var(--bg);
--c: rgb(0 0 0 / 2%);
background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%);
background-size: 16px 16px;
}
> .hide {
display: block;
position: absolute;
border-radius: 6px;
background-color: var(--accentedBg);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
color: var(--accent);
font-size: 0.8em;
padding: 6px 8px;
text-align: center;
top: 12px;
right: 12px;
.hide {
display: block;
position: absolute;
border-radius: 6px;
background-color: var(--accentedBg);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
color: var(--accent);
font-size: 0.8em;
padding: 6px 8px;
text-align: center;
top: 12px;
right: 12px;
}
> i {
display: block;
}
}
.imageContainer {
display: block;
cursor: zoom-in;
overflow: hidden;
width: 100%;
height: 100%;
background-position: center;
background-size: contain;
background-repeat: no-repeat;
}
> a {
display: block;
cursor: zoom-in;
overflow: hidden;
width: 100%;
height: 100%;
background-position: center;
background-size: contain;
background-repeat: no-repeat;
> .gif {
background-color: var(--fg);
border-radius: 6px;
color: var(--accentLighten);
display: inline-block;
font-size: 14px;
font-weight: bold;
left: 12px;
opacity: .5;
padding: 0 6px;
text-align: center;
top: 12px;
pointer-events: none;
}
}
.gif {
background-color: var(--fg);
border-radius: 6px;
color: var(--accentLighten);
display: inline-block;
font-size: 14px;
font-weight: bold;
left: 12px;
opacity: .5;
padding: 0 6px;
text-align: center;
top: 12px;
pointer-events: none;
}
</style>

View File

@@ -1,11 +1,11 @@
<template>
<div class="hoawjimk">
<div>
<XBanner v-for="media in mediaList.filter(media => !previewable(media))" :key="media.id" :media="media"/>
<div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container">
<div ref="gallery" :data-count="mediaList.filter(media => previewable(media)).length">
<div v-if="mediaList.filter(media => previewable(media)).length > 0" :class="$style.container">
<div ref="gallery" :class="[$style.medias, count <= 4 ? $style['n' + count] : $style.nMany]">
<template v-for="media in mediaList.filter(media => previewable(media))">
<XVideo v-if="media.type.startsWith('video')" :key="media.id" :video="media"/>
<XImage v-else-if="media.type.startsWith('image')" :key="media.id" class="image" :data-id="media.id" :image="media" :raw="raw"/>
<XVideo v-if="media.type.startsWith('video')" :key="media.id" :class="$style.media" :video="media"/>
<XImage v-else-if="media.type.startsWith('image')" :key="media.id" :class="$style.media" class="image" :data-id="media.id" :image="media" :raw="raw"/>
</template>
</div>
</div>
@@ -32,6 +32,7 @@ const props = defineProps<{
const gallery = ref(null);
const pswpZIndex = os.claimZIndex('middle');
const count = $computed(() => props.mediaList.filter(media => previewable(media)).length);
onMounted(() => {
const lightbox = new PhotoSwipeLightbox({
@@ -122,82 +123,61 @@ const previewable = (file: misskey.entities.DriveFile): boolean => {
};
</script>
<style lang="scss" scoped>
.hoawjimk {
> .gird-container {
position: relative;
width: 100%;
margin-top: 4px;
<style lang="scss" module>
.container {
position: relative;
width: 100%;
margin-top: 4px;
}
&:before {
content: '';
display: block;
padding-top: 56.25% // 16:9;
.medias {
display: grid;
grid-gap: 8px;
&.n1 {
aspect-ratio: 16/9;
grid-template-rows: 1fr;
}
&.n2 {
aspect-ratio: 16/9;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr;
}
&.n3 {
aspect-ratio: 16/9;
grid-template-columns: 1fr 0.5fr;
grid-template-rows: 1fr 1fr;
> .media:nth-child(1) {
grid-row: 1 / 3;
}
> div {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: grid;
grid-gap: 8px;
> * {
overflow: hidden;
border-radius: 6px;
}
&[data-count="1"] {
grid-template-rows: 1fr;
}
&[data-count="2"] {
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr;
}
&[data-count="3"] {
grid-template-columns: 1fr 0.5fr;
grid-template-rows: 1fr 1fr;
> *:nth-child(1) {
grid-row: 1 / 3;
}
> *:nth-child(3) {
grid-column: 2 / 3;
grid-row: 2 / 3;
}
}
&[data-count="4"] {
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
}
> *:nth-child(1) {
grid-column: 1 / 2;
grid-row: 1 / 2;
}
> *:nth-child(2) {
grid-column: 2 / 3;
grid-row: 1 / 2;
}
> *:nth-child(3) {
grid-column: 1 / 2;
grid-row: 2 / 3;
}
> *:nth-child(4) {
grid-column: 2 / 3;
grid-row: 2 / 3;
}
> .media:nth-child(3) {
grid-column: 2 / 3;
grid-row: 2 / 3;
}
}
&.n4 {
aspect-ratio: 16/9;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
}
&.nMany {
grid-template-columns: 1fr 1fr;
> .media {
aspect-ratio: 16/9;
}
}
}
.media {
overflow: hidden; // clipにするとバグる
border-radius: 8px;
}
</style>

View File

@@ -6,7 +6,7 @@
</div>
</div>
<div v-else class="kkjnbbplepmiyuadieoenjgutgcmtsvu">
<vue-plyr>
<VuePlyr :options="{ volume: 0.5 }">
<video
controls
:data-poster="video.thumbnailUrl"
@@ -17,7 +17,7 @@
:type="video.type"
/>
</video>
</vue-plyr>
</VuePlyr>
<i class="ti ti-eye-off" @click="hide = true"></i>
</div>
</template>

View File

@@ -48,7 +48,7 @@
<div :class="$style.text">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<MkA v-if="appearNote.replyId" :class="$style.replyIcon" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
<Mfm v-if="appearNote.text" v-once :text="appearNote.text" :author="appearNote.user" :i="$i" :emoji-urls="appearNote.emojis"/>
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :emoji-urls="appearNote.emojis"/>
<div v-if="translating || translation" :class="$style.translation">
<MkLoading v-if="translating" mini/>
<div v-else :class="$style.translated">

View File

@@ -5,6 +5,9 @@
</MkA>
<div v-if="note.user.isBot" :class="$style.isBot">bot</div>
<div :class="$style.username"><MkAcct :user="note.user"/></div>
<div v-if="note.user.badgeRoles" :class="$style.badgeRoles">
<img v-for="role in note.user.badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl"/>
</div>
<div :class="$style.info">
<MkA :to="notePage(note)">
<MkTime :time="note.createdAt"/>
@@ -77,4 +80,17 @@ defineProps<{
margin-left: auto;
font-size: 0.9em;
}
.badgeRoles {
margin: 0 .5em 0 0;
}
.badgeRole {
height: 1.3em;
vertical-align: -20%;
& + .badgeRole {
margin-left: .125em;
}
}
</style>

View File

@@ -63,10 +63,23 @@
<MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements">
{{ i18n.ts._achievements._types['_' + notification.achievement].title }}
</MkA>
<span v-else-if="notification.type === 'follow'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span>
<template v-else-if="notification.type === 'follow'">
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span>
<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div>
</template>
<span v-else-if="notification.type === 'followRequestAccepted'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span>
<span v-else-if="notification.type === 'receiveFollowRequest'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ i18n.ts.reject }}</button></div></span>
<span v-else-if="notification.type === 'groupInvited'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.groupInvited }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ i18n.ts.reject }}</button></div></span>
<template v-else-if="notification.type === 'receiveFollowRequest'">
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}</span>
<div v-if="full && !followRequestDone">
<button class="_textButton" @click="acceptFollowRequest()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ i18n.ts.reject }}</button>
</div>
</template>
<template v-else-if="notification.type === 'groupInvited'">
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.groupInvited }}: <b>{{ notification.invitation.group.name }}</b></span>
<div v-if="full && !groupInviteDone">
<button class="_textButton" @click="acceptGroupInvitation()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ i18n.ts.reject }}</button>
</div>
</template>
<span v-else-if="notification.type === 'app'" :class="$style.text">
<Mfm :text="notification.body" :nowrap="false"/>
</span>
@@ -202,7 +215,7 @@ useTooltip(reactionRef, (showing) => {
border-radius: 100%;
background: var(--panel);
box-shadow: 0 0 0 3px var(--panel);
font-size: 12px;
font-size: 11px;
text-align: center;
color: #fff;

View File

@@ -4,7 +4,7 @@
<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span>
<MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
<Mfm v-if="note.text" v-once :text="note.text" :author="note.user" :i="$i" :emoji-urls="note.emojis"/>
<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :emoji-urls="note.emojis"/>
<MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
</div>
<details v-if="note.files.length > 0">

View File

@@ -195,7 +195,7 @@ export default defineComponent({
return h(MkSparkle, {}, genEl(token.children));
}
case 'rotate': {
const degrees = parseFloat(token.props.args.deg) ?? '90';
const degrees = parseFloat(token.props.args.deg ?? '90');
style = `transform: rotate(${degrees}deg); transform-origin: center center;`;
break;
}

View File

@@ -438,7 +438,7 @@ if ($i) {
}
window.setInterval(() => {
if (Math.floor(Math.random() * 10000) === 0) {
if (Math.floor(Math.random() * 20000) === 0) {
claimAchievement('justPlainLucky');
}
}, 1000 * 10);

View File

@@ -35,6 +35,9 @@ export const apiWithDialog = ((
} else if (err.code.startsWith('TOO_MANY')) {
title = i18n.ts.youCannotCreateAnymore;
text = `${i18n.ts.error}: ${err.id}`;
} else if (err.message.startsWith('Unexpected token')) {
title = i18n.ts.gotInvalidResponseError;
text = i18n.ts.gotInvalidResponseErrorDescription;
}
alert({
type: 'error',

View File

@@ -73,7 +73,13 @@
</FormSection>
<FormSection>
<template #label><Mfm text="$[jelly ❤]"/> {{ i18n.ts._aboutMisskey.patrons }}</template>
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); grid-gap: 12px;">
<div :class="$style.patronsWithIcon">
<div v-for="patron in patronsWithIcon" :class="$style.patronWithIcon">
<img :src="patron.icon" :class="$style.patronIcon">
<span :class="$style.patronName">{{ patron.name }}</span>
</div>
</div>
<div style="margin-top: 16px; display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); grid-gap: 12px;">
<div v-for="patron in patrons" :key="patron">{{ patron }}</div>
</div>
<p>{{ i18n.ts._aboutMisskey.morePatrons }}</p>
@@ -99,6 +105,14 @@ import { definePageMetadata } from '@/scripts/page-metadata';
import { claimAchievement, claimedAchievements } from '@/scripts/achievements';
import { $i } from '@/account';
const patronsWithIcon = [{
name: 'カイヤン',
icon: 'https://misskey-hub.net/patrons/a2820716883e408cb87773e377ce7c8d.jpg',
}, {
name: 'だれかさん',
icon: 'https://misskey-hub.net/patrons/f7409b5e5a88477a9b9d740c408de125.jpg',
}];
const patrons = [
'まっちゃとーにゅ',
'mametsuko',
@@ -352,4 +366,27 @@ definePageMetadata({
.contributorUsername {
margin-left: 12px;
}
.patronsWithIcon {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
grid-gap: 12px;
}
.patronWithIcon {
display: flex;
align-items: center;
padding: 12px;
background: var(--buttonBg);
border-radius: 6px;
}
.patronIcon {
width: 24px;
border-radius: 100%;
}
.patronName {
margin-left: 12px;
}
</style>

View File

@@ -29,7 +29,7 @@
<MkInput v-model="ad.ratio" type="number">
<template #label>{{ i18n.ts.ratio }}</template>
</MkInput>
<MkInput v-model="ad.expiresAt" type="date">
<MkInput v-model="ad.expiresAt" type="datetime-local">
<template #label>{{ i18n.ts.expiration }}</template>
</MkInput>
</FormSplit>
@@ -61,7 +61,12 @@ import { definePageMetadata } from '@/scripts/page-metadata';
let ads: any[] = $ref([]);
os.api('admin/ad/list').then(adsResponse => {
ads = adsResponse;
ads = adsResponse.map(r => {
return {
...r,
expiresAt: new Date(r.expiresAt).toISOString().slice(0, 16),
};
});
});
function add() {

View File

@@ -13,6 +13,10 @@
<template #caption>#RRGGBB</template>
</MkInput>
<MkInput v-model="iconUrl">
<template #label>{{ i18n.ts._role.iconUrl }}</template>
</MkInput>
<MkSelect v-model="rolePermission" :readonly="readonly">
<template #label><i class="ti ti-shield-lock"></i> {{ i18n.ts._role.permission }}</template>
<template #caption><div v-html="i18n.ts._role.descriptionOfPermission.replaceAll('\n', '<br>')"></div></template>
@@ -35,6 +39,21 @@
</div>
</MkFolder>
<MkSwitch v-model="canEditMembersByModerator" :readonly="readonly">
<template #label>{{ i18n.ts._role.canEditMembersByModerator }}</template>
<template #caption>{{ i18n.ts._role.descriptionOfCanEditMembersByModerator }}</template>
</MkSwitch>
<MkSwitch v-model="isPublic" :readonly="readonly">
<template #label>{{ i18n.ts._role.isPublic }}</template>
<template #caption>{{ i18n.ts._role.descriptionOfIsPublic }}</template>
</MkSwitch>
<MkSwitch v-model="asBadge" :readonly="readonly">
<template #label>{{ i18n.ts._role.asBadge }}</template>
<template #caption>{{ i18n.ts._role.descriptionOfAsBadge }}</template>
</MkSwitch>
<FormSlot>
<template #label><i class="ti ti-license"></i> {{ i18n.ts._role.policies }}</template>
<div class="_gaps_s">
@@ -358,16 +377,6 @@
</div>
</FormSlot>
<MkSwitch v-model="canEditMembersByModerator" :readonly="readonly">
<template #label>{{ i18n.ts._role.canEditMembersByModerator }}</template>
<template #caption>{{ i18n.ts._role.descriptionOfCanEditMembersByModerator }}</template>
</MkSwitch>
<MkSwitch v-model="isPublic" :readonly="readonly">
<template #label>{{ i18n.ts._role.isPublic }}</template>
<template #caption>{{ i18n.ts._role.descriptionOfIsPublic }}</template>
</MkSwitch>
<div v-if="!readonly" class="_buttons">
<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ role ? i18n.ts.save : i18n.ts.create }}</MkButton>
</div>
@@ -426,9 +435,11 @@ let name = $ref(role?.name ?? 'New Role');
let description = $ref(role?.description ?? '');
let rolePermission = $ref(role?.isAdministrator ? 'administrator' : role?.isModerator ? 'moderator' : 'normal');
let color = $ref(role?.color ?? null);
let iconUrl = $ref(role?.iconUrl ?? null);
let target = $ref(role?.target ?? 'manual');
let condFormula = $ref(role?.condFormula ?? { id: uuid(), type: 'isRemote' });
let isPublic = $ref(role?.isPublic ?? false);
let asBadge = $ref(role?.asBadge ?? false);
let canEditMembersByModerator = $ref(role?.canEditMembersByModerator ?? false);
const policies = reactive<Record<typeof ROLE_POLICIES[number], { useDefault: boolean; priority: number; value: any; }>>({});
@@ -466,11 +477,13 @@ async function save() {
name,
description,
color: color === '' ? null : color,
iconUrl: iconUrl === '' ? null : iconUrl,
target,
condFormula,
isAdministrator: rolePermission === 'administrator',
isModerator: rolePermission === 'moderator',
isPublic,
asBadge,
canEditMembersByModerator,
policies,
});
@@ -480,11 +493,13 @@ async function save() {
name,
description,
color: color === '' ? null : color,
iconUrl: iconUrl === '' ? null : iconUrl,
target,
condFormula,
isAdministrator: rolePermission === 'administrator',
isModerator: rolePermission === 'moderator',
isPublic,
asBadge,
canEditMembersByModerator,
policies,
});

View File

@@ -77,6 +77,8 @@ export default defineComponent({
accepted() {
this.state = 'accepted';
if (this.session.app.callbackUrl) {
const url = new URL(this.session.app.callbackUrl);
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(url.protocol)) throw new Error('invalid url');
location.href = `${this.session.app.callbackUrl}?token=${this.session.token}`;
}
}, onLogin(res) {

View File

@@ -23,7 +23,7 @@
</div>
</div>
<XPostForm v-if="$i" :channel="channel" class="post-form _panel _margin" fixed/>
<MkPostForm v-if="$i" :channel="channel" class="post-form _panel _margin" fixed/>
<XTimeline :key="channelId" class="_margin" src="channel" :channel="channelId" @before="before" @after="after"/>
</div>
@@ -34,7 +34,7 @@
<script lang="ts" setup>
import { computed, inject, watch } from 'vue';
import MkContainer from '@/components/MkContainer.vue';
import XPostForm from '@/components/MkPostForm.vue';
import MkPostForm from '@/components/MkPostForm.vue';
import XTimeline from '@/components/MkTimeline.vue';
import XChannelFollowButton from '@/components/MkChannelFollowButton.vue';
import * as os from '@/os';

View File

@@ -155,7 +155,11 @@ async function run() {
os.inputText({
title: q,
}).then(({ canceled, result: a }) => {
ok(a);
if (canceled) {
ok('');
} else {
ok(a);
}
});
});
},

View File

@@ -70,7 +70,7 @@ async function accept(): Promise<void> {
state = 'accepted';
if (props.callback) {
const cbUrl = new URL(props.callback);
if (!['http:', 'https:'].includes(cbUrl.protocol)) throw new Error('invalid url');
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(cbUrl.protocol)) throw new Error('invalid url');
cbUrl.searchParams.set('session', props.session);
location.href = cbUrl.href;
}

View File

@@ -86,7 +86,11 @@ async function run() {
os.inputText({
title: q,
}).then(({ canceled, result: a }) => {
ok(a);
if (canceled) {
ok('');
} else {
ok(a);
}
});
});
},

View File

@@ -91,7 +91,7 @@ function regenerateToken() {
type: 'password',
}).then(({ canceled, result: password }) => {
if (canceled) return;
os.api('i/regenerate_token', {
os.api('i/regenerate-token', {
password: password,
});
});

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