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);
+					}
 				});
 			});
 		},