diff --git a/.dockerignore b/.dockerignore index 854e643d393b0db52077a639fa9f56f332b31449..8f984831ef86c7d422e53886931c7626f403a412 100644 --- a/.dockerignore +++ b/.dockerignore @@ -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 diff --git a/.dockleignore b/.dockleignore new file mode 100644 index 0000000000000000000000000000000000000000..2f932664564137f205d5e901ce02cdce2e716569 --- /dev/null +++ b/.dockleignore @@ -0,0 +1,3 @@ +DKL-DI-0005 +DKL-DI-0006 +DKL-LI-0003 diff --git a/.github/workflows/docker-develop.yml b/.github/workflows/docker-develop.yml index 63dc940e24c4bf3fae65c9d291f83a278a18ab50..a999dc51e61d50feded70b3870cff6c5c453a8b2 100644 --- a/.github/workflows/docker-develop.yml +++ b/.github/workflows/docker-develop.yml @@ -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 diff --git a/.github/workflows/dockle.yml b/.github/workflows/dockle.yml new file mode 100644 index 0000000000000000000000000000000000000000..9b79ee54f0ac3ea5e02e102ba4a426e629480c37 --- /dev/null +++ b/.github/workflows/dockle.yml @@ -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}" diff --git a/CHANGELOG.md b/CHANGELOG.md index 914bde051ca7763f1cbc4d695c4b803329a978db..71426953508d7de2338efa3b45f43c804625b57f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,23 @@ You should also include the user name that made the change. --> +## 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) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 811e4219e5c667850e4d98a2ad20cbc3534d09a2..e5399267894d6fe5e8c420e5b9d4a9e9ba015ba9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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`. diff --git a/Dockerfile b/Dockerfile index 3876b5f6ce2e758f38b2be1acd084d2edffc82ed..89a8d38f8c1cf6257c906d33f2b63835b31de0f6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,9 @@ 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 -yqq --no-install-recommends \ - build-essential + build-essential wget ca-certificates \ + && wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq \ + && chmod +x /usr/bin/yq RUN corepack enable @@ -29,6 +31,7 @@ ARG NODE_ENV=production RUN git submodule update --init RUN pnpm build +RUN rm -rf .git/ FROM node:${NODE_VERSION}-slim AS runner @@ -44,11 +47,14 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ ffmpeg tini \ && 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 +COPY --from=builder /usr/bin/yq /usr/bin/yq COPY --chown=misskey:misskey --from=builder /misskey/node_modules ./node_modules COPY --chown=misskey:misskey --from=builder /misskey/built ./built COPY --chown=misskey:misskey --from=builder /misskey/packages/backend/node_modules ./packages/backend/node_modules @@ -58,5 +64,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"] diff --git a/healthcheck.sh b/healthcheck.sh new file mode 100644 index 0000000000000000000000000000000000000000..f8e598b28233bee1c0e765b67b4931aed498f02d --- /dev/null +++ b/healthcheck.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +PORT=$(yq '.port' /misskey/.config/default.yml) +curl -s -S -o /dev/null "http://localhost:${PORT}" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 423116416a4e2d52633df06e4895b0a2dd2e743b..dd1494fb216d8d21a0cac1bd1af3149b2a54b722 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -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" diff --git a/locales/en-US.yml b/locales/en-US.yml index 4fd3bf3f0d50f21b96b184b64a79372a8e762439..0c39a5e35657e72af0357bd8aa61c6eb88f6e725 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -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" diff --git a/locales/es-ES.yml b/locales/es-ES.yml index 3e062ba51bf9ade855be05c5025ead3cab810893..ba1c717f856b47e26de7136d6b19419bf2ae97f8 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -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" diff --git a/locales/index.js b/locales/index.js index 92cd9b467c28670839eb07298c62c3c620d8eb1d..2248bb6ac976c9728a1164d7fa83b87fe1a098c3 100644 --- a/locales/index.js +++ b/locales/index.js @@ -34,6 +34,7 @@ const languages = [ 'pt-PT', 'ru-RU', 'sk-SK', + 'th-TH', 'ug-CN', 'uk-UA', 'vi-VN', diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index a241d54b47f41144388e898c3deda2ff1ef5afc3..6286367b50f62854dcd9227aea28b7259dcc10f8 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -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: "優先度" diff --git a/locales/lo-LA.yml b/locales/lo-LA.yml new file mode 100644 index 0000000000000000000000000000000000000000..a46ddcc10f983c3af5000077482eaae15babfcdd --- /dev/null +++ b/locales/lo-LA.yml @@ -0,0 +1,2 @@ +--- +_lang_: "ພາສາລາວ" diff --git a/locales/th-TH.yml b/locales/th-TH.yml index ca23e44fa46f557d572b9481b92e1375c6e547e6..8087090f5afde68a6331abf84920e9b5d463d7c1 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -1195,6 +1195,9 @@ _role: baseRole: "บทบาทพื้นà¸à¸²à¸™" useBaseValue: "ใช้บทบาทพื้นà¸à¸²à¸™à¹€à¸£à¸´à¹ˆà¸¡à¸•à¹‰à¸™" chooseRoleToAssign: "เลืà¸à¸à¸šà¸—บาทที่ต้à¸à¸‡à¸à¸²à¸£à¸à¸³à¸«à¸™à¸”" + iconUrl: "ไà¸à¸„à¸à¸™ URL" + asBadge: "à¹à¸ªà¸”งเป็นตรา" + descriptionOfAsBadge: "ไà¸à¸„à¸à¸™à¸‚à¸à¸‡à¸šà¸—บาทนี้จะปราà¸à¸à¸–ัดจาà¸à¸Šà¸·à¹ˆà¸à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸‚à¸à¸‡à¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¸‡à¸²à¸™à¸”้วยบทบาทนี้ถ้าหาà¸à¹€à¸›à¸´à¸”ใช้งาน" canEditMembersByModerator: "à¸à¸™à¸¸à¸à¸²à¸•à¹ƒà¸«à¹‰à¸œà¸¹à¹‰à¸”ูà¹à¸¥à¹à¸à¹‰à¹„ขสมาชิà¸" descriptionOfCanEditMembersByModerator: "เมื่à¸à¹€à¸›à¸´à¸”ใช้ ผู้ดูà¹à¸¥à¸™à¸à¸à¹€à¸«à¸™à¸·à¸à¸ˆà¸²à¸à¸œà¸¹à¹‰à¸”ูà¹à¸¥à¸£à¸°à¸šà¸šà¹à¸¥à¹‰à¸§ จะสามารถà¸à¸³à¸«à¸™à¸”à¹à¸¥à¸°à¸¢à¸à¹€à¸¥à¸´à¸à¸à¸²à¸£à¸¡à¸à¸šà¸«à¸¡à¸²à¸¢à¸šà¸—บาทนี้ให้à¸à¸±à¸šà¸œà¸¹à¹‰à¹ƒà¸Šà¹‰à¹„ด้ เมื่à¸à¸›à¸´à¸” เฉพาะผู้ดูà¹à¸¥à¸£à¸°à¸šà¸šà¹€à¸—่านั้นที่จะสามารถà¸à¸³à¸«à¸™à¸”ผู้ใช้ได้นะ" priority: "ลำดับความสำคัà¸" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index bc29aba0a010887401f55a9ae7d1442a6c5593c0..7796dc3de1980df4f9e5ffcf37bcaeee97c24e8e 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -1195,6 +1195,9 @@ _role: baseRole: "基本角色" useBaseValue: "使用基本角色的值" chooseRoleToAssign: "选择è¦åˆ†é…的角色" + iconUrl: "å›¾æ ‡URL" + asBadge: "ä½œä¸ºå¾½ç« æ˜¾ç¤º" + descriptionOfAsBadge: "å¼€å¯åŽï¼Œç”¨æˆ·åæ—è¾¹å°†ä¼šå‡ºçŽ°è§’è‰²å›¾æ ‡ã€‚" canEditMembersByModerator: "å…许监察者编辑æˆå‘˜" descriptionOfCanEditMembersByModerator: "如果选ä¸ï¼Œç›‘察者和管ç†å‘˜éƒ½èƒ½å¤Ÿä¸ºç”¨æˆ·åˆ†é…/å–消分é…角色。如果未选ä¸ï¼Œåˆ™åªæœ‰ç®¡ç†å‘˜å¯ä»¥æ‰§è¡Œæ¤æ“作。" priority: "优先级" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 058ee416ef86d49248920cc31524598d77f29997..74f3c237faf092b4fe0844c845a57b1ce9fd76fa 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -1195,6 +1195,9 @@ _role: baseRole: "基本角色" useBaseValue: "使用基本角色的值" chooseRoleToAssign: "é¸æ“‡è¦æŒ‡æ´¾çš„角色" + iconUrl: "圖示的URL" + asBadge: "é¡¯ç¤ºç‚ºå¾½ç« " + descriptionOfAsBadge: "開啟的話,角色圖示會顯示在用戶åæ—邊。" canEditMembersByModerator: "å…許編輯監察員的æˆå“¡" descriptionOfCanEditMembersByModerator: "如果開啟,管ç†å“¡èˆ‡ç›£å¯Ÿå“¡éƒ½å¯ä»¥ç‚ºä½¿ç”¨è€…指派/解除指派該角色。如果關閉,則åªæœ‰ç®¡ç†å“¡å¯ä»¥åŸ·è¡Œã€‚" priority: "優先級" diff --git a/package.json b/package.json index 2c33c5a04c11d4cb90960fd9fd4483b9f3458a96..6e0414ec08fc482208f44db29ff58f12ca88082b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "13.3.4", + "version": "13.4.0", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/backend/migration/1675557528704-role-icon-badge.js b/packages/backend/migration/1675557528704-role-icon-badge.js new file mode 100644 index 0000000000000000000000000000000000000000..0ebca088e3228308f0668ed0fb355d57297a4337 --- /dev/null +++ b/packages/backend/migration/1675557528704-role-icon-badge.js @@ -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"`); + } +} diff --git a/packages/backend/src/core/DownloadService.ts b/packages/backend/src/core/DownloadService.ts index a971e06fd830911f6676ad62979a7d48088601c9..852c1f32e3a592aafb26a6f0e7053bd625b334c0 100644 --- a/packages/backend/src/core/DownloadService.ts +++ b/packages/backend/src/core/DownloadService.ts @@ -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)) { diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index f8f9231cdda3112b059907667a2640927d6ff024..d15d8c0aeead0660bd9e0f2288a51532efc128a9 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -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(); diff --git a/packages/backend/src/core/entities/RoleEntityService.ts b/packages/backend/src/core/entities/RoleEntityService.ts index 52f33744682d35a2a80880e6ec391c27151ecafa..dbb89ff19b87ac59ad4dd13b22836f6667dddaf9 100644 --- a/packages/backend/src/core/entities/RoleEntityService.ts +++ b/packages/backend/src/core/entities/RoleEntityService.ts @@ -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, diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index ff42c0735957f0e7fb1ae2a7d94c1d8033e0926b..eea9d5567d16b399e7371429c8f3f1877a3565ef 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -415,6 +415,11 @@ export class UserEntityService implements OnModuleInit { } : undefined) : undefined, emojis: this.customEmojiService.populateEmojis(user.emojis, user.host), onlineStatus: this.getOnlineStatus(user), + // パフォーマンス上ã®ç†ç”±ã§ãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ã¿ + badgeRoles: user.host == null ? this.roleService.getUserBadgeRoles(user.id).then(rs => rs.map(r => ({ + name: r.name, + iconUrl: r.iconUrl, + }))) : undefined, ...(opts.detail ? { url: profile!.url, @@ -454,6 +459,7 @@ export class UserEntityService implements OnModuleInit { id: role.id, name: role.name, color: role.color, + iconUrl: role.iconUrl, description: role.description, isModerator: role.isModerator, isAdministrator: role.isAdministrator, diff --git a/packages/backend/src/models/entities/Role.ts b/packages/backend/src/models/entities/Role.ts index abd5f864a27b6679378b969bf9577d427d3670b5..8cf6811863b420db2c6faf741b8197ffcc377626 100644 --- a/packages/backend/src/models/entities/Role.ts +++ b/packages/backend/src/models/entities/Role.ts @@ -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, }) diff --git a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts index 87b23f1891c26e22b62d2159d92723dc853ee6e6..df024a8f3ccc650ef274ec54f447dc5c16913d04 100644 --- a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts @@ -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; diff --git a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts index 0061c2a8f7228ed33ebb483ba461be2bffbbb3f4..2d43615e25797cc7402ee0d3328d6f48a4e6bcde 100644 --- a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts @@ -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({ diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index 39bc4c1d969a55088130cd00aa11e12f83b2dba2..4bd6d0f55686a2f441e38cd665212177f4837650 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -146,6 +146,8 @@ export class FileServerService { const url = new URL(`${this.config.mediaProxy}/static.webp`); url.searchParams.set('url', file.url); url.searchParams.set('static', '1'); + + file.cleanup(); return await reply.redirect(301, url.toString()); } else if (file.mime.startsWith('video/')) { image = await this.videoProcessingService.generateVideoThumbnail(file.path); @@ -158,6 +160,8 @@ export class FileServerService { const url = new URL(`${this.config.mediaProxy}/svg.webp`); url.searchParams.set('url', file.url); + + file.cleanup(); return await reply.redirect(301, url.toString()); } } diff --git a/packages/backend/src/server/api/endpoints/admin/roles/create.ts b/packages/backend/src/server/api/endpoints/admin/roles/create.ts index f136c6d62439e98fbbdecee46042bd083aa0fb71..1a2a9fb7470e5d78eba6c5afbff0f7a8622deb88 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/create.ts @@ -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])); diff --git a/packages/backend/src/server/api/endpoints/admin/roles/update.ts b/packages/backend/src/server/api/endpoints/admin/roles/update.ts index fc4c3d8f1175d58479257d4a9305db90a87f223f..c9f4a9fed8f0104839bfa785f85ed4fab2e4d493 100644 --- a/packages/backend/src/server/api/endpoints/admin/roles/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/roles/update.ts @@ -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, }); diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts index e423f0f10967baa362c795674c761798dcd29ef2..0ce80a1a63e8faefc1da1877e6a51c47e946364f 100644 --- a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts @@ -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'); } }); diff --git a/packages/frontend/src/components/MkCode.core.vue b/packages/frontend/src/components/MkCode.core.vue index b074028821ba472cbcdfddf034151080b283005e..b656307d902c047930a3f9799e5ef8382aa6e5d2 100644 --- a/packages/frontend/src/components/MkCode.core.vue +++ b/packages/frontend/src/components/MkCode.core.vue @@ -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> diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue index cb88444d346bf224ee076a934ec2293ee004543d..4525d3a009df25f8f2fa31a8dd1c70a2ce7196b8 100644 --- a/packages/frontend/src/components/MkDateSeparatedList.vue +++ b/packages/frontend/src/components/MkDateSeparatedList.vue @@ -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; } diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue index 8771168a423ac358fb95fb8c3ab1e14703820776..6b43f146650b2fc0d7d5c57a804a66e87712eacd 100644 --- a/packages/frontend/src/components/MkNoteHeader.vue +++ b/packages/frontend/src/components/MkNoteHeader.vue @@ -5,6 +5,9 @@ </MkA> <div v-if="note.user.isBot" :class="$style.isBot">bot</div> <div :class="$style.username"><MkAcct :user="note.user"/></div> + <div v-if="note.user.badgeRoles" :class="$style.badgeRoles"> + <img v-for="role in note.user.badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl"/> + </div> <div :class="$style.info"> <MkA :to="notePage(note)"> <MkTime :time="note.createdAt"/> @@ -77,4 +80,17 @@ defineProps<{ margin-left: auto; font-size: 0.9em; } + +.badgeRoles { + margin: 0 .5em 0 0; +} + +.badgeRole { + height: 1.3em; + vertical-align: -20%; + + & + .badgeRole { + margin-left: .125em; + } +} </style> diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index b51d456eab50b1f08e248a7e88aa0a61d89b7eee..e7a951dd27d68519f3ce572bddc28cb02fa1541e 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -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> diff --git a/packages/frontend/src/init.ts b/packages/frontend/src/init.ts index 4227f5cf4a3587155d6d602f9d22d3f4197977d5..64c252ce552c2f26b67844ea755be19c6afec9c2 100644 --- a/packages/frontend/src/init.ts +++ b/packages/frontend/src/init.ts @@ -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); diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index ae5ef39bae36f788089671d8c9b8b82954c6ee8f..086537a94a75233ae67fd3c1eaf47cbec4cd3107 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -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, }); diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue index 0e785f259cc8160c2a73ecb65d886f9a531b6d0d..c82559d55a3572b8fec8570d082cb1e479930067 100644 --- a/packages/frontend/src/pages/flash/flash.vue +++ b/packages/frontend/src/pages/flash/flash.vue @@ -155,7 +155,11 @@ async function run() { os.inputText({ title: q, }).then(({ canceled, result: a }) => { - ok(a); + if (canceled) { + ok(''); + } else { + ok(a); + } }); }); }, diff --git a/packages/frontend/src/pages/scratchpad.vue b/packages/frontend/src/pages/scratchpad.vue index 0d52850b5d38936b49ab7e0ddac46ccd61003718..6075dde32623fa92bc9da809a1ec906124123664 100644 --- a/packages/frontend/src/pages/scratchpad.vue +++ b/packages/frontend/src/pages/scratchpad.vue @@ -86,7 +86,11 @@ async function run() { os.inputText({ title: q, }).then(({ canceled, result: a }) => { - ok(a); + if (canceled) { + ok(''); + } else { + ok(a); + } }); }); }, diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue index 7a819eb9f0859e9411ff2318aed5eb505294acdf..a01e3f8cee4b74a1f26c0f94e3833509b5608398 100644 --- a/packages/frontend/src/pages/settings/webhook.edit.vue +++ b/packages/frontend/src/pages/settings/webhook.edit.vue @@ -31,6 +31,7 @@ <div class="_buttons"> <MkButton primary inline @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton> + <MkButton danger inline @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> </div> </div> </template> @@ -44,6 +45,9 @@ import MkButton from '@/components/MkButton.vue'; import * as os from '@/os'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; +import { useRouter } from '@/router'; + +const router = useRouter(); const props = defineProps<{ webhookId: string; @@ -86,6 +90,19 @@ async function save(): Promise<void> { }); } +async function del(): Promise<void> { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.t('deleteAreYouSure', { x: webhook.name }), + }); + if (canceled) return; + + await os.apiWithDialog('i/webhooks/delete', { + webhookId: props.webhookId, + }); + + router.push('/settings/webhook'); +} const headerActions = $computed(() => []); const headerTabs = $computed(() => []); diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index 59dc1114d1c8e143136d0780d7149f7d81ff5060..080772951e636238601960f84e07ff6ef934c890 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -1,9 +1,9 @@ <template> <MkStickyContainer> - <template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="headerTabs" :display-my-avatar="true"/></template> + <template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :display-my-avatar="true"/></template> <MkSpacer :content-max="800"> <div ref="rootEl" v-hotkey.global="keymap"> - <XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="_panel" style="margin-bottom: var(--margin);"/> + <XTutorial v-if="$i && $store.reactiveState.tutorial.value != -1" class="_panel" style="margin-bottom: var(--margin);"/> <XPostForm v-if="$store.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/> <div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div> @@ -45,7 +45,8 @@ const tlComponent = $shallowRef<InstanceType<typeof XTimeline>>(); const rootEl = $shallowRef<HTMLElement>(); let queue = $ref(0); -const src = $computed({ get: () => defaultStore.reactiveState.tl.value.src, set: (x) => saveSrc(x) }); +let srcWhenNotSignin = $ref(isLocalTimelineAvailable ? 'local' : 'global'); +const src = $computed({ get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin), set: (x) => saveSrc(x) }); watch ($$(src), () => queue = 0); @@ -94,6 +95,7 @@ function saveSrc(newSrc: 'home' | 'local' | 'social' | 'global'): void { ...defaultStore.state.tl, src: newSrc, }); + srcWhenNotSignin = newSrc; } async function timetravel(): Promise<void> { @@ -148,6 +150,21 @@ const headerTabs = $computed(() => [{ onClick: chooseChannel, }]); +const headerTabsWhenNotLogin = $computed(() => [ + ...(isLocalTimelineAvailable ? [{ + key: 'local', + title: i18n.ts._timelines.local, + icon: 'ti ti-planet', + iconOnly: true, + }] : []), + ...(isGlobalTimelineAvailable ? [{ + key: 'global', + title: i18n.ts._timelines.global, + icon: 'ti ti-whirl', + iconOnly: true, + }] : []), +]); + definePageMetadata(computed(() => ({ title: i18n.ts.timeline, icon: src === 'local' ? 'ti ti-planet' : src === 'social' ? 'ti ti-rocket' : src === 'global' ? 'ti ti-whirl' : 'ti ti-home', diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index c960b312745b0515f96230822f1cac89d6f94376..56858a9377c11e7ed12cd925ba398c0896340550 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -39,7 +39,10 @@ </div> </div> <div v-if="user.roles.length > 0" class="roles"> - <span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color }">{{ role.name }}</span> + <span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color }"> + <img v-if="role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="role.iconUrl"/> + {{ role.name }} + </span> </div> <div class="description"> <MkOmit> diff --git a/packages/frontend/src/plugin.ts b/packages/frontend/src/plugin.ts index c19fe2b08d653355b0069ad84db0c8ada4edce9f..17eb99be22447f1d48999c38fbb52d71737977ce 100644 --- a/packages/frontend/src/plugin.ts +++ b/packages/frontend/src/plugin.ts @@ -20,7 +20,11 @@ export function install(plugin) { inputText({ title: q, }).then(({ canceled, result: a }) => { - ok(a); + if (canceled) { + ok(''); + } else { + ok(a); + } }); }); }, diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index 595b1f622aacb69b63f012554e444a0252190a3f..87d42c5c87ce5d382a55e9fc232d7632ab9a4ded 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -484,6 +484,9 @@ export const routes = [{ path: '/clicker', component: page(() => import('./pages/clicker.vue')), loginRequired: true, +}, { + path: '/timeline', + component: page(() => import('./pages/timeline.vue')), }, { name: 'index', path: '/', diff --git a/packages/frontend/src/scripts/aiscript/api.ts b/packages/frontend/src/scripts/aiscript/api.ts index 12f00bd32ba457ae89cf12b70388ffb033fba6c1..1b47eaa42001aa0169ac811fdc92ff3d85b2e035 100644 --- a/packages/frontend/src/scripts/aiscript/api.ts +++ b/packages/frontend/src/scripts/aiscript/api.ts @@ -27,7 +27,11 @@ export function createAiScriptEnv(opts) { return confirm.canceled ? values.FALSE : values.TRUE; }), 'Mk:api': values.FN_NATIVE(async ([ep, param, token]) => { - if (token) utils.assertString(token); + if (token) { + utils.assertString(token); + // ãƒã‚°ãŒã‚ã‚Œã°undefinedã‚‚ã‚ã‚Šå¾—ã‚‹ãŸã‚念ã®ãŸã‚ + if (typeof token.value !== 'string') throw new Error('invalid token'); + } apiRequests++; if (apiRequests > 16) return values.NULL; const res = await os.api(ep.value, utils.valToJs(param), token ? token.value : (opts.token ?? null)); diff --git a/packages/frontend/src/ui/visitor.vue b/packages/frontend/src/ui/visitor.vue index ec9150d346385231be4e53a368b5cf137cb214b4..797e2aa6c3fa45643db422c2076d149fdf8a2b49 100644 --- a/packages/frontend/src/ui/visitor.vue +++ b/packages/frontend/src/ui/visitor.vue @@ -5,14 +5,14 @@ <script lang="ts"> import { defineComponent, defineAsyncComponent } from 'vue'; -import DesignA from './visitor/a.vue'; +//import DesignA from './visitor/a.vue'; import DesignB from './visitor/b.vue'; import XCommon from './_common_/common.vue'; export default defineComponent({ components: { XCommon, - DesignA, + //DesignA, DesignB, }, }); diff --git a/packages/frontend/src/ui/visitor/b.vue b/packages/frontend/src/ui/visitor/b.vue index 9a2320da88c65267fac298d965efbcf46051c03d..058a9482fad36c641cfd61cb6e80b74d51f8ab37 100644 --- a/packages/frontend/src/ui/visitor/b.vue +++ b/packages/frontend/src/ui/visitor/b.vue @@ -10,7 +10,7 @@ <XKanban v-if="narrow && !root" class="banner" :powered-by="root"/> <div class="contents"> - <XHeader v-if="!root" class="header" :info="pageInfo"/> + <XHeader v-if="!root" class="header"/> <main style="container-type: inline-size;"> <RouterView/> </main> @@ -33,9 +33,14 @@ <Transition :name="$store.state.animation ? 'tray' : ''"> <div v-if="showMenu" class="menu"> <MkA to="/" class="link" active-class="active"><i class="ti ti-home icon"></i>{{ $ts.home }}</MkA> + <MkA v-if="isTimelineAvailable" to="/timeline" class="link" active-class="active"><i class="ti ti-message icon"></i>{{ $ts.timeline }}</MkA> <MkA to="/explore" class="link" active-class="active"><i class="ti ti-hash icon"></i>{{ $ts.explore }}</MkA> - <MkA to="/featured" class="link" active-class="active"><i class="ti ti-flare icon"></i>{{ $ts.featured }}</MkA> + <MkA to="/announcements" class="link" active-class="active"><i class="ti ti-speakerphone icon"></i>{{ $ts.announcements }}</MkA> <MkA to="/channels" class="link" active-class="active"><i class="ti ti-device-tv icon"></i>{{ $ts.channel }}</MkA> + <div class="divider"></div> + <MkA to="/pages" class="link" active-class="active"><i class="ti ti-news icon"></i>{{ $ts.pages }}</MkA> + <MkA to="/play" class="link" active-class="active"><i class="ti ti-player-play icon"></i>Play</MkA> + <MkA to="/gallery" class="link" active-class="active"><i class="ti ti-icons icon"></i>{{ $ts.gallery }}</MkA> <div class="action"> <button class="_buttonPrimary" @click="signup()">{{ $ts.signup }}</button> <button class="_button" @click="signin()">{{ $ts.login }}</button> @@ -52,6 +57,7 @@ import XKanban from './kanban.vue'; import { host, instanceName } from '@/config'; import { search } from '@/scripts/search'; import * as os from '@/os'; +import { instance } from '@/instance'; import MkPagination from '@/components/MkPagination.vue'; import XSigninDialog from '@/components/MkSigninDialog.vue'; import XSignupDialog from '@/components/MkSignupDialog.vue'; @@ -76,6 +82,9 @@ const announcements = { endpoint: 'announcements', limit: 10, }; + +const isTimelineAvailable = instance.policies.ltlAvailable || instance.policies.gtlAvailable; + let showMenu = $ref(false); let isDesktop = $ref(window.innerWidth >= DESKTOP_THRESHOLD); let narrow = $ref(window.innerWidth < 1280); @@ -223,6 +232,12 @@ defineExpose({ } } + > .divider { + margin: 8px auto; + width: calc(100% - 32px); + border-top: solid 0.5px var(--divider); + } + > .action { padding: 16px; diff --git a/packages/frontend/src/ui/visitor/header.vue b/packages/frontend/src/ui/visitor/header.vue index 984fd1104407a4860a485dbb0de2acfc1dfd525e..2647d0e62a1dab6c171970831c5e02528eb0073e 100644 --- a/packages/frontend/src/ui/visitor/header.vue +++ b/packages/frontend/src/ui/visitor/header.vue @@ -3,18 +3,9 @@ <div v-if="narrow === false" class="wide"> <div class="content"> <MkA to="/" class="link" active-class="active"><i class="ti ti-home icon"></i>{{ $ts.home }}</MkA> + <MkA v-if="isTimelineAvailable" to="/timeline" class="link" active-class="active"><i class="ti ti-message icon"></i>{{ $ts.timeline }}</MkA> <MkA to="/explore" class="link" active-class="active"><i class="ti ti-hash icon"></i>{{ $ts.explore }}</MkA> - <MkA to="/featured" class="link" active-class="active"><i class="ti ti-flare icon"></i>{{ $ts.featured }}</MkA> <MkA to="/channels" class="link" active-class="active"><i class="ti ti-device-tv icon"></i>{{ $ts.channel }}</MkA> - <div v-if="info" class="page active link"> - <div class="title"> - <i v-if="info.icon" class="icon" :class="info.icon"></i> - <MkAvatar v-else-if="info.avatar" class="avatar" :user="info.avatar" indicator/> - <span v-if="info.title" class="text">{{ info.title }}</span> - <MkUserName v-else-if="info.userName" :user="info.userName" :nowrap="false" class="text"/> - </div> - <button v-if="info.action" class="_button action" @click.stop="info.action.handler"><!-- TODO --></button> - </div> <div class="right"> <button class="_button search" @click="search()"><i class="ti ti-search icon"></i><span>{{ $ts.search }}</span></button> <button class="_buttonPrimary signup" @click="signup()">{{ $ts.signup }}</button> @@ -26,15 +17,6 @@ <button class="menu _button" @click="$parent.showMenu = true"> <i class="ti ti-menu-2 icon"></i> </button> - <div v-if="info" class="title"> - <i v-if="info.icon" class="icon" :class="info.icon"></i> - <MkAvatar v-else-if="info.avatar" class="avatar" :user="info.avatar" indicator/> - <span v-if="info.title" class="text">{{ info.title }}</span> - <MkUserName v-else-if="info.userName" :user="info.userName" :nowrap="false" class="text"/> - </div> - <button v-if="info && info.action" class="action _button" @click.stop="info.action.handler"> - <!-- TODO --> - </button> </div> </div> </template> @@ -44,19 +26,15 @@ import { defineComponent } from 'vue'; import XSigninDialog from '@/components/MkSigninDialog.vue'; import XSignupDialog from '@/components/MkSignupDialog.vue'; import * as os from '@/os'; +import { instance } from '@/instance'; import { search } from '@/scripts/search'; export default defineComponent({ - props: { - info: { - required: true, - }, - }, - data() { return { narrow: null, showMenu: false, + isTimelineAvailable: instance.policies.ltlAvailable || instance.policies.gtlAvailable, }; }, @@ -84,8 +62,9 @@ export default defineComponent({ <style lang="scss" scoped> .sqxihjet { - $height: 60px; + $height: 50px; position: sticky; + width: 50px; top: 0; left: 0; z-index: 1000; diff --git a/packages/frontend/src/widgets/WidgetAiscript.vue b/packages/frontend/src/widgets/WidgetAiscript.vue index 141626955ef7461552f6ae6ebf60e3e39a47a668..9e489227ff513b789ed6f5a79b942cf882cfad00 100644 --- a/packages/frontend/src/widgets/WidgetAiscript.vue +++ b/packages/frontend/src/widgets/WidgetAiscript.vue @@ -72,7 +72,11 @@ const run = async () => { os.inputText({ title: q, }).then(({ canceled, result: a }) => { - ok(a); + if (canceled) { + ok(''); + } else { + ok(a); + } }); }); }, diff --git a/packages/frontend/src/widgets/WidgetAiscriptApp.vue b/packages/frontend/src/widgets/WidgetAiscriptApp.vue index 406fb92d8663408d2e78191745f06289c0aeec34..9a2b60eb05c6c383b37e6e81cd7ebf7f48fdf792 100644 --- a/packages/frontend/src/widgets/WidgetAiscriptApp.vue +++ b/packages/frontend/src/widgets/WidgetAiscriptApp.vue @@ -67,7 +67,11 @@ async function run() { os.inputText({ title: q, }).then(({ canceled, result: a }) => { - ok(a); + if (canceled) { + ok(''); + } else { + ok(a); + } }); }); }, diff --git a/packages/frontend/src/widgets/WidgetButton.vue b/packages/frontend/src/widgets/WidgetButton.vue index 63e8e485e1879e65d24088d899056b3a30247fe8..6c2c366aa2a6383d1dcbc4e0c881c8d17e0f6e0c 100644 --- a/packages/frontend/src/widgets/WidgetButton.vue +++ b/packages/frontend/src/widgets/WidgetButton.vue @@ -60,7 +60,11 @@ const run = async () => { os.inputText({ title: q, }).then(({ canceled, result: a }) => { - ok(a); + if (canceled) { + ok(''); + } else { + ok(a); + } }); }); },