Compare commits
57 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
03744a25ed | ||
![]() |
eac3bf8bff | ||
![]() |
2e1fbb5b16 | ||
![]() |
98b3517d36 | ||
![]() |
dee662705e | ||
![]() |
0da0cc80b9 | ||
![]() |
650187deaf | ||
![]() |
2e565cac2c | ||
![]() |
ac7537278c | ||
![]() |
f9a2e98831 | ||
![]() |
54f789bd55 | ||
![]() |
5ac9d13516 | ||
![]() |
2be1a39d13 | ||
![]() |
f3c5edc852 | ||
![]() |
30704e6de8 | ||
![]() |
41932ac409 | ||
![]() |
9843c596d8 | ||
![]() |
baf65bfa69 | ||
![]() |
6501f80fc7 | ||
![]() |
b037f6566b | ||
![]() |
0ec8ebeba3 | ||
![]() |
af1c9251fc | ||
![]() |
4ad399c593 | ||
![]() |
55a9646f23 | ||
![]() |
46017f5725 | ||
![]() |
c20ce12f86 | ||
![]() |
1e28db2396 | ||
![]() |
5f3640c7fd | ||
![]() |
d65e5f6794 | ||
![]() |
e67d7bc0ea | ||
![]() |
1139632f95 | ||
![]() |
b51a8c3f82 | ||
![]() |
0d7256678e | ||
![]() |
eea33d07fd | ||
![]() |
f599337320 | ||
![]() |
7df019db0e | ||
![]() |
04f92bd688 | ||
![]() |
505ecf6c1f | ||
![]() |
c9ec08704e | ||
![]() |
6a3039f7b7 | ||
![]() |
868c8fffb3 | ||
![]() |
faed3b438e | ||
![]() |
6c982629ea | ||
![]() |
110bbbc7dc | ||
![]() |
4ad0345f20 | ||
![]() |
9d84214462 | ||
![]() |
3f199c7113 | ||
![]() |
e9417fb741 | ||
![]() |
ee74df6823 | ||
![]() |
26630bae81 | ||
![]() |
9bde9edcf6 | ||
![]() |
a12f07c42b | ||
![]() |
e7334c4fb0 | ||
![]() |
38f9d1e764 | ||
![]() |
2dfed75402 | ||
![]() |
0c12e80106 | ||
![]() |
b7522f69e7 |
@@ -130,6 +130,7 @@ proxyBypassHosts:
|
||||
#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5
|
||||
|
||||
# Media Proxy
|
||||
# Reference Implementation: https://github.com/misskey-dev/media-proxy
|
||||
#mediaProxy: https://example.com/proxy
|
||||
|
||||
# Proxy remote files (default: false)
|
||||
|
@@ -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
3
.dockleignore
Normal file
@@ -0,0 +1,3 @@
|
||||
DKL-DI-0005
|
||||
DKL-DI-0006
|
||||
DKL-LI-0003
|
2
.github/workflows/docker-develop.yml
vendored
2
.github/workflows/docker-develop.yml
vendored
@@ -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
30
.github/workflows/dockle.yml
vendored
Normal 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}"
|
48
CHANGELOG.md
48
CHANGELOG.md
@@ -8,6 +8,54 @@
|
||||
|
||||
You should also include the user name that made the change.
|
||||
-->
|
||||
## 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)
|
||||
|
||||
### Bugfixes
|
||||
- Server: improve security
|
||||
|
||||
## 13.3.2 (2023/02/04)
|
||||
|
||||
### Improvements
|
||||
- 外部メディアプロキシへの対応を強化しました
|
||||
外部メディアプロキシのFastify実装を作りました
|
||||
https://github.com/misskey-dev/media-proxy
|
||||
- Server: improve performance
|
||||
|
||||
### Bugfixes
|
||||
- Client: validate urls to improve security
|
||||
|
||||
## 13.3.1 (2023/02/04)
|
||||
|
||||
|
@@ -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`.
|
||||
|
||||
|
@@ -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"]
|
||||
|
4
healthcheck.sh
Normal file
4
healthcheck.sh
Normal 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}"
|
@@ -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"
|
||||
|
@@ -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"
|
||||
|
@@ -1195,6 +1195,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"
|
||||
|
@@ -34,6 +34,7 @@ const languages = [
|
||||
'pt-PT',
|
||||
'ru-RU',
|
||||
'sk-SK',
|
||||
'th-TH',
|
||||
'ug-CN',
|
||||
'uk-UA',
|
||||
'vi-VN',
|
||||
|
@@ -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à"
|
||||
|
@@ -1148,7 +1148,7 @@ _achievements:
|
||||
description: "ここをクリックした"
|
||||
_justPlainLucky:
|
||||
title: "単なるラッキー"
|
||||
description: "10秒ごとに0.01%の確率で獲得"
|
||||
description: "10秒ごとに0.005%の確率で獲得"
|
||||
_setNameToSyuilo:
|
||||
title: "神様コンプレックス"
|
||||
description: "名前を syuilo に設定した"
|
||||
@@ -1184,7 +1184,7 @@ _role:
|
||||
description: "ロールの説明"
|
||||
permission: "ロールの権限"
|
||||
descriptionOfPermission: "<b>モデレーター</b>は基本的なモデレーションに関する操作を行えます。\n<b>管理者</b>はインスタンスの全ての設定を変更できます。"
|
||||
assignTarget: "アサインターゲット"
|
||||
assignTarget: "アサイン"
|
||||
descriptionOfAssignTarget: "<b>マニュアル</b>は誰がこのロールに含まれるかを手動で管理します。\n<b>コンディショナル</b>は条件を設定し、それに合致するユーザーが自動で含まれるようになります。"
|
||||
manual: "マニュアル"
|
||||
conditional: "コンディショナル"
|
||||
@@ -1197,6 +1197,9 @@ _role:
|
||||
baseRole: "ベースロール"
|
||||
useBaseValue: "ベースロールの値を使用"
|
||||
chooseRoleToAssign: "アサインするロールを選択"
|
||||
iconUrl: "アイコン画像のURL"
|
||||
asBadge: "バッジとして表示"
|
||||
descriptionOfAsBadge: "オンにすると、ユーザー名の横にロールのアイコンが表示されます。"
|
||||
canEditMembersByModerator: "モデレーターのメンバー編集を許可"
|
||||
descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。"
|
||||
priority: "優先度"
|
||||
|
72
locales/lo-LA.yml
Normal file
72
locales/lo-LA.yml
Normal 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: "ເສັ້ນກຳນົດເວລາ"
|
@@ -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: "ลำดับความสำคัญ"
|
||||
|
@@ -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: "Після написання вмісту ви можете опублікувати його, натиснувши кнопку у верхньому правому куті форми."
|
||||
|
@@ -1023,17 +1023,23 @@ _achievements:
|
||||
title: "定期联系Ⅲ"
|
||||
description: "总登录天数400天"
|
||||
_login500:
|
||||
title: "老熟人Ⅰ"
|
||||
description: "总登录天数500天"
|
||||
flavor: "诸君,我喜欢贴文"
|
||||
_login600:
|
||||
title: "老熟人Ⅱ"
|
||||
description: "总登录天数600天"
|
||||
_login700:
|
||||
title: "老熟人Ⅲ"
|
||||
description: "总登录天数700天"
|
||||
_login800:
|
||||
title: "帖子大师Ⅰ"
|
||||
description: "总登录天数800天"
|
||||
_login900:
|
||||
title: "帖子大师Ⅱ"
|
||||
description: "总登录天数900天"
|
||||
_login1000:
|
||||
title: "帖子大师Ⅲ"
|
||||
description: "总登录天数1000天"
|
||||
flavor: "感谢您使用Misskey!"
|
||||
_noteClipped1:
|
||||
@@ -1086,6 +1092,7 @@ _achievements:
|
||||
title: "信号塔"
|
||||
description: "拥有超过500名关注者"
|
||||
_followers1000:
|
||||
title: "大影响家"
|
||||
description: "拥有超过1000名关注者"
|
||||
_collectAchievements30:
|
||||
title: "成就收藏家"
|
||||
@@ -1188,6 +1195,9 @@ _role:
|
||||
baseRole: "基本角色"
|
||||
useBaseValue: "使用基本角色的值"
|
||||
chooseRoleToAssign: "选择要分配的角色"
|
||||
iconUrl: "图标URL"
|
||||
asBadge: "作为徽章显示"
|
||||
descriptionOfAsBadge: "开启后,用户名旁边将会出现角色图标。"
|
||||
canEditMembersByModerator: "允许监察者编辑成员"
|
||||
descriptionOfCanEditMembersByModerator: "如果选中,监察者和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。"
|
||||
priority: "优先级"
|
||||
|
@@ -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 旗標"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "13.3.1",
|
||||
"version": "13.5.0",
|
||||
"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",
|
||||
|
13
packages/backend/migration/1675557528704-role-icon-badge.js
Normal file
13
packages/backend/migration/1675557528704-role-icon-badge.js
Normal 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"`);
|
||||
}
|
||||
}
|
@@ -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();
|
||||
|
||||
|
@@ -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],
|
||||
|
@@ -87,6 +87,8 @@ export type Mixin = {
|
||||
userAgent: string;
|
||||
clientEntry: string;
|
||||
clientManifestExists: boolean;
|
||||
mediaProxy: string;
|
||||
externalMediaProxyEnabled: boolean;
|
||||
};
|
||||
|
||||
export type Config = Source & Mixin;
|
||||
@@ -135,6 +137,13 @@ export function loadConfig() {
|
||||
mixin.clientEntry = clientManifest['src/init.ts'];
|
||||
mixin.clientManifestExists = clientManifestExists;
|
||||
|
||||
const externalMediaProxy = config.mediaProxy ?
|
||||
config.mediaProxy.endsWith('/') ? config.mediaProxy.substring(0, config.mediaProxy.length - 1) : config.mediaProxy
|
||||
: null;
|
||||
const internalMediaProxy = `${mixin.scheme}://${mixin.host}/proxy`;
|
||||
mixin.mediaProxy = externalMediaProxy ?? internalMediaProxy;
|
||||
mixin.externalMediaProxyEnabled = externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy;
|
||||
|
||||
if (!config.redis.prefix) config.redis.prefix = mixin.host;
|
||||
|
||||
return Object.assign(config, mixin);
|
||||
|
@@ -10,10 +10,9 @@ import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { PushNotificationService } from '@/core/PushNotificationService.js';
|
||||
import * as Acct from '@/misc/acct.js';
|
||||
import { Cache } from '@/misc/cache.js';
|
||||
import type { Packed } from '@/misc/schema.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { MutingsRepository, BlockingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/index.js';
|
||||
import type { MutingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/index.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
||||
@@ -23,7 +22,6 @@ import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
export class AntennaService implements OnApplicationShutdown {
|
||||
private antennasFetched: boolean;
|
||||
private antennas: Antenna[];
|
||||
private blockingCache: Cache<User['id'][]>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redisSubscriber)
|
||||
@@ -32,9 +30,6 @@ export class AntennaService implements OnApplicationShutdown {
|
||||
@Inject(DI.mutingsRepository)
|
||||
private mutingsRepository: MutingsRepository,
|
||||
|
||||
@Inject(DI.blockingsRepository)
|
||||
private blockingsRepository: BlockingsRepository,
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
@@ -52,14 +47,13 @@ export class AntennaService implements OnApplicationShutdown {
|
||||
|
||||
private utilityService: UtilityService,
|
||||
private idService: IdService,
|
||||
private globalEventServie: GlobalEventService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private pushNotificationService: PushNotificationService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
private antennaEntityService: AntennaEntityService,
|
||||
) {
|
||||
this.antennasFetched = false;
|
||||
this.antennas = [];
|
||||
this.blockingCache = new Cache<User['id'][]>(1000 * 60 * 5);
|
||||
|
||||
this.redisSubscriber.on('message', this.onRedisMessage);
|
||||
}
|
||||
@@ -109,7 +103,7 @@ export class AntennaService implements OnApplicationShutdown {
|
||||
read: read,
|
||||
});
|
||||
|
||||
this.globalEventServie.publishAntennaStream(antenna.id, 'note', note);
|
||||
this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
|
||||
|
||||
if (!read) {
|
||||
const mutings = await this.mutingsRepository.find({
|
||||
@@ -139,7 +133,7 @@ export class AntennaService implements OnApplicationShutdown {
|
||||
setTimeout(async () => {
|
||||
const unread = await this.antennaNotesRepository.findOneBy({ antennaId: antenna.id, read: false });
|
||||
if (unread) {
|
||||
this.globalEventServie.publishMainStream(antenna.userId, 'unreadAntenna', antenna);
|
||||
this.globalEventService.publishMainStream(antenna.userId, 'unreadAntenna', antenna);
|
||||
this.pushNotificationService.pushNotification(antenna.userId, 'unreadAntennaNote', {
|
||||
antenna: { id: antenna.id, name: antenna.name },
|
||||
note: await this.noteEntityService.pack(note),
|
||||
@@ -155,10 +149,6 @@ export class AntennaService implements OnApplicationShutdown {
|
||||
public async checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { id: User['id']; username: string; host: string | null; }): Promise<boolean> {
|
||||
if (note.visibility === 'specified') return false;
|
||||
if (note.visibility === 'followers') return false;
|
||||
|
||||
// アンテナ作成者がノート作成者にブロックされていたらスキップ
|
||||
const blockings = await this.blockingCache.fetch(noteUser.id, () => this.blockingsRepository.findBy({ blockerId: noteUser.id }).then(res => res.map(x => x.blockeeId)));
|
||||
if (blockings.some(blocking => blocking === antenna.userId)) return false;
|
||||
|
||||
if (!antenna.withReplies && note.replyId != null) return false;
|
||||
|
||||
|
@@ -26,7 +26,7 @@ export class CreateNotificationService {
|
||||
|
||||
private notificationEntityService: NotificationEntityService,
|
||||
private idService: IdService,
|
||||
private globalEventServie: GlobalEventService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private pushNotificationService: PushNotificationService,
|
||||
) {
|
||||
}
|
||||
@@ -60,7 +60,7 @@ export class CreateNotificationService {
|
||||
const packed = await this.notificationEntityService.pack(notification, {});
|
||||
|
||||
// Publish notification event
|
||||
this.globalEventServie.publishMainStream(notifieeId, 'notification', packed);
|
||||
this.globalEventService.publishMainStream(notifieeId, 'notification', packed);
|
||||
|
||||
// 2秒経っても(今回作成した)通知が既読にならなかったら「未読の通知がありますよ」イベントを発行する
|
||||
setTimeout(async () => {
|
||||
@@ -77,7 +77,7 @@ export class CreateNotificationService {
|
||||
}
|
||||
//#endregion
|
||||
|
||||
this.globalEventServie.publishMainStream(notifieeId, 'unreadNotification', packed);
|
||||
this.globalEventService.publishMainStream(notifieeId, 'unreadNotification', packed);
|
||||
this.pushNotificationService.pushNotification(notifieeId, 'notification', packed);
|
||||
|
||||
if (type === 'follow') this.emailNotificationFollow(notifieeId, await this.usersRepository.findOneByOrFail({ id: data.notifierId! }));
|
||||
|
@@ -120,7 +120,7 @@ export class CustomEmojiService {
|
||||
const url = isLocal
|
||||
? emojiUrl
|
||||
: this.config.proxyRemoteFiles
|
||||
? `${this.config.url}/proxy/${encodeURIComponent((new URL(emojiUrl)).pathname)}?${query({ url: emojiUrl })}`
|
||||
? `${this.config.mediaProxy}/emoji.webp?${query({ url: emojiUrl })}`
|
||||
: emojiUrl;
|
||||
|
||||
return url;
|
||||
@@ -150,17 +150,9 @@ export class CustomEmojiService {
|
||||
if (note.renote) {
|
||||
emojis = emojis.concat(note.renote.emojis
|
||||
.map(e => this.parseEmojiStr(e, note.renote!.userHost)));
|
||||
if (note.renote.user) {
|
||||
emojis = emojis.concat(note.renote.user.emojis
|
||||
.map(e => this.parseEmojiStr(e, note.renote!.userHost)));
|
||||
}
|
||||
}
|
||||
const customReactions = Object.keys(note.reactions).map(x => this.reactionService.decodeReaction(x)).filter(x => x.name != null) as typeof emojis;
|
||||
emojis = emojis.concat(customReactions);
|
||||
if (note.user) {
|
||||
emojis = emojis.concat(note.user.emojis
|
||||
.map(e => this.parseEmojiStr(e, note.userHost)));
|
||||
}
|
||||
}
|
||||
return emojis.filter(x => x.name != null && x.host != null) as { name: string; host: string; }[];
|
||||
}
|
||||
|
@@ -14,7 +14,7 @@ export class DeleteAccountService {
|
||||
|
||||
private userSuspendService: UserSuspendService,
|
||||
private queueService: QueueService,
|
||||
private globalEventServie: GlobalEventService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -38,6 +38,6 @@ export class DeleteAccountService {
|
||||
});
|
||||
|
||||
// Terminate streaming
|
||||
this.globalEventServie.publishUserEvent(user.id, 'terminate', {});
|
||||
this.globalEventService.publishUserEvent(user.id, 'terminate', {});
|
||||
}
|
||||
}
|
||||
|
@@ -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)) {
|
||||
|
@@ -175,7 +175,7 @@ export class NoteCreateService {
|
||||
private userEntityService: UserEntityService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
private idService: IdService,
|
||||
private globalEventServie: GlobalEventService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private queueService: QueueService,
|
||||
private noteReadService: NoteReadService,
|
||||
private createNotificationService: CreateNotificationService,
|
||||
@@ -535,7 +535,7 @@ export class NoteCreateService {
|
||||
// Pack the note
|
||||
const noteObj = await this.noteEntityService.pack(note);
|
||||
|
||||
this.globalEventServie.publishNotesStream(noteObj);
|
||||
this.globalEventService.publishNotesStream(noteObj);
|
||||
|
||||
this.webhookService.getActiveWebhooks().then(webhooks => {
|
||||
webhooks = webhooks.filter(x => x.userId === user.id && x.on.includes('note'));
|
||||
@@ -561,7 +561,7 @@ export class NoteCreateService {
|
||||
|
||||
if (!threadMuted) {
|
||||
nm.push(data.reply.userId, 'reply');
|
||||
this.globalEventServie.publishMainStream(data.reply.userId, 'reply', noteObj);
|
||||
this.globalEventService.publishMainStream(data.reply.userId, 'reply', noteObj);
|
||||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.reply!.userId && x.on.includes('reply'));
|
||||
for (const webhook of webhooks) {
|
||||
@@ -584,7 +584,7 @@ export class NoteCreateService {
|
||||
|
||||
// Publish event
|
||||
if ((user.id !== data.renote.userId) && data.renote.userHost === null) {
|
||||
this.globalEventServie.publishMainStream(data.renote.userId, 'renote', noteObj);
|
||||
this.globalEventService.publishMainStream(data.renote.userId, 'renote', noteObj);
|
||||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === data.renote!.userId && x.on.includes('renote'));
|
||||
for (const webhook of webhooks) {
|
||||
@@ -684,7 +684,7 @@ export class NoteCreateService {
|
||||
detail: true,
|
||||
});
|
||||
|
||||
this.globalEventServie.publishMainStream(u.id, 'mention', detailPackedNote);
|
||||
this.globalEventService.publishMainStream(u.id, 'mention', detailPackedNote);
|
||||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === u.id && x.on.includes('mention'));
|
||||
for (const webhook of webhooks) {
|
||||
|
@@ -34,7 +34,7 @@ export class NoteDeleteService {
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
private globalEventServie: GlobalEventService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private relayService: RelayService,
|
||||
private federatedInstanceService: FederatedInstanceService,
|
||||
private apRendererService: ApRendererService,
|
||||
@@ -63,7 +63,7 @@ export class NoteDeleteService {
|
||||
}
|
||||
|
||||
if (!quiet) {
|
||||
this.globalEventServie.publishNoteStream(note.id, 'deleted', {
|
||||
this.globalEventService.publishNoteStream(note.id, 'deleted', {
|
||||
deletedAt: deletedAt,
|
||||
});
|
||||
|
||||
|
@@ -40,7 +40,7 @@ export class NoteReadService {
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private idService: IdService,
|
||||
private globalEventServie: GlobalEventService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private notificationService: NotificationService,
|
||||
private antennaService: AntennaService,
|
||||
private pushNotificationService: PushNotificationService,
|
||||
@@ -87,13 +87,13 @@ export class NoteReadService {
|
||||
if (exist == null) return;
|
||||
|
||||
if (params.isMentioned) {
|
||||
this.globalEventServie.publishMainStream(userId, 'unreadMention', note.id);
|
||||
this.globalEventService.publishMainStream(userId, 'unreadMention', note.id);
|
||||
}
|
||||
if (params.isSpecified) {
|
||||
this.globalEventServie.publishMainStream(userId, 'unreadSpecifiedNote', note.id);
|
||||
this.globalEventService.publishMainStream(userId, 'unreadSpecifiedNote', note.id);
|
||||
}
|
||||
if (note.channelId) {
|
||||
this.globalEventServie.publishMainStream(userId, 'unreadChannel', note.id);
|
||||
this.globalEventService.publishMainStream(userId, 'unreadChannel', note.id);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
@@ -155,7 +155,7 @@ export class NoteReadService {
|
||||
}).then(mentionsCount => {
|
||||
if (mentionsCount === 0) {
|
||||
// 全て既読になったイベントを発行
|
||||
this.globalEventServie.publishMainStream(userId, 'readAllUnreadMentions');
|
||||
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -165,7 +165,7 @@ export class NoteReadService {
|
||||
}).then(specifiedCount => {
|
||||
if (specifiedCount === 0) {
|
||||
// 全て既読になったイベントを発行
|
||||
this.globalEventServie.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
|
||||
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -175,7 +175,7 @@ export class NoteReadService {
|
||||
}).then(channelNoteCount => {
|
||||
if (channelNoteCount === 0) {
|
||||
// 全て既読になったイベントを発行
|
||||
this.globalEventServie.publishMainStream(userId, 'readAllChannels');
|
||||
this.globalEventService.publishMainStream(userId, 'readAllChannels');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -200,14 +200,14 @@ export class NoteReadService {
|
||||
});
|
||||
|
||||
if (count === 0) {
|
||||
this.globalEventServie.publishMainStream(userId, 'readAntenna', antenna);
|
||||
this.globalEventService.publishMainStream(userId, 'readAntenna', antenna);
|
||||
this.pushNotificationService.pushNotification(userId, 'readAntenna', { antennaId: antenna.id });
|
||||
}
|
||||
}
|
||||
|
||||
this.userEntityService.getHasUnreadAntenna(userId).then(unread => {
|
||||
if (!unread) {
|
||||
this.globalEventServie.publishMainStream(userId, 'readAllAntennas');
|
||||
this.globalEventService.publishMainStream(userId, 'readAllAntennas');
|
||||
this.pushNotificationService.pushNotification(userId, 'readAllAntennas', undefined);
|
||||
}
|
||||
});
|
||||
|
@@ -1,17 +1,17 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Not } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { NotesRepository, UsersRepository, BlockingsRepository, PollsRepository, PollVotesRepository } from '@/models/index.js';
|
||||
import type { NotesRepository, UsersRepository, PollsRepository, PollVotesRepository } from '@/models/index.js';
|
||||
import type { Note } from '@/models/entities/Note.js';
|
||||
import { RelayService } from '@/core/RelayService.js';
|
||||
import type { CacheableUser } from '@/models/entities/User.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { CreateNotificationService } from '@/core/CreateNotificationService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
|
||||
@Injectable()
|
||||
export class PollService {
|
||||
@@ -28,14 +28,11 @@ export class PollService {
|
||||
@Inject(DI.pollVotesRepository)
|
||||
private pollVotesRepository: PollVotesRepository,
|
||||
|
||||
@Inject(DI.blockingsRepository)
|
||||
private blockingsRepository: BlockingsRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private idService: IdService,
|
||||
private relayService: RelayService,
|
||||
private globalEventServie: GlobalEventService,
|
||||
private createNotificationService: CreateNotificationService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private userBlockingService: UserBlockingService,
|
||||
private apRendererService: ApRendererService,
|
||||
private apDeliverManagerService: ApDeliverManagerService,
|
||||
) {
|
||||
@@ -52,11 +49,8 @@ export class PollService {
|
||||
|
||||
// Check blocking
|
||||
if (note.userId !== user.id) {
|
||||
const block = await this.blockingsRepository.findOneBy({
|
||||
blockerId: note.userId,
|
||||
blockeeId: user.id,
|
||||
});
|
||||
if (block) {
|
||||
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
|
||||
if (blocked) {
|
||||
throw new Error('blocked');
|
||||
}
|
||||
}
|
||||
@@ -88,7 +82,7 @@ export class PollService {
|
||||
const index = choice + 1; // In SQL, array index is 1 based
|
||||
await this.pollsRepository.query(`UPDATE poll SET votes[${index}] = votes[${index}] + 1 WHERE "noteId" = '${poll.noteId}'`);
|
||||
|
||||
this.globalEventServie.publishNoteStream(note.id, 'pollVoted', {
|
||||
this.globalEventService.publishNoteStream(note.id, 'pollVoted', {
|
||||
choice: choice,
|
||||
userId: user.id,
|
||||
});
|
||||
|
@@ -18,7 +18,8 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { UtilityService } from './UtilityService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
|
||||
const legacies: Record<string, string> = {
|
||||
'like': '👍',
|
||||
@@ -73,8 +74,9 @@ export class ReactionService {
|
||||
private metaService: MetaService,
|
||||
private userEntityService: UserEntityService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
private userBlockingService: UserBlockingService,
|
||||
private idService: IdService,
|
||||
private globalEventServie: GlobalEventService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private apRendererService: ApRendererService,
|
||||
private apDeliverManagerService: ApDeliverManagerService,
|
||||
private createNotificationService: CreateNotificationService,
|
||||
@@ -86,11 +88,8 @@ export class ReactionService {
|
||||
public async create(user: { id: User['id']; host: User['host']; isBot: User['isBot'] }, note: Note, reaction?: string) {
|
||||
// Check blocking
|
||||
if (note.userId !== user.id) {
|
||||
const block = await this.blockingsRepository.findOneBy({
|
||||
blockerId: note.userId,
|
||||
blockeeId: user.id,
|
||||
});
|
||||
if (block) {
|
||||
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
|
||||
if (blocked) {
|
||||
throw new IdentifiableError('e70412a4-7197-4726-8e74-f3e0deb92aa7');
|
||||
}
|
||||
}
|
||||
@@ -157,7 +156,7 @@ export class ReactionService {
|
||||
select: ['name', 'host', 'originalUrl', 'publicUrl'],
|
||||
});
|
||||
|
||||
this.globalEventServie.publishNoteStream(note.id, 'reacted', {
|
||||
this.globalEventService.publishNoteStream(note.id, 'reacted', {
|
||||
reaction: decodedReaction.reaction,
|
||||
emoji: emoji != null ? {
|
||||
name: emoji.host ? `${emoji.name}@${emoji.host}` : `${emoji.name}@.`,
|
||||
@@ -229,7 +228,7 @@ export class ReactionService {
|
||||
|
||||
if (!user.isBot) this.notesRepository.decrement({ id: note.id }, 'score', 1);
|
||||
|
||||
this.globalEventServie.publishNoteStream(note.id, 'unreacted', {
|
||||
this.globalEventService.publishNoteStream(note.id, 'unreacted', {
|
||||
reaction: this.decodeReaction(exist.reaction).reaction,
|
||||
userId: user.id,
|
||||
});
|
||||
|
@@ -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();
|
||||
|
@@ -1,5 +1,6 @@
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { CacheableUser, User } from '@/models/entities/User.js';
|
||||
import type { Blocking } from '@/models/entities/Blocking.js';
|
||||
@@ -7,7 +8,6 @@ import { QueueService } from '@/core/QueueService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import logger from '@/logger.js';
|
||||
import type { UsersRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, UserListsRepository, UserListJoiningsRepository } from '@/models/index.js';
|
||||
import Logger from '@/logger.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
@@ -15,12 +15,20 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { WebhookService } from '@/core/WebhookService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { Cache } from '@/misc/cache.js';
|
||||
import { StreamMessages } from '@/server/api/stream/types.js';
|
||||
|
||||
@Injectable()
|
||||
export class UserBlockingService {
|
||||
export class UserBlockingService implements OnApplicationShutdown {
|
||||
private logger: Logger;
|
||||
|
||||
// キーがユーザーIDで、値がそのユーザーがブロックしているユーザーのIDのリストなキャッシュ
|
||||
private blockingsByUserIdCache: Cache<User['id'][]>;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.redisSubscriber)
|
||||
private redisSubscriber: Redis.Redis,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@@ -42,13 +50,44 @@ export class UserBlockingService {
|
||||
private userEntityService: UserEntityService,
|
||||
private idService: IdService,
|
||||
private queueService: QueueService,
|
||||
private globalEventServie: GlobalEventService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private webhookService: WebhookService,
|
||||
private apRendererService: ApRendererService,
|
||||
private perUserFollowingChart: PerUserFollowingChart,
|
||||
private loggerService: LoggerService,
|
||||
) {
|
||||
this.logger = this.loggerService.getLogger('user-block');
|
||||
|
||||
this.blockingsByUserIdCache = new Cache<User['id'][]>(Infinity);
|
||||
|
||||
this.redisSubscriber.on('message', this.onMessage);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async onMessage(_: string, data: string): Promise<void> {
|
||||
const obj = JSON.parse(data);
|
||||
|
||||
if (obj.channel === 'internal') {
|
||||
const { type, body } = obj.message as StreamMessages['internal']['payload'];
|
||||
switch (type) {
|
||||
case 'blockingCreated': {
|
||||
const cached = this.blockingsByUserIdCache.get(body.blockerId);
|
||||
if (cached) {
|
||||
this.blockingsByUserIdCache.set(body.blockerId, [...cached, ...[body.blockeeId]]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'blockingDeleted': {
|
||||
const cached = this.blockingsByUserIdCache.get(body.blockerId);
|
||||
if (cached) {
|
||||
this.blockingsByUserIdCache.set(body.blockerId, cached.filter(x => x !== body.blockeeId));
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -72,6 +111,11 @@ export class UserBlockingService {
|
||||
|
||||
await this.blockingsRepository.insert(blocking);
|
||||
|
||||
this.globalEventService.publishInternalEvent('blockingCreated', {
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
});
|
||||
|
||||
if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) {
|
||||
const content = this.apRendererService.renderActivity(this.apRendererService.renderBlock(blocking));
|
||||
this.queueService.deliver(blocker, content, blockee.inbox);
|
||||
@@ -97,15 +141,15 @@ export class UserBlockingService {
|
||||
if (this.userEntityService.isLocalUser(followee)) {
|
||||
this.userEntityService.pack(followee, followee, {
|
||||
detail: true,
|
||||
}).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed));
|
||||
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
|
||||
}
|
||||
|
||||
if (this.userEntityService.isLocalUser(follower)) {
|
||||
this.userEntityService.pack(followee, follower, {
|
||||
detail: true,
|
||||
}).then(async packed => {
|
||||
this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packed);
|
||||
this.globalEventServie.publishMainStream(follower.id, 'unfollow', packed);
|
||||
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
|
||||
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
|
||||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||
for (const webhook of webhooks) {
|
||||
@@ -152,8 +196,8 @@ export class UserBlockingService {
|
||||
this.userEntityService.pack(followee, follower, {
|
||||
detail: true,
|
||||
}).then(async packed => {
|
||||
this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packed);
|
||||
this.globalEventServie.publishMainStream(follower.id, 'unfollow', packed);
|
||||
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
|
||||
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
|
||||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||
for (const webhook of webhooks) {
|
||||
@@ -210,10 +254,31 @@ export class UserBlockingService {
|
||||
|
||||
await this.blockingsRepository.delete(blocking.id);
|
||||
|
||||
this.globalEventService.publishInternalEvent('blockingDeleted', {
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
});
|
||||
|
||||
// deliver if remote bloking
|
||||
if (this.userEntityService.isLocalUser(blocker) && this.userEntityService.isRemoteUser(blockee)) {
|
||||
const content = this.apRendererService.renderActivity(this.apRendererService.renderUndo(this.apRendererService.renderBlock(blocking), blocker));
|
||||
this.queueService.deliver(blocker, content, blockee.inbox);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async checkBlocked(blockerId: User['id'], blockeeId: User['id']): Promise<boolean> {
|
||||
const blockedUserIds = await this.blockingsByUserIdCache.fetch(blockerId, () => this.blockingsRepository.find({
|
||||
where: {
|
||||
blockerId,
|
||||
},
|
||||
select: ['blockeeId'],
|
||||
}).then(records => records.map(record => record.blockeeId)));
|
||||
return blockedUserIds.includes(blockeeId);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined) {
|
||||
this.redisSubscriber.off('message', this.onMessage);
|
||||
}
|
||||
}
|
||||
|
@@ -12,10 +12,11 @@ import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
import { WebhookService } from '@/core/WebhookService.js';
|
||||
import { CreateNotificationService } from '@/core/CreateNotificationService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { BlockingsRepository, FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import Logger from '../logger.js';
|
||||
|
||||
const logger = new Logger('following/create');
|
||||
@@ -48,21 +49,18 @@ export class UserFollowingService {
|
||||
@Inject(DI.followRequestsRepository)
|
||||
private followRequestsRepository: FollowRequestsRepository,
|
||||
|
||||
@Inject(DI.blockingsRepository)
|
||||
private blockingsRepository: BlockingsRepository,
|
||||
|
||||
@Inject(DI.instancesRepository)
|
||||
private instancesRepository: InstancesRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private userBlockingService: UserBlockingService,
|
||||
private idService: IdService,
|
||||
private queueService: QueueService,
|
||||
private globalEventServie: GlobalEventService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private createNotificationService: CreateNotificationService,
|
||||
private federatedInstanceService: FederatedInstanceService,
|
||||
private webhookService: WebhookService,
|
||||
private apRendererService: ApRendererService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private perUserFollowingChart: PerUserFollowingChart,
|
||||
private instanceChart: InstanceChart,
|
||||
) {
|
||||
@@ -77,28 +75,22 @@ export class UserFollowingService {
|
||||
|
||||
// check blocking
|
||||
const [blocking, blocked] = await Promise.all([
|
||||
this.blockingsRepository.findOneBy({
|
||||
blockerId: follower.id,
|
||||
blockeeId: followee.id,
|
||||
}),
|
||||
this.blockingsRepository.findOneBy({
|
||||
blockerId: followee.id,
|
||||
blockeeId: follower.id,
|
||||
}),
|
||||
this.userBlockingService.checkBlocked(follower.id, followee.id),
|
||||
this.userBlockingService.checkBlocked(followee.id, follower.id),
|
||||
]);
|
||||
|
||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocked) {
|
||||
// リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。
|
||||
// リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。
|
||||
const content = this.apRendererService.renderActivity(this.apRendererService.renderReject(this.apRendererService.renderFollow(follower, followee, requestId), followee));
|
||||
this.queueService.deliver(followee, content, follower.inbox);
|
||||
return;
|
||||
} else if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee) && blocking) {
|
||||
// リモートフォローを受けてブロックされているはずの場合だったら、ブロック解除しておく。
|
||||
await this.blockingsRepository.delete(blocking.id);
|
||||
// リモートフォローを受けてブロックされているはずの場合だったら、ブロック解除しておく。
|
||||
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 });
|
||||
@@ -227,8 +219,8 @@ export class UserFollowingService {
|
||||
this.userEntityService.pack(followee.id, follower, {
|
||||
detail: true,
|
||||
}).then(async packed => {
|
||||
this.globalEventServie.publishUserEvent(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
|
||||
this.globalEventServie.publishMainStream(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
|
||||
this.globalEventService.publishUserEvent(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
|
||||
this.globalEventService.publishMainStream(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
|
||||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
|
||||
for (const webhook of webhooks) {
|
||||
@@ -242,7 +234,7 @@ export class UserFollowingService {
|
||||
// Publish followed event
|
||||
if (this.userEntityService.isLocalUser(followee)) {
|
||||
this.userEntityService.pack(follower.id, followee).then(async packed => {
|
||||
this.globalEventServie.publishMainStream(followee.id, 'followed', packed);
|
||||
this.globalEventService.publishMainStream(followee.id, 'followed', packed);
|
||||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === followee.id && x.on.includes('followed'));
|
||||
for (const webhook of webhooks) {
|
||||
@@ -288,8 +280,8 @@ export class UserFollowingService {
|
||||
this.userEntityService.pack(followee.id, follower, {
|
||||
detail: true,
|
||||
}).then(async packed => {
|
||||
this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packed);
|
||||
this.globalEventServie.publishMainStream(follower.id, 'unfollow', packed);
|
||||
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packed);
|
||||
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
|
||||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||
for (const webhook of webhooks) {
|
||||
@@ -357,18 +349,12 @@ export class UserFollowingService {
|
||||
|
||||
// check blocking
|
||||
const [blocking, blocked] = await Promise.all([
|
||||
this.blockingsRepository.findOneBy({
|
||||
blockerId: follower.id,
|
||||
blockeeId: followee.id,
|
||||
}),
|
||||
this.blockingsRepository.findOneBy({
|
||||
blockerId: followee.id,
|
||||
blockeeId: follower.id,
|
||||
}),
|
||||
this.userBlockingService.checkBlocked(follower.id, followee.id),
|
||||
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(),
|
||||
@@ -388,11 +374,11 @@ export class UserFollowingService {
|
||||
|
||||
// Publish receiveRequest event
|
||||
if (this.userEntityService.isLocalUser(followee)) {
|
||||
this.userEntityService.pack(follower.id, followee).then(packed => this.globalEventServie.publishMainStream(followee.id, 'receiveFollowRequest', packed));
|
||||
this.userEntityService.pack(follower.id, followee).then(packed => this.globalEventService.publishMainStream(followee.id, 'receiveFollowRequest', packed));
|
||||
|
||||
this.userEntityService.pack(followee.id, followee, {
|
||||
detail: true,
|
||||
}).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed));
|
||||
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
|
||||
|
||||
// 通知を作成
|
||||
this.createNotificationService.createNotification(followee.id, 'receiveFollowRequest', {
|
||||
@@ -440,7 +426,7 @@ export class UserFollowingService {
|
||||
|
||||
this.userEntityService.pack(followee.id, followee, {
|
||||
detail: true,
|
||||
}).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed));
|
||||
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -468,7 +454,7 @@ export class UserFollowingService {
|
||||
|
||||
this.userEntityService.pack(followee.id, followee, {
|
||||
detail: true,
|
||||
}).then(packed => this.globalEventServie.publishMainStream(followee.id, 'meUpdated', packed));
|
||||
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -583,8 +569,8 @@ export class UserFollowingService {
|
||||
detail: true,
|
||||
});
|
||||
|
||||
this.globalEventServie.publishUserEvent(follower.id, 'unfollow', packedFollowee);
|
||||
this.globalEventServie.publishMainStream(follower.id, 'unfollow', packedFollowee);
|
||||
this.globalEventService.publishUserEvent(follower.id, 'unfollow', packedFollowee);
|
||||
this.globalEventService.publishMainStream(follower.id, 'unfollow', packedFollowee);
|
||||
|
||||
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('unfollow'));
|
||||
for (const webhook of webhooks) {
|
||||
|
@@ -25,7 +25,7 @@ export class UserListService {
|
||||
private idService: IdService,
|
||||
private userFollowingService: UserFollowingService,
|
||||
private roleService: RoleService,
|
||||
private globalEventServie: GlobalEventService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private proxyAccountService: ProxyAccountService,
|
||||
) {
|
||||
}
|
||||
@@ -46,7 +46,7 @@ export class UserListService {
|
||||
userListId: list.id,
|
||||
} as UserListJoining);
|
||||
|
||||
this.globalEventServie.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target));
|
||||
this.globalEventService.publishUserListStream(list.id, 'userAdded', await this.userEntityService.pack(target));
|
||||
|
||||
// このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする
|
||||
if (this.userEntityService.isRemoteUser(target)) {
|
||||
|
@@ -18,7 +18,7 @@ export class UserMutingService {
|
||||
|
||||
private idService: IdService,
|
||||
private queueService: QueueService,
|
||||
private globalEventServie: GlobalEventService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
@@ -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();
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -54,7 +54,7 @@ export class ChannelEntityService {
|
||||
name: channel.name,
|
||||
description: channel.description,
|
||||
userId: channel.userId,
|
||||
bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner, false) : null,
|
||||
bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null,
|
||||
usersCount: channel.usersCount,
|
||||
notesCount: channel.notesCount,
|
||||
|
||||
|
@@ -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 {
|
||||
@@ -71,27 +72,42 @@ export class DriveFileEntityService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getPublicUrl(file: DriveFile, thumbnail = false): string | null {
|
||||
public getPublicUrl(file: DriveFile, mode? : 'static' | 'avatar'): string | null { // static = thumbnail
|
||||
const proxiedUrl = (url: string) => appendQuery(
|
||||
`${this.config.mediaProxy}/${mode ?? 'image'}.webp`,
|
||||
query({
|
||||
url,
|
||||
...(mode ? { [mode]: '1' } : {}),
|
||||
})
|
||||
);
|
||||
|
||||
// リモートかつメディアプロキシ
|
||||
if (file.uri != null && file.userHost != null && this.config.mediaProxy != null) {
|
||||
return appendQuery(this.config.mediaProxy, query({
|
||||
url: file.uri,
|
||||
thumbnail: thumbnail ? '1' : undefined,
|
||||
}));
|
||||
if (file.uri != null && file.userHost != null && this.config.externalMediaProxyEnabled) {
|
||||
if (!(mode === 'static' && file.type.startsWith('video'))) {
|
||||
return proxiedUrl(file.uri);
|
||||
}
|
||||
}
|
||||
|
||||
// リモートかつ期限切れはローカルプロキシを試みる
|
||||
if (file.uri != null && file.isLink && this.config.proxyRemoteFiles) {
|
||||
const key = thumbnail ? file.thumbnailAccessKey : file.webpublicAccessKey;
|
||||
const key = mode === 'static' ? file.thumbnailAccessKey : file.webpublicAccessKey;
|
||||
|
||||
if (key && !key.match('/')) { // 古いものはここにオブジェクトストレージキーが入ってるので除外
|
||||
return `${this.config.url}/files/${key}`;
|
||||
const url = `${this.config.url}/files/${key}`;
|
||||
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);
|
||||
const url = file.webpublicUrl ?? file.url;
|
||||
|
||||
return thumbnail ? (file.thumbnailUrl ?? (isImage ? (file.webpublicUrl ?? file.url) : null)) : (file.webpublicUrl ?? file.url);
|
||||
if (mode === 'static') {
|
||||
return file.thumbnailUrl ?? (isMimeImage(file.type, 'sharp-convertible-image') ? proxiedUrl(url) : null);
|
||||
}
|
||||
if (mode === 'avatar') {
|
||||
return proxiedUrl(url);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -166,8 +182,8 @@ export class DriveFileEntityService {
|
||||
isSensitive: file.isSensitive,
|
||||
blurhash: file.blurhash,
|
||||
properties: opts.self ? file.properties : this.getPublicProperties(file),
|
||||
url: opts.self ? file.url : this.getPublicUrl(file, false),
|
||||
thumbnailUrl: this.getPublicUrl(file, true),
|
||||
url: opts.self ? file.url : this.getPublicUrl(file),
|
||||
thumbnailUrl: this.getPublicUrl(file, 'static'),
|
||||
comment: file.comment,
|
||||
folderId: file.folderId,
|
||||
folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, {
|
||||
@@ -201,8 +217,8 @@ export class DriveFileEntityService {
|
||||
isSensitive: file.isSensitive,
|
||||
blurhash: file.blurhash,
|
||||
properties: opts.self ? file.properties : this.getPublicProperties(file),
|
||||
url: opts.self ? file.url : this.getPublicUrl(file, false),
|
||||
thumbnailUrl: this.getPublicUrl(file, true),
|
||||
url: opts.self ? file.url : this.getPublicUrl(file),
|
||||
thumbnailUrl: this.getPublicUrl(file, 'static'),
|
||||
comment: file.comment,
|
||||
folderId: file.folderId,
|
||||
folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, {
|
||||
|
@@ -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,
|
||||
|
@@ -314,10 +314,10 @@ export class UserEntityService implements OnModuleInit {
|
||||
@bindThis
|
||||
public async getAvatarUrl(user: User): Promise<string> {
|
||||
if (user.avatar) {
|
||||
return this.driveFileEntityService.getPublicUrl(user.avatar, true) ?? this.getIdenticonUrl(user.id);
|
||||
return this.driveFileEntityService.getPublicUrl(user.avatar, 'avatar') ?? this.getIdenticonUrl(user.id);
|
||||
} else if (user.avatarId) {
|
||||
const avatar = await this.driveFilesRepository.findOneByOrFail({ id: user.avatarId });
|
||||
return this.driveFileEntityService.getPublicUrl(avatar, true) ?? this.getIdenticonUrl(user.id);
|
||||
return this.driveFileEntityService.getPublicUrl(avatar, 'avatar') ?? this.getIdenticonUrl(user.id);
|
||||
} else {
|
||||
return this.getIdenticonUrl(user.id);
|
||||
}
|
||||
@@ -326,7 +326,7 @@ export class UserEntityService implements OnModuleInit {
|
||||
@bindThis
|
||||
public getAvatarUrlSync(user: User): string {
|
||||
if (user.avatar) {
|
||||
return this.driveFileEntityService.getPublicUrl(user.avatar, true) ?? this.getIdenticonUrl(user.id);
|
||||
return this.driveFileEntityService.getPublicUrl(user.avatar, 'avatar') ?? this.getIdenticonUrl(user.id);
|
||||
} else {
|
||||
return this.getIdenticonUrl(user.id);
|
||||
}
|
||||
@@ -413,8 +413,12 @@ export class UserEntityService implements OnModuleInit {
|
||||
faviconUrl: instance.faviconUrl,
|
||||
themeColor: instance.themeColor,
|
||||
} : 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,
|
||||
@@ -422,7 +426,7 @@ export class UserEntityService implements OnModuleInit {
|
||||
createdAt: user.createdAt.toISOString(),
|
||||
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
|
||||
lastFetchedAt: user.lastFetchedAt ? user.lastFetchedAt.toISOString() : null,
|
||||
bannerUrl: user.banner ? this.driveFileEntityService.getPublicUrl(user.banner, false) : null,
|
||||
bannerUrl: user.banner ? this.driveFileEntityService.getPublicUrl(user.banner) : null,
|
||||
bannerBlurhash: user.banner?.blurhash ?? null,
|
||||
isLocked: user.isLocked,
|
||||
isSilenced: this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
|
||||
@@ -454,10 +458,12 @@ 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,
|
||||
}))),
|
||||
emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
|
||||
} : {}),
|
||||
|
||||
...(opts.detail && isMe ? {
|
||||
|
@@ -1,5 +1,7 @@
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
|
||||
|
||||
export class Cache<T> {
|
||||
public cache: Map<string | null, { date: number; value: T; }>;
|
||||
private lifetime: number;
|
||||
|
@@ -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,
|
||||
})
|
||||
|
@@ -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,
|
@@ -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;
|
||||
|
@@ -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({
|
||||
|
@@ -137,38 +137,42 @@ export class FileServerService {
|
||||
|
||||
try {
|
||||
if (file.state === 'remote') {
|
||||
const convertFile = async () => {
|
||||
if (file.fileRole === 'thumbnail') {
|
||||
if (['image/jpeg', 'image/webp', 'image/avif', 'image/png', 'image/svg+xml'].includes(file.mime)) {
|
||||
return this.imageProcessingService.convertToWebpStream(
|
||||
file.path,
|
||||
498,
|
||||
280
|
||||
);
|
||||
} else if (file.mime.startsWith('video/')) {
|
||||
return await this.videoProcessingService.generateVideoThumbnail(file.path);
|
||||
}
|
||||
}
|
||||
let image: IImageStreamable | null = null;
|
||||
|
||||
if (file.fileRole === 'webpublic') {
|
||||
if (['image/svg+xml'].includes(file.mime)) {
|
||||
return this.imageProcessingService.convertToWebpStream(
|
||||
file.path,
|
||||
2048,
|
||||
2048,
|
||||
{ ...webpDefault, lossless: true }
|
||||
)
|
||||
}
|
||||
}
|
||||
if (file.fileRole === 'thumbnail') {
|
||||
if (isMimeImage(file.mime, 'sharp-convertible-image')) {
|
||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
||||
|
||||
return {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if (file.fileRole === 'webpublic') {
|
||||
if (['image/svg+xml'].includes(file.mime)) {
|
||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
||||
|
||||
const url = new URL(`${this.config.mediaProxy}/svg.webp`);
|
||||
url.searchParams.set('url', file.url);
|
||||
|
||||
file.cleanup();
|
||||
return await reply.redirect(301, url.toString());
|
||||
}
|
||||
}
|
||||
|
||||
if (!image) {
|
||||
image = {
|
||||
data: fs.createReadStream(file.path),
|
||||
ext: file.ext,
|
||||
type: file.mime,
|
||||
};
|
||||
};
|
||||
|
||||
const image = await convertFile();
|
||||
}
|
||||
|
||||
if ('pipe' in image.data && typeof image.data.pipe === 'function') {
|
||||
// image.dataがstreamなら、stream終了後にcleanup
|
||||
@@ -180,7 +184,6 @@ export class FileServerService {
|
||||
}
|
||||
|
||||
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream');
|
||||
reply.header('Cache-Control', 'max-age=31536000, immutable');
|
||||
return image.data;
|
||||
}
|
||||
|
||||
@@ -217,6 +220,23 @@ export class FileServerService {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.config.externalMediaProxyEnabled) {
|
||||
// 外部のメディアプロキシが有効なら、そちらにリダイレクト
|
||||
|
||||
reply.header('Cache-Control', 'public, max-age=259200'); // 3 days
|
||||
|
||||
const url = new URL(`${this.config.mediaProxy}/${request.params.url || ''}`);
|
||||
|
||||
for (const [key, value] of Object.entries(request.query)) {
|
||||
url.searchParams.append(key, value);
|
||||
}
|
||||
|
||||
return await reply.redirect(
|
||||
301,
|
||||
url.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
// Create temp file
|
||||
const file = await this.getStreamAndTypeFromUrl(url);
|
||||
if (file === '404') {
|
||||
@@ -236,7 +256,7 @@ export class FileServerService {
|
||||
const isAnimationConvertibleImage = isMimeImage(file.mime, 'sharp-animation-convertible-image');
|
||||
|
||||
let image: IImageStreamable | null = null;
|
||||
if ('emoji' in request.query && isConvertibleImage) {
|
||||
if (('emoji' in request.query || 'avatar' in request.query) && isConvertibleImage) {
|
||||
if (!isAnimationConvertibleImage && !('static' in request.query)) {
|
||||
image = {
|
||||
data: fs.createReadStream(file.path),
|
||||
@@ -246,7 +266,7 @@ export class FileServerService {
|
||||
} else {
|
||||
const data = sharp(file.path, { animated: !('static' in request.query) })
|
||||
.resize({
|
||||
height: 128,
|
||||
height: 'emoji' in request.query ? 128 : 320,
|
||||
withoutEnlargement: true,
|
||||
})
|
||||
.webp(webpDefault);
|
||||
@@ -370,7 +390,7 @@ export class FileServerService {
|
||||
|
||||
@bindThis
|
||||
private async getFileFromKey(key: string): Promise<
|
||||
{ state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; mime: string; ext: string | null; path: string; cleanup: () => void; }
|
||||
{ state: 'remote'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; url: string; mime: string; ext: string | null; path: string; cleanup: () => void; }
|
||||
| { state: 'stored_internal'; fileRole: 'thumbnail' | 'webpublic' | 'original'; file: DriveFile; mime: string; ext: string | null; path: string; }
|
||||
| '404'
|
||||
| '204'
|
||||
@@ -392,6 +412,7 @@ export class FileServerService {
|
||||
const result = await this.downloadAndDetectTypeFromUrl(file.uri);
|
||||
return {
|
||||
...result,
|
||||
url: file.uri,
|
||||
fileRole: isThumbnail ? 'thumbnail' : isWebpublic ? 'webpublic' : 'original',
|
||||
file,
|
||||
}
|
||||
|
@@ -106,7 +106,7 @@ export class ServerService {
|
||||
}
|
||||
}
|
||||
|
||||
const url = new URL('/proxy/emoji.webp', this.config.url);
|
||||
const url = new URL(`${this.config.mediaProxy}/emoji.webp`);
|
||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||
url.searchParams.set('url', emoji.publicUrl || emoji.originalUrl);
|
||||
url.searchParams.set('emoji', '1');
|
||||
|
@@ -19,11 +19,13 @@ export const paramDef = {
|
||||
name: { type: 'string' },
|
||||
description: { type: 'string' },
|
||||
color: { type: 'string', nullable: true },
|
||||
iconUrl: { type: 'string', nullable: true },
|
||||
target: { type: 'string' },
|
||||
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]));
|
||||
|
@@ -27,11 +27,13 @@ export const paramDef = {
|
||||
name: { type: 'string' },
|
||||
description: { type: 'string' },
|
||||
color: { type: 'string', nullable: true },
|
||||
iconUrl: { type: 'string', nullable: true },
|
||||
target: { type: 'string' },
|
||||
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,
|
||||
});
|
||||
|
@@ -181,6 +181,10 @@ export const meta = {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
mediaProxy: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
features: {
|
||||
type: 'object',
|
||||
optional: true, nullable: false,
|
||||
@@ -307,6 +311,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
policies: { ...DEFAULT_POLICIES, ...instance.policies },
|
||||
|
||||
mediaProxy: this.config.mediaProxy,
|
||||
|
||||
...(ps.detail ? {
|
||||
pinnedPages: instance.pinnedPages,
|
||||
pinnedClipId: instance.pinnedClipId,
|
||||
|
@@ -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');
|
||||
}
|
||||
});
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Not } from 'typeorm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UsersRepository, BlockingsRepository, PollsRepository, PollVotesRepository } from '@/models/index.js';
|
||||
import type { UsersRepository, PollsRepository, PollVotesRepository } from '@/models/index.js';
|
||||
import type { IRemoteUser } from '@/models/entities/User.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
@@ -11,6 +11,7 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { CreateNotificationService } from '@/core/CreateNotificationService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -77,9 +78,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.blockingsRepository)
|
||||
private blockingsRepository: BlockingsRepository,
|
||||
|
||||
@Inject(DI.pollsRepository)
|
||||
private pollsRepository: PollsRepository,
|
||||
|
||||
@@ -93,6 +91,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
private apRendererService: ApRendererService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private createNotificationService: CreateNotificationService,
|
||||
private userBlockingService: UserBlockingService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const createdAt = new Date();
|
||||
@@ -109,11 +108,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
// Check blocking
|
||||
if (note.userId !== me.id) {
|
||||
const block = await this.blockingsRepository.findOneBy({
|
||||
blockerId: note.userId,
|
||||
blockeeId: me.id,
|
||||
});
|
||||
if (block) {
|
||||
const blocked = await this.userBlockingService.checkBlocked(note.userId, me.id);
|
||||
if (blocked) {
|
||||
throw new ApiError(meta.errors.youHaveBeenBlocked);
|
||||
}
|
||||
}
|
||||
|
@@ -95,14 +95,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
try {
|
||||
if (ps.tag) {
|
||||
if (!safeForSql(ps.tag)) throw 'Injection';
|
||||
if (!safeForSql(normalizeForSearch(ps.tag))) throw 'Injection';
|
||||
query.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`);
|
||||
} else {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
for (const tags of ps.query!) {
|
||||
qb.orWhere(new Brackets(qb => {
|
||||
for (const tag of tags) {
|
||||
if (!safeForSql(tag)) throw 'Injection';
|
||||
if (!safeForSql(normalizeForSearch(tag))) throw 'Injection';
|
||||
qb.andWhere(`'{"${normalizeForSearch(tag)}"}' <@ note.tags`);
|
||||
}
|
||||
}));
|
||||
|
@@ -25,6 +25,8 @@ export interface InternalStreamTypes {
|
||||
remoteUserUpdated: { id: User['id']; };
|
||||
follow: { followerId: User['id']; followeeId: User['id']; };
|
||||
unfollow: { followerId: User['id']; followeeId: User['id']; };
|
||||
blockingCreated: { blockerId: User['id']; blockeeId: User['id']; };
|
||||
blockingDeleted: { blockerId: User['id']; blockeeId: User['id']; };
|
||||
policiesUpdated: Role['policies'];
|
||||
roleCreated: Role;
|
||||
roleDeleted: Role;
|
||||
|
@@ -33,7 +33,7 @@ export class UrlPreviewService {
|
||||
private wrap(url?: string): string | null {
|
||||
return url != null
|
||||
? url.match(/^https?:\/\//)
|
||||
? `${this.config.url}/proxy/preview.webp?${query({
|
||||
? `${this.config.mediaProxy}/preview.webp?${query({
|
||||
url,
|
||||
preview: '1',
|
||||
})}`
|
||||
@@ -73,6 +73,14 @@ export class UrlPreviewService {
|
||||
});
|
||||
|
||||
this.logger.succ(`Got preview of ${url}: ${summary.title}`);
|
||||
|
||||
if (summary.url && !(summary.url.startsWith('http://') || summary.url.startsWith('https://'))) {
|
||||
throw new Error('unsupported schema included');
|
||||
}
|
||||
|
||||
if (summary.player?.url && !(summary.player.url.startsWith('http://') || summary.player.url.startsWith('https://'))) {
|
||||
throw new Error('unsupported schema included');
|
||||
}
|
||||
|
||||
summary.icon = this.wrap(summary.icon);
|
||||
summary.thumbnail = this.wrap(summary.thumbnail);
|
||||
|
@@ -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);
|
||||
|
@@ -4,7 +4,7 @@
|
||||
<MkA v-user-preview="report.targetUserId" class="info" :to="`/user-info/${report.targetUserId}`">
|
||||
<MkAvatar class="avatar" :user="report.targetUser" indicator/>
|
||||
<div class="names">
|
||||
<MkUserName class="name" :user="report.targetUser"/>
|
||||
<span class="name _nowrap">{{ report.targetUser.name ?? report.targetUser.username }}</span>
|
||||
<MkAcct class="acct" :user="report.targetUser" style="display: block;"/>
|
||||
</div>
|
||||
</MkA>
|
||||
|
@@ -4,7 +4,7 @@
|
||||
<li v-for="user in users" tabindex="-1" :class="$style.item" @click="complete(type, user)" @keydown="onKeydown">
|
||||
<img :class="$style.avatar" :src="user.avatarUrl"/>
|
||||
<span :class="$style.userName">
|
||||
<MkUserName :key="user.id" :user="user"/>
|
||||
<span :key="user.id" class="_nowrap">{{ user.name ?? user.username }}</span>
|
||||
</span>
|
||||
<span>@{{ acct(user) }}</span>
|
||||
</li>
|
||||
|
@@ -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>
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
||||
|
@@ -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>
|
||||
|
@@ -27,7 +27,7 @@
|
||||
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
|
||||
</a>
|
||||
<button v-else-if="item.type === 'user'" :tabindex="i" class="_button" :class="[$style.item, { [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
||||
<MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/>
|
||||
<MkAvatar :user="item.user" :class="$style.avatar"/><span class="_nowrap">{{ item.user.name ?? item.user.username }}</span>
|
||||
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
|
||||
</button>
|
||||
<span v-else-if="item.type === 'switch'" :tabindex="i" :class="$style.item" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
||||
|
@@ -17,7 +17,7 @@
|
||||
<I18n :src="i18n.ts.renotedBy" tag="span" :class="$style.renoteText">
|
||||
<template #user>
|
||||
<MkA v-user-preview="note.userId" :class="$style.renoteUserName" :to="userPage(note.user)">
|
||||
<MkUserName :user="note.user"/>
|
||||
<span class="_nowrap">{{ note.user.name ?? note.user.username }}</span>
|
||||
</MkA>
|
||||
</template>
|
||||
</I18n>
|
||||
@@ -108,7 +108,7 @@
|
||||
<I18n :src="i18n.ts.userSaysSomething" tag="small">
|
||||
<template #name>
|
||||
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
<span class="_nowrap">{{ appearNote.user.name ?? appearNote.user.username }}</span>
|
||||
</MkA>
|
||||
</template>
|
||||
</I18n>
|
||||
|
@@ -16,7 +16,7 @@
|
||||
<I18n :src="i18n.ts.renotedBy" tag="span">
|
||||
<template #user>
|
||||
<MkA v-user-preview="note.userId" class="name" :to="userPage(note.user)">
|
||||
<MkUserName :user="note.user"/>
|
||||
<span class="_nowrap">{{ note.user.name ?? note.user.username }}</span>
|
||||
</MkA>
|
||||
</template>
|
||||
</I18n>
|
||||
@@ -39,7 +39,7 @@
|
||||
<div class="body">
|
||||
<div class="top">
|
||||
<MkA v-user-preview="appearNote.user.id" class="name" :to="userPage(appearNote.user)">
|
||||
<MkUserName :nowrap="false" :user="appearNote.user"/>
|
||||
<span class="_nowrap">{{ appearNote.user.name ?? appearNote.user.username }}</span>
|
||||
</MkA>
|
||||
<span v-if="appearNote.user.isBot" class="is-bot">bot</span>
|
||||
<div class="info">
|
||||
@@ -125,7 +125,7 @@
|
||||
<I18n :src="i18n.ts.userSaysSomething" tag="small">
|
||||
<template #name>
|
||||
<MkA v-user-preview="appearNote.userId" class="name" :to="userPage(appearNote.user)">
|
||||
<MkUserName :user="appearNote.user"/>
|
||||
<span class="_nowrap">{{ appearNote.user.name ?? appearNote.user.username }}</span>
|
||||
</MkA>
|
||||
</template>
|
||||
</I18n>
|
||||
|
@@ -1,10 +1,13 @@
|
||||
<template>
|
||||
<header :class="$style.root">
|
||||
<MkA v-once v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)">
|
||||
<MkUserName :user="note.user"/>
|
||||
<span class="_nowrap">{{ note.user.name ?? note.user.username }}</span>
|
||||
</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>
|
||||
|
@@ -3,7 +3,7 @@
|
||||
<MkAvatar :class="$style.avatar" :user="$i" link preview/>
|
||||
<div :class="$style.main">
|
||||
<div :class="$style.header">
|
||||
<MkUserName :user="$i"/>
|
||||
<span class="_nowrap">{{ $i.name ?? $i.username }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div :class="$style.content">
|
||||
|
@@ -31,7 +31,7 @@
|
||||
<header :class="$style.header">
|
||||
<span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span>
|
||||
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
|
||||
<MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
||||
<MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><span class="_nowrap">{{ notification.user.name ?? notification.user.username }}</span></MkA>
|
||||
<span v-else>{{ notification.header }}</span>
|
||||
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
|
||||
</header>
|
||||
@@ -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>
|
||||
|
@@ -8,7 +8,7 @@
|
||||
<div :class="$style.users">
|
||||
<div v-for="u in users" :key="u.id" :class="$style.user">
|
||||
<MkAvatar :class="$style.avatar" :user="u"/>
|
||||
<MkUserName :user="u" :nowrap="true"/>
|
||||
<span class="_nowrap">{{ u.name ?? u.username }}</span>
|
||||
</div>
|
||||
<div v-if="users.length > 10">+{{ count - 10 }}</div>
|
||||
</div>
|
||||
|
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<div v-if="playerEnabled" :class="$style.player" :style="`padding: ${(player.height || 0) / (player.width || 1) * 100}% 0 0`">
|
||||
<button :class="$style.disablePlayer" :title="i18n.ts.disablePlayer" @click="playerEnabled = false"><i class="ti ti-x"></i></button>
|
||||
<iframe :class="$style.playerIframe" :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen/>
|
||||
<iframe v-if="player.url.startsWith('http://') || player.url.startsWith('https://')" :class="$style.playerIframe" :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen/>
|
||||
<span v-else>invalid url</span>
|
||||
</div>
|
||||
<div v-else-if="tweetId && tweetExpanded" ref="twitter" :class="$style.twitter">
|
||||
<iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${$store.state.darkMode ? 'dark' : 'light'}&id=${tweetId}`"></iframe>
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<div v-adaptive-bg :class="[$style.root, { yellow: user.isSilenced, red: user.isSuspended, gray: false }]">
|
||||
<MkAvatar class="avatar" :user="user" indicator/>
|
||||
<div class="body">
|
||||
<span class="name"><MkUserName class="name" :user="user"/></span>
|
||||
<span class="name _nowrap">{{ user.name ?? user.username }}</span>
|
||||
<span class="sub"><span class="acct _monospace">@{{ acct(user) }}</span></span>
|
||||
</div>
|
||||
<MkMiniChart v-if="chartValues" class="chart" :src="chartValues"/>
|
||||
|
@@ -3,8 +3,9 @@
|
||||
<div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div>
|
||||
<MkAvatar class="avatar" :user="user" indicator/>
|
||||
<div class="title">
|
||||
<MkA class="name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA>
|
||||
<p class="username"><MkAcct :user="user"/></p>
|
||||
<MkA class="name _nowrap" :to="userPage(user)"{{ user.name ?? user.username }}</mk-a>
|
||||
<p class="username"><MkAcct :user="user"/></p>
|
||||
</mka>
|
||||
</div>
|
||||
<span v-if="$i && $i.id !== user.id && user.isFollowed" class="followed">{{ $ts.followsYou }}</span>
|
||||
<div class="description">
|
||||
|
@@ -7,7 +7,7 @@
|
||||
</div>
|
||||
<MkAvatar class="avatar" :user="user" indicator/>
|
||||
<div class="title">
|
||||
<MkA class="name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA>
|
||||
<MkA class="name _nowrap" :to="userPage(user)">{{ user.name ?? user.username }}</MkA>
|
||||
<p class="username"><MkAcct :user="user"/></p>
|
||||
</div>
|
||||
<div class="description">
|
||||
|
@@ -27,7 +27,7 @@
|
||||
<div v-for="user in users" :key="user.id" class="_button" :class="[$style.user, { [$style.selected]: selected && selected.id === user.id }]" @click="selected = user" @dblclick="ok()">
|
||||
<MkAvatar :user="user" :class="$style.avatar" indicator/>
|
||||
<div :class="$style.userBody">
|
||||
<MkUserName :user="user" :class="$style.userName"/>
|
||||
<span :class="$style.userName" class="_nowrap">{{ user.name ?? user.username }}</span>
|
||||
<MkAcct :user="user" :class="$style.userAcct"/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -41,7 +41,7 @@
|
||||
<div v-for="user in recentUsers" :key="user.id" class="_button" :class="[$style.user, { [$style.selected]: selected && selected.id === user.id }]" @click="selected = user" @dblclick="ok()">
|
||||
<MkAvatar :user="user" :class="$style.avatar" indicator/>
|
||||
<div :class="$style.userBody">
|
||||
<MkUserName :user="user" :class="$style.userName"/>
|
||||
<span :class="$style.userName" class="_nowrap">{{ user.name ?? user.username }}</span>
|
||||
<MkAcct :user="user" :class="$style.userAcct"/>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -3,7 +3,7 @@
|
||||
<div :class="$style.root">
|
||||
<div v-for="u in users" :key="u.id" :class="$style.user">
|
||||
<MkAvatar :class="$style.avatar" :user="u"/>
|
||||
<MkUserName :class="$style.name" :user="u" :nowrap="true"/>
|
||||
<span :class="$style.name" class="_nowrap">{{ u.name ?? u.username }}</span>
|
||||
</div>
|
||||
<div v-if="users.length < count" :class="$style.omitted">+{{ count - users.length }}</div>
|
||||
</div>
|
||||
|
@@ -7,9 +7,10 @@
|
||||
|
||||
<div class="poamfof">
|
||||
<Transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
|
||||
<div v-if="player.url" class="player">
|
||||
<div v-if="player.url && (player.url.startsWith('http://') || player.url.startsWith('https://'))" class="player">
|
||||
<iframe v-if="!fetching" :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen/>
|
||||
</div>
|
||||
<span v-else>invalid url</span>
|
||||
</Transition>
|
||||
<MkLoading v-if="fetching"/>
|
||||
<MkError v-else-if="!player.url" @retry="ytFetch()"/>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<MfmCore :text="text" :plain="plain" :nowrap="nowrap" :author="author" :is-note="isNote" :class="[$style.root, { [$style.nowrap]: nowrap }]"/>
|
||||
<MfmCore :text="text" :plain="plain" :nowrap="nowrap" :author="author" :is-note="isNote" :class="[$style.root, { '_nowrap': nowrap }]"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -160,12 +160,5 @@ const props = withDefaults(defineProps<{
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
white-space: pre-wrap;
|
||||
|
||||
&.nowrap {
|
||||
white-space: pre;
|
||||
word-wrap: normal; // https://codeday.me/jp/qa/20190424/690106.html
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -9,7 +9,7 @@
|
||||
<i v-else-if="metadata.icon" :class="[$style.titleIcon, metadata.icon]"></i>
|
||||
|
||||
<div :class="$style.title">
|
||||
<MkUserName v-if="metadata.userName" :user="metadata.userName" :nowrap="true"/>
|
||||
<span v-if="metadata.userName" class="_nowrap">{{ metadata.userName.name ?? metadata.userName.username }}</span>
|
||||
<div v-else-if="metadata.title">{{ metadata.title }}</div>
|
||||
<div v-if="!narrow && metadata.subtitle" :class="$style.subtitle">
|
||||
{{ metadata.subtitle }}
|
||||
|
@@ -1,15 +0,0 @@
|
||||
<template>
|
||||
<Mfm :text="user.name ?? user.username" :author="user" :plain="true" :nowrap="nowrap" :emoji-urls="user.emojis"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
user: misskey.entities.User;
|
||||
nowrap?: boolean;
|
||||
}>(), {
|
||||
nowrap: true,
|
||||
});
|
||||
</script>
|
@@ -6,7 +6,6 @@ import MkAcct from './global/MkAcct.vue';
|
||||
import MkAvatar from './global/MkAvatar.vue';
|
||||
import MkEmoji from './global/MkEmoji.vue';
|
||||
import MkCustomEmoji from './global/MkCustomEmoji.vue';
|
||||
import MkUserName from './global/MkUserName.vue';
|
||||
import MkEllipsis from './global/MkEllipsis.vue';
|
||||
import MkTime from './global/MkTime.vue';
|
||||
import MkUrl from './global/MkUrl.vue';
|
||||
@@ -28,7 +27,6 @@ export default function(app: App) {
|
||||
app.component('MkAvatar', MkAvatar);
|
||||
app.component('MkEmoji', MkEmoji);
|
||||
app.component('MkCustomEmoji', MkCustomEmoji);
|
||||
app.component('MkUserName', MkUserName);
|
||||
app.component('MkEllipsis', MkEllipsis);
|
||||
app.component('MkTime', MkTime);
|
||||
app.component('MkUrl', MkUrl);
|
||||
@@ -50,7 +48,6 @@ declare module '@vue/runtime-core' {
|
||||
MkAvatar: typeof MkAvatar;
|
||||
MkEmoji: typeof MkEmoji;
|
||||
MkCustomEmoji: typeof MkCustomEmoji;
|
||||
MkUserName: typeof MkUserName;
|
||||
MkEllipsis: typeof MkEllipsis;
|
||||
MkTime: typeof MkTime;
|
||||
MkUrl: typeof MkUrl;
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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,
|
||||
});
|
||||
|
@@ -8,7 +8,7 @@
|
||||
<Mfm :text="clip.description" :is-note="false" :i="$i"/>
|
||||
</div>
|
||||
<div class="user">
|
||||
<MkAvatar :user="clip.user" class="avatar" indicator link preview/> <MkUserName :user="clip.user" :nowrap="false"/>
|
||||
<MkAvatar :user="clip.user" class="avatar" indicator link preview/> <span class="_nowrap">{{ clip.user.name ?? clip.user.username }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@@ -155,7 +155,11 @@ async function run() {
|
||||
os.inputText({
|
||||
title: q,
|
||||
}).then(({ canceled, result: a }) => {
|
||||
ok(a);
|
||||
if (canceled) {
|
||||
ok('');
|
||||
} else {
|
||||
ok(a);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@@ -15,7 +15,7 @@
|
||||
<MkAvatar class="avatar" :user="req.follower" indicator link preview/>
|
||||
<div class="body">
|
||||
<div class="name">
|
||||
<MkA v-user-preview="req.follower.id" class="name" :to="userPage(req.follower)"><MkUserName :user="req.follower"/></MkA>
|
||||
<MkA v-user-preview="req.follower.id" class="name _nowrap" :to="userPage(req.follower)">{{ req.follower.name ?? req.follower.username }}</MkA>
|
||||
<p class="acct">@{{ acct(req.follower) }}</p>
|
||||
</div>
|
||||
<div v-if="req.follower.description" class="description" :title="req.follower.description">
|
||||
|
@@ -30,7 +30,7 @@
|
||||
<div class="user">
|
||||
<MkAvatar :user="post.user" class="avatar" link preview/>
|
||||
<div class="name">
|
||||
<MkUserName :user="post.user" style="display: block;"/>
|
||||
<div class="_nowrap">{{ post.user.name ?? post.user.username }}</div>
|
||||
<MkAcct :user="post.user"/>
|
||||
</div>
|
||||
<MkFollowButton v-if="!$i || $i.id != post.user.id" :user="post.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
|
||||
|
@@ -22,7 +22,7 @@
|
||||
<MkTime :time="message.createdAt" class="time"/>
|
||||
</header>
|
||||
<header v-else>
|
||||
<span class="name"><MkUserName :user="isMe(message) ? message.recipient : message.user"/></span>
|
||||
<span class="name">{{ isMe(message) ? message.recipient.name : message.user.name }}</span>
|
||||
<span class="username">@{{ acct(isMe(message) ? message.recipient : message.user) }}</span>
|
||||
<MkTime :time="message.createdAt" class="time"/>
|
||||
</header>
|
||||
|
@@ -21,7 +21,7 @@
|
||||
<div v-for="user in users" :key="user.id" class="user _panel">
|
||||
<MkAvatar :user="user" class="avatar" indicator link preview/>
|
||||
<div class="body">
|
||||
<MkUserName :user="user" class="name"/>
|
||||
<span class="name _nowrap">{{ user.name ?? user.username }}</span>
|
||||
<MkAcct :user="user" class="acct"/>
|
||||
</div>
|
||||
<div class="action">
|
||||
|
@@ -21,7 +21,7 @@
|
||||
<b>{{ item.name }}</b>
|
||||
<div v-if="item.description" class="description">{{ item.description }}</div>
|
||||
<div class="user">
|
||||
<MkAvatar :user="item.user" class="avatar" indicator link preview/> <MkUserName :user="item.user" :nowrap="false"/>
|
||||
<MkAvatar :user="item.user" class="avatar" indicator link preview/> <span class="_nowrap">{{ item.user.name ?? item.user.username }}</span>
|
||||
</div>
|
||||
</MkA>
|
||||
</div>
|
||||
|
@@ -29,7 +29,7 @@
|
||||
<div class="user">
|
||||
<MkAvatar :user="page.user" class="avatar" link preview/>
|
||||
<div class="name">
|
||||
<MkUserName :user="page.user" style="display: block;"/>
|
||||
<div class="_nowrap">{{ page.user.name ?? page.user.username }}</div>
|
||||
<MkAcct :user="page.user"/>
|
||||
</div>
|
||||
<MkFollowButton v-if="!$i || $i.id != page.user.id" :user="page.user" :inline="true" :transparent="false" :full="true" large class="koudoku"/>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user