1
0
forked from mirror/misskey

Merge branch 'develop'

This commit is contained in:
syuilo 2023-02-05 20:55:51 +09:00
commit baf65bfa69
50 changed files with 347 additions and 86 deletions

View File

@ -16,9 +16,15 @@ files/
misskey-assets/
fluent-emojis/
.pnp.*
# .yarn関連
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
.idea/
packages/*/.vscode/
packages/backend/test/docker-compose.yml

3
.dockleignore Normal file
View File

@ -0,0 +1,3 @@
DKL-DI-0005
DKL-DI-0006
DKL-LI-0003

View File

@ -14,6 +14,8 @@ jobs:
steps:
- name: Check out the repo
uses: actions/checkout@v3.3.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.3.0
- name: Docker meta
id: meta
uses: docker/metadata-action@v4

30
.github/workflows/dockle.yml vendored Normal file
View File

@ -0,0 +1,30 @@
---
name: Dockle
on:
push:
branches:
- master
- develop
pull_request:
jobs:
dockle:
runs-on: ubuntu-latest
env:
DOCKER_CONTENT_TRUST: 1
steps:
- uses: actions/checkout@v3.2.0
- run: |
curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v0.4.10/dockle_0.4.10_Linux-64bit.deb"
sudo dpkg -i dockle.deb
- run: |
cp .config/docker_example.env .config/docker.env
cp ./docker-compose.yml.example ./docker-compose.yml
- run: |
docker compose up -d web
docker tag "$(docker compose images web | awk 'OFS=":" {print $4}' | tail -n +2)" misskey-web:latest
- run: |
cmd="dockle --exit-code 1 misskey-web:latest ${image_name}"
echo "> ${cmd}"
eval "${cmd}"

View File

@ -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)

View File

@ -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`.

View File

@ -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"]

4
healthcheck.sh Normal file
View File

@ -0,0 +1,4 @@
#!/bin/bash
PORT=$(yq '.port' /misskey/.config/default.yml)
curl -s -S -o /dev/null "http://localhost:${PORT}"

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -34,6 +34,7 @@ const languages = [
'pt-PT',
'ru-RU',
'sk-SK',
'th-TH',
'ug-CN',
'uk-UA',
'vi-VN',

View File

@ -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: "優先度"

2
locales/lo-LA.yml Normal file
View File

@ -0,0 +1,2 @@
---
_lang_: "ພາສາລາວ"

View File

@ -1195,6 +1195,9 @@ _role:
baseRole: "บทบาทพื้นฐาน"
useBaseValue: "ใช้บทบาทพื้นฐานเริ่มต้น"
chooseRoleToAssign: "เลือกบทบาทที่ต้องการกำหนด"
iconUrl: "ไอคอน URL"
asBadge: "แสดงเป็นตรา"
descriptionOfAsBadge: "ไอคอนของบทบาทนี้จะปรากฏถัดจากชื่อผู้ใช้ของผู้ใช้งานด้วยบทบาทนี้ถ้าหากเปิดใช้งาน"
canEditMembersByModerator: "อนุญาตให้ผู้ดูแลแก้ไขสมาชิก"
descriptionOfCanEditMembersByModerator: "เมื่อเปิดใช้ ผู้ดูแลนอกเหนือจากผู้ดูแลระบบแล้ว จะสามารถกำหนดและยกเลิกการมอบหมายบทบาทนี้ให้กับผู้ใช้ได้ เมื่อปิด เฉพาะผู้ดูแลระบบเท่านั้นที่จะสามารถกำหนดผู้ใช้ได้นะ"
priority: "ลำดับความสำคัญ"

View File

@ -1195,6 +1195,9 @@ _role:
baseRole: "基本角色"
useBaseValue: "使用基本角色的值"
chooseRoleToAssign: "选择要分配的角色"
iconUrl: "图标URL"
asBadge: "作为徽章显示"
descriptionOfAsBadge: "开启后,用户名旁边将会出现角色图标。"
canEditMembersByModerator: "允许监察者编辑成员"
descriptionOfCanEditMembersByModerator: "如果选中,监察者和管理员都能够为用户分配/取消分配角色。如果未选中,则只有管理员可以执行此操作。"
priority: "优先级"

View File

@ -1195,6 +1195,9 @@ _role:
baseRole: "基本角色"
useBaseValue: "使用基本角色的值"
chooseRoleToAssign: "選擇要指派的角色"
iconUrl: "圖示的URL"
asBadge: "顯示為徽章"
descriptionOfAsBadge: "開啟的話,角色圖示會顯示在用戶名旁邊。"
canEditMembersByModerator: "允許編輯監察員的成員"
descriptionOfCanEditMembersByModerator: "如果開啟,管理員與監察員都可以為使用者指派/解除指派該角色。如果關閉,則只有管理員可以執行。"
priority: "優先級"

View File

@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "13.3.4",
"version": "13.4.0",
"codename": "nasubi",
"repository": {
"type": "git",

View File

@ -0,0 +1,13 @@
export class roleIconBadge1675557528704 {
name = 'roleIconBadge1675557528704'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "role" ADD "iconUrl" character varying(512)`);
await queryRunner.query(`ALTER TABLE "role" ADD "asBadge" boolean NOT NULL DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "asBadge"`);
await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "iconUrl"`);
}
}

View File

@ -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)) {

View File

@ -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();

View File

@ -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,

View File

@ -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,

View File

@ -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,
})

View File

@ -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;

View File

@ -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({

View File

@ -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());
}
}

View File

@ -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]));

View File

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

View File

@ -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');
}
});

View File

@ -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>

View File

@ -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;
}

View File

@ -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>

View File

@ -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>

View File

@ -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);

View File

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

View File

@ -155,7 +155,11 @@ async function run() {
os.inputText({
title: q,
}).then(({ canceled, result: a }) => {
ok(a);
if (canceled) {
ok('');
} else {
ok(a);
}
});
});
},

View File

@ -86,7 +86,11 @@ async function run() {
os.inputText({
title: q,
}).then(({ canceled, result: a }) => {
ok(a);
if (canceled) {
ok('');
} else {
ok(a);
}
});
});
},

View File

@ -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(() => []);

View File

@ -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',

View File

@ -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>

View File

@ -20,7 +20,11 @@ export function install(plugin) {
inputText({
title: q,
}).then(({ canceled, result: a }) => {
ok(a);
if (canceled) {
ok('');
} else {
ok(a);
}
});
});
},

View File

@ -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: '/',

View File

@ -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));

View File

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

View File

@ -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;

View File

@ -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;

View File

@ -72,7 +72,11 @@ const run = async () => {
os.inputText({
title: q,
}).then(({ canceled, result: a }) => {
ok(a);
if (canceled) {
ok('');
} else {
ok(a);
}
});
});
},

View File

@ -67,7 +67,11 @@ async function run() {
os.inputText({
title: q,
}).then(({ canceled, result: a }) => {
ok(a);
if (canceled) {
ok('');
} else {
ok(a);
}
});
});
},

View File

@ -60,7 +60,11 @@ const run = async () => {
os.inputText({
title: q,
}).then(({ canceled, result: a }) => {
ok(a);
if (canceled) {
ok('');
} else {
ok(a);
}
});
});
},