diff --git a/.dockerignore b/.dockerignore index 854e643d39..8f984831ef 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 0000000000..2f93266456 --- /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 63dc940e24..a999dc51e6 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 0000000000..9b79ee54f0 --- /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 914bde051c..7142695350 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 811e4219e5..e539926789 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 3876b5f6ce..89a8d38f8c 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 0000000000..f8e598b282 --- /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 423116416a..dd1494fb21 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 4fd3bf3f0d..0c39a5e356 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 3e062ba51b..ba1c717f85 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 92cd9b467c..2248bb6ac9 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 a241d54b47..6286367b50 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 0000000000..a46ddcc10f --- /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 ca23e44fa4..8087090f5a 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 bc29aba0a0..7796dc3de1 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 058ee416ef..74f3c237fa 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 2c33c5a04c..6e0414ec08 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 0000000000..0ebca088e3 --- /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 a971e06fd8..852c1f32e3 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 f8f9231cdd..d15d8c0aee 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 52f3374468..dbb89ff19b 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 ff42c07359..eea9d5567d 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 abd5f864a2..8cf6811863 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 87b23f1891..df024a8f3c 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 0061c2a8f7..2d43615e25 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 39bc4c1d96..4bd6d0f556 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 f136c6d624..1a2a9fb747 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 fc4c3d8f11..c9f4a9fed8 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 e423f0f109..0ce80a1a63 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 b074028821..b656307d90 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 cb88444d34..4525d3a009 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 8771168a42..6b43f14665 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 b51d456eab..e7a951dd27 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 4227f5cf4a..64c252ce55 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 ae5ef39bae..086537a94a 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 0e785f259c..c82559d55a 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 0d52850b5d..6075dde326 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 7a819eb9f0..a01e3f8cee 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 59dc1114d1..080772951e 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -1,9 +1,9 @@