diff --git a/.dockerignore b/.dockerignore index 854e643d3..8f984831e 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 000000000..2f9326645 --- /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 63dc940e2..a999dc51e 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 000000000..9b79ee54f --- /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 914bde051..714269535 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 811e4219e..e53992678 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 3876b5f6c..89a8d38f8 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 000000000..f8e598b28 --- /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 423116416..dd1494fb2 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 4fd3bf3f0..0c39a5e35 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 3e062ba51..ba1c717f8 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 92cd9b467..2248bb6ac 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 a241d54b4..6286367b5 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: "モデレーターは基本的なモデレーションに関する操作を行えます。\n管理者はインスタンスの全ての設定を変更できます。" - assignTarget: "アサインターゲット" + assignTarget: "アサイン" descriptionOfAssignTarget: "マニュアルは誰がこのロールに含まれるかを手動で管理します。\nコンディショナルは条件を設定し、それに合致するユーザーが自動で含まれるようになります。" 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 000000000..a46ddcc10 --- /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 ca23e44fa..8087090f5 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 bc29aba0a..7796dc3de 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 058ee416e..74f3c237f 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 2c33c5a04..6e0414ec0 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 000000000..0ebca088e --- /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 a971e06fd..852c1f32e 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 f8f9231cd..d15d8c0ae 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 { 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 52f337446..dbb89ff19 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 ff42c0735..eea9d5567 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 abd5f864a..8cf681186 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 87b23f189..df024a8f3 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 0061c2a8f..2d43615e2 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 39bc4c1d9..4bd6d0f55 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 f136c6d62..1a2a9fb74 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 { 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 fc4c3d8f1..c9f4a9fed 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 { 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 e423f0f10..0ce80a1a6 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 { 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 b07402882..b656307d9 100644 --- a/packages/frontend/src/components/MkCode.core.vue +++ b/packages/frontend/src/components/MkCode.core.vue @@ -1,6 +1,6 @@ diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue index cb88444d3..4525d3a00 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 8771168a4..6b43f1466 100644 --- a/packages/frontend/src/components/MkNoteHeader.vue +++ b/packages/frontend/src/components/MkNoteHeader.vue @@ -5,6 +5,9 @@
bot
+
+ +
@@ -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; + } +} diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index b51d456ea..e7a951dd2 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -63,10 +63,23 @@ {{ i18n.ts._achievements._types['_' + notification.achievement].title }} - {{ i18n.ts.youGotNewFollower }}
+ {{ i18n.ts.followRequestAccepted }} - {{ i18n.ts.receiveFollowRequest }}
|
- {{ i18n.ts.groupInvited }}: {{ notification.invitation.group.name }}
|
+ + diff --git a/packages/frontend/src/init.ts b/packages/frontend/src/init.ts index 4227f5cf4..64c252ce5 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 ae5ef39ba..086537a94 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -13,6 +13,10 @@ + + + + @@ -35,6 +39,21 @@
+ + + + + + + + + + + + + + +
@@ -358,16 +377,6 @@
- - - - - - - - - -
{{ role ? i18n.ts.save : i18n.ts.create }}
@@ -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>({}); @@ -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 0e785f259..c82559d55 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 0d52850b5..6075dde32 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 7a819eb9f..a01e3f8ce 100644 --- a/packages/frontend/src/pages/settings/webhook.edit.vue +++ b/packages/frontend/src/pages/settings/webhook.edit.vue @@ -31,6 +31,7 @@
{{ i18n.ts.save }} + {{ i18n.ts.delete }}
@@ -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 { }); } +async function del(): Promise { + 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 59dc1114d..080772951 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -1,9 +1,9 @@