Merge branch 'develop' into mk.absturztau.be

This commit is contained in:
Puniko 2022-05-21 13:15:15 +02:00
commit ab0f2dc922
77 changed files with 1808 additions and 1629 deletions

5
.github/labeler.yml vendored Normal file
View file

@ -0,0 +1,5 @@
'⚙Server':
- packages/backend/**/*
'🖥Client':
- packages/client/**/*

14
.github/workflows/labeler.yml vendored Normal file
View file

@ -0,0 +1,14 @@
name: "Pull Request Labeler"
on:
- pull_request_target
jobs:
triage:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v4
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"

View file

@ -18,6 +18,14 @@ You should also include the user name that made the change.
- enhance: API: notifications/readは配列でも受け付けるように #7667 @tamaina
- enhance: プッシュ通知を複数アカウント対応に #7667 @tamaina
- enhance: プッシュ通知にクリックやactionを設定 #7667 @tamaina
- replaced webpack with Vite @tamaina
- update dependencies @syuilo
- enhance: display URL of QR code for TOTP registration @syuilo
- make CAPTCHA required for signin to improve security @syuilo
- The theme color is now better validated. @Johann150
Your own theme color may be unset if it was in an invalid format.
Admins should check their instance settings if in doubt.
- Perform port diagnosis at startup only when Listen fails @mei23
### Bugfixes
- Client: fix settings page @tamaina
@ -25,6 +33,14 @@ You should also include the user name that made the change.
- Server: await promises when following or unfollowing users @Johann150
- Client: fix abuse reports page to be able to show all reports @Johann150
- Federation: Add rel attribute to host-meta @mei23
- Client: fix profile picture height in mentions @tamaina
- MFM: more animated functions support `speed` parameter @futchitwo
- Federation: Fix quote renotes containing no text being federated correctly @Johann150
- Server: fix missing foreign key for reports leading to reports page being unusable @Johann150
- Server: fix internal in-memory caching @Johann150
- Server: use correct order of attachments on notes @Johann150
- Server: prevent crash when processing certain PNGs @syuilo
- Server: Fix unable to generate video thumbnails @mei23
## 12.110.1 (2022/04/23)

View file

@ -15,6 +15,7 @@ Before creating an issue, please check the following:
- Do not use Issues to ask questions or troubleshooting.
- Issues should only be used to feature requests, suggestions, and bug tracking.
- Please ask questions or troubleshooting in the [Misskey Forum](https://forum.misskey.io/) or [Discord](https://discord.gg/Wp8gVStHW3).
- Do not close issues that are about to be resolved. It should remain open until a commit that actually resolves it is merged.
## Before implementation
When you want to add a feature or fix a bug, **first have the design and policy reviewed in an Issue** (if it is not there, please make one). Without this step, there is a high possibility that the PR will not be merged even if it is implemented.

View file

@ -1,6 +1,6 @@
FROM node:18.0.0-alpine3.15 AS base
ENV NODE_ENV=production
ARG NODE_ENV=production
WORKDIR /misskey
@ -31,5 +31,6 @@ COPY --from=builder /misskey/packages/backend/built ./packages/backend/built
COPY --from=builder /misskey/packages/client/node_modules ./packages/client/node_modules
COPY . ./
ENV NODE_ENV=production
CMD ["npm", "run", "migrateandstart"]

View file

@ -1,27 +1,32 @@
[![Misskey](https://github.com/misskey-dev/assets/blob/main/banner.png?raw=true)](https://join.misskey.page/)
<div align="center">
<a href="https://misskey-hub.net">
<img src="./assets/title_float.svg" alt="Misskey logo" style="border-radius:50%" width="400"/>
</a>
**🌎 A forever evolving, interplanetary microblogging platform. 🚀**
**Misskey** is a distributed microblogging platform with advanced features such as Reactions and a highly customizable UI.
[Learn more](https://misskey-hub.net/)
**🌎 **[Misskey](https://misskey-hub.net/)** is an open source, decentralized social media platform that's free forever! 🚀**
---
[✨ Find an instance](https://misskey-hub.net/instances.html)
[📦 Create your own instance](https://misskey-hub.net/docs/install.html)
[🛠️ Contribute](./CONTRIBUTING.md)
[🚀 Join the community](https://discord.gg/Wp8gVStHW3)
<a href="https://misskey-hub.net/instances.html">
<img src="https://custom-icon-badges.herokuapp.com/badge/find_an-instance-acea31?logoColor=acea31&style=for-the-badge&logo=misskey&labelColor=363B40" alt="find an instance"/></a>
<a href="https://misskey-hub.net/docs/install.html">
<img src="https://custom-icon-badges.herokuapp.com/badge/create_an-instance-FBD53C?logoColor=FBD53C&style=for-the-badge&logo=server&labelColor=363B40" alt="create an instance"/></a>
<a href="./CONTRIBUTING.md">
<img src="https://custom-icon-badges.herokuapp.com/badge/become_a-contributor-A371F7?logoColor=A371F7&style=for-the-badge&logo=git-merge&labelColor=363B40" alt="become a contributor"/>
</a>
<a href="https://discord.gg/Wp8gVStHW3">
<img src="https://custom-icon-badges.herokuapp.com/badge/join_the-community-5865F2?logoColor=5865F2&style=for-the-badge&logo=discord&labelColor=363B40" alt="join the community"/>
</a>
<a href="https://www.patreon.com/syuilo">
<img src="https://custom-icon-badges.herokuapp.com/badge/become_a-patron-F96854?logoColor=F96854&style=for-the-badge&logo=patreon&labelColor=363B40" alt="become a patron"/>
</a>
---
<a href="https://www.patreon.com/syuilo"><img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" alt="Become a Patron!" width="160" /></a>
</div>
<div>
@ -30,17 +35,16 @@
## ✨ Features
- **ActivityPub support**\
It is possible to interact with other software.
Not on Misskey? No problem! Not only can Misskey instances talk to each other, but you can make friends with people on other networks like Mastodon and Pixelfed!
- **Reactions**\
You can add "reactions" to each post, making it easy for you to express your feelings.
You can add emoji reactions to any post! No longer are you bound by a like button, show everyone exactly how you feel with the tap of a button.
- **Drive**\
An interface to manage uploaded files such as images, videos, sounds, etc.
You can also organize your favorite content into folders, making it easy to share again.
With Misskey's built in drive, you get cloud storage right in your social media, where you can upload any files, make folders, and find media from posts you've made!
- **Rich Web UI**\
Misskey has a rich WebUI by default.
It is highly customizable by flexibly changing the layout and installing various widgets and themes.
Furthermore, plug-ins can be created using AiScript, a original programming language.
- and more...
Misskey has a rich and easy to use Web UI!
It is highly customizable, from changing the layout and adding widgets to making custom themes.
Furthermore, plugins can be created using AiScript, an original programming language.
- And much more...
</div>

BIN
assets/title_float.svg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

3
chart/Chart.yaml Normal file
View file

@ -0,0 +1,3 @@
apiVersion: v2
name: misskey
version: 0.0.0

165
chart/files/default.yml Normal file
View file

@ -0,0 +1,165 @@
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Misskey configuration
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ┌─────┐
#───┘ URL └─────────────────────────────────────────────────────
# Final accessible URL seen by a user.
# url: https://example.tld/
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
# URL SETTINGS AFTER THAT!
# ┌───────────────────────┐
#───┘ Port and TLS settings └───────────────────────────────────
#
# Misskey supports two deployment options for public.
#
# Option 1: With Reverse Proxy
#
# +----- https://example.tld/ ------------+
# +------+ |+-------------+ +----------------+|
# | User | ---> || Proxy (443) | ---> | Misskey (3000) ||
# +------+ |+-------------+ +----------------+|
# +---------------------------------------+
#
# You need to setup reverse proxy. (eg. nginx)
# You do not define 'https' section.
# Option 2: Standalone
#
# +- https://example.tld/ -+
# +------+ | +---------------+ |
# | User | ---> | | Misskey (443) | |
# +------+ | +---------------+ |
# +------------------------+
#
# You need to run Misskey as root.
# You need to set Certificate in 'https' section.
# To use option 1, uncomment below line.
port: 3000 # A port that your Misskey server should listen.
# To use option 2, uncomment below lines.
#port: 443
#https:
# # path for certification
# key: /etc/letsencrypt/live/example.tld/privkey.pem
# cert: /etc/letsencrypt/live/example.tld/fullchain.pem
# ┌──────────────────────────┐
#───┘ PostgreSQL configuration └────────────────────────────────
db:
host: localhost
port: 5432
# Database name
db: misskey
# Auth
user: example-misskey-user
pass: example-misskey-pass
# Whether disable Caching queries
#disableCache: true
# Extra Connection options
#extra:
# ssl: true
# ┌─────────────────────┐
#───┘ Redis configuration └─────────────────────────────────────
redis:
host: localhost
port: 6379
#pass: example-pass
#prefix: example-prefix
#db: 1
# ┌─────────────────────────────┐
#───┘ Elasticsearch configuration └─────────────────────────────
#elasticsearch:
# host: localhost
# port: 9200
# ssl: false
# user:
# pass:
# ┌───────────────┐
#───┘ ID generation └───────────────────────────────────────────
# You can select the ID generation method.
# You don't usually need to change this setting, but you can
# change it according to your preferences.
# Available methods:
# aid ... Short, Millisecond accuracy
# meid ... Similar to ObjectID, Millisecond accuracy
# ulid ... Millisecond accuracy
# objectid ... This is left for backward compatibility
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
# ID SETTINGS AFTER THAT!
id: "aid"
# ┌─────────────────────┐
#───┘ Other configuration └─────────────────────────────────────
# Whether disable HSTS
#disableHsts: true
# Number of worker processes
#clusterLimit: 1
# Job concurrency per worker
# deliverJobConcurrency: 128
# inboxJobConcurrency: 16
# Job rate limiter
# deliverJobPerSec: 128
# inboxJobPerSec: 16
# Job attempts
# deliverJobMaxAttempts: 12
# inboxJobMaxAttempts: 8
# IP address family used for outgoing request (ipv4, ipv6 or dual)
#outgoingAddressFamily: ipv4
# Syslog option
#syslog:
# host: localhost
# port: 514
# Proxy for HTTP/HTTPS
#proxy: http://127.0.0.1:3128
#proxyBypassHosts: [
# 'example.com',
# '192.0.2.8'
#]
# Proxy for SMTP/SMTPS
#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT
#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4
#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5
# Media Proxy
#mediaProxy: https://example.com/proxy
# Sign to ActivityPub GET request (default: false)
#signToActivityPubGet: true
#allowedPrivateNetworks: [
# '127.0.0.1/32'
#]
# Upload or download file size limits (bytes)
#maxFileSize: 262144000

View file

@ -0,0 +1,8 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "misskey.fullname" . }}-configuration
data:
default.yml: |-
{{ .Files.Get "files/default.yml"|nindent 4 }}
url: {{ .Values.url }}

View file

@ -0,0 +1,47 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "misskey.fullname" . }}
labels:
{{- include "misskey.labels" . | nindent 4 }}
spec:
selector:
matchLabels:
{{- include "misskey.selectorLabels" . | nindent 6 }}
replicas: 1
template:
metadata:
labels:
{{- include "misskey.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: misskey
image: {{ .Values.image }}
env:
- name: NODE_ENV
value: {{ .Values.environment }}
volumeMounts:
- name: {{ include "misskey.fullname" . }}-configuration
mountPath: /misskey/.config
readOnly: true
ports:
- containerPort: 3000
- name: postgres
image: postgres:14-alpine
env:
- name: POSTGRES_USER
value: "example-misskey-user"
- name: POSTGRES_PASSWORD
value: "example-misskey-pass"
- name: POSTGRES_DB
value: "misskey"
ports:
- containerPort: 5432
- name: redis
image: redis:alpine
ports:
- containerPort: 6379
volumes:
- name: {{ include "misskey.fullname" . }}-configuration
configMap:
name: {{ include "misskey.fullname" . }}-configuration

View file

@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "misskey.fullname" . }}
annotations:
dev.okteto.com/auto-ingress: "true"
spec:
type: ClusterIP
ports:
- port: 3000
protocol: TCP
name: http
selector:
{{- include "misskey.selectorLabels" . | nindent 4 }}

View file

@ -0,0 +1,62 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "misskey.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "misskey.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "misskey.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "misskey.labels" -}}
helm.sh/chart: {{ include "misskey.chart" . }}
{{ include "misskey.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "misskey.selectorLabels" -}}
app.kubernetes.io/name: {{ include "misskey.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "misskey.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "misskey.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

3
chart/values.yml Normal file
View file

@ -0,0 +1,3 @@
url: https://example.tld/
image: okteto.dev/misskey
environment: production

View file

@ -9,7 +9,7 @@ services:
- redis
# - es
ports:
- "127.0.0.1:3000:3000"
- "3000:3000"
networks:
- internal_network
- external_network

View file

@ -425,7 +425,7 @@ quoteQuestion: "引用として添付しますか?"
noMessagesYet: "まだチャットはありません"
newMessageExists: "新しいメッセージがあります"
onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです"
signinRequired: "ログインしてください"
signinRequired: "続行する前に、サインアップまたはサインインが必要です"
invitations: "招待"
invitationCode: "招待コード"
checking: "確認しています"

6
okteto.yml Normal file
View file

@ -0,0 +1,6 @@
build:
misskey:
args:
- NODE_ENV=development
deploy:
- helm upgrade --install misskey chart --set image=${OKTETO_BUILD_MISSKEY_IMAGE} --set url="https://misskey-$(kubectl config view --minify -o jsonpath='{..namespace}').cloud.okteto.net" --set environment=development

View file

@ -0,0 +1,36 @@
import tinycolor from 'tinycolor2';
export class uniformThemecolor1652859567549 {
name = 'uniformThemecolor1652859567549'
async up(queryRunner) {
const formatColor = (color) => {
let tc = new tinycolor(color);
if (tc.isValid()) {
return tc.toHexString();
} else {
return null;
}
};
await queryRunner.query('SELECT "id", "themeColor" FROM "instance" WHERE "themeColor" IS NOT NULL')
.then(instances => Promise.all(instances.map(instance => {
// update theme color to uniform format, e.g. #00ff00
// invalid theme colors get set to null
return queryRunner.query('UPDATE "instance" SET "themeColor" = $1 WHERE "id" = $2', [formatColor(instance.themeColor), instance.id]);
})));
// also fix own theme color
await queryRunner.query('SELECT "themeColor" FROM "meta" WHERE "themeColor" IS NOT NULL LIMIT 1')
.then(metas => {
if (metas.length > 0) {
return queryRunner.query('UPDATE "meta" SET "themeColor" = $1', [formatColor(metas[0].themeColor)]);
}
});
}
async down(queryRunner) {
// The original representation is not stored, so migrating back is not possible.
// The new format also works in older versions so this is not a problem.
}
}

View file

@ -77,7 +77,6 @@
"os-utils": "0.0.14",
"parse5": "6.0.1",
"pg": "8.7.3",
"portscanner": "2.2.0",
"private-ip": "2.3.3",
"probe-image-size": "7.2.3",
"promise-limit": "2.7.0",
@ -97,7 +96,7 @@
"s-age": "1.1.2",
"sanitize-html": "2.7.0",
"semver": "7.3.7",
"sharp": "0.30.4",
"sharp": "0.29.3",
"speakeasy": "2.0.0",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
@ -151,7 +150,6 @@
"@types/nodemailer": "6.4.4",
"@types/oauth": "0.9.1",
"@types/parse5": "6.0.3",
"@types/portscanner": "2.1.1",
"@types/pug": "2.0.6",
"@types/punycode": "2.1.0",
"@types/qrcode": "1.4.2",

View file

@ -5,7 +5,6 @@ import * as os from 'node:os';
import cluster from 'node:cluster';
import chalk from 'chalk';
import chalkTemplate from 'chalk-template';
import * as portscanner from 'portscanner';
import semver from 'semver';
import Logger from '@/services/logger.js';
@ -48,11 +47,6 @@ function greet() {
bootLogger.info(`Misskey v${meta.version}`, null, true);
}
function isRoot() {
// maybe process.getuid will be undefined under not POSIX environment (e.g. Windows)
return process.getuid != null && process.getuid() === 0;
}
/**
* Init master process
*/
@ -67,7 +61,6 @@ export async function masterMain() {
showNodejsVersion();
config = loadConfigBoot();
await connectDb();
await validatePort(config);
} catch (e) {
bootLogger.error('Fatal error occurred during initialization', null, true);
process.exit(1);
@ -97,8 +90,6 @@ function showEnvironment(): void {
logger.warn('The environment is not in production mode.');
logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', null, true);
}
logger.info(`You ${isRoot() ? '' : 'do not '}have root privileges`);
}
function showNodejsVersion(): void {
@ -152,29 +143,6 @@ async function connectDb(): Promise<void> {
}
}
async function validatePort(config: Config): Promise<void> {
const isWellKnownPort = (port: number) => port < 1024;
async function isPortAvailable(port: number): Promise<boolean> {
return await portscanner.checkPortStatus(port, '127.0.0.1') === 'closed';
}
if (config.port == null || Number.isNaN(config.port)) {
bootLogger.error('The port is not configured. Please configure port.', null, true);
process.exit(1);
}
if (process.platform === 'linux' && isWellKnownPort(config.port) && !isRoot()) {
bootLogger.error('You need root privileges to listen on well-known port on Linux', null, true);
process.exit(1);
}
if (!await isPortAvailable(config.port)) {
bootLogger.error(`Port ${config.port} is already in use`, null, true);
process.exit(1);
}
}
async function spawnWorkers(limit: number = 1) {
const workers = Math.min(limit, os.cpus().length);
bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`);
@ -186,6 +154,10 @@ function spawnWorker(): Promise<void> {
return new Promise(res => {
const worker = cluster.fork();
worker.on('message', message => {
if (message === 'listenFailed') {
bootLogger.error(`The server Listen failed due to the previous error.`);
process.exit(1);
}
if (message !== 'ready') return;
res();
});

View file

@ -9,6 +9,7 @@ import { fetchMeta } from '@/misc/fetch-meta.js';
import { getApLock } from '@/misc/app-lock.js';
import { parseAudience } from '../../audience.js';
import { StatusError } from '@/misc/fetch.js';
import { Notes } from '@/models/index.js';
const logger = apLogger;
@ -52,6 +53,8 @@ export default async function(resolver: Resolver, actor: CacheableRemoteUser, ac
throw e;
}
if (!await Notes.isVisibleForMe(renote, actor)) return 'skip: invalid actor for this activity';
logger.info(`Creating the (Re)Note: ${uri}`);
const activityAudience = await parseAudience(actor, activity.to, activity.cc);

View file

@ -13,37 +13,37 @@ export default async (actor: CacheableRemoteUser, activity: IDelete): Promise<st
}
// 削除対象objectのtype
let formarType: string | undefined;
let formerType: string | undefined;
if (typeof activity.object === 'string') {
// typeが不明だけど、どうせ消えてるのでremote resolveしない
formarType = undefined;
formerType = undefined;
} else {
const object = activity.object as IObject;
if (isTombstone(object)) {
formarType = toSingle(object.formerType);
formerType = toSingle(object.formerType);
} else {
formarType = toSingle(object.type);
formerType = toSingle(object.type);
}
}
const uri = getApId(activity.object);
// type不明でもactorとobjectが同じならばそれはPersonに違いない
if (!formarType && actor.uri === uri) {
formarType = 'Person';
if (!formerType && actor.uri === uri) {
formerType = 'Person';
}
// それでもなかったらおそらくNote
if (!formarType) {
formarType = 'Note';
if (!formerType) {
formerType = 'Note';
}
if (validPost.includes(formarType)) {
if (validPost.includes(formerType)) {
return await deleteNote(actor, uri);
} else if (validActor.includes(formarType)) {
} else if (validActor.includes(formerType)) {
return await deleteActor(actor, uri);
} else {
return `Unknown type ${formarType}`;
return `Unknown type ${formerType}`;
}
};

View file

@ -8,6 +8,7 @@ export const undoAnnounce = async (actor: CacheableRemoteUser, activity: IAnnoun
const note = await Notes.findOneBy({
uri,
userId: actor.id,
});
if (!note) return 'skip: no such Announce';

View file

@ -27,7 +27,7 @@ export const paramDef = {
blockedHosts: { type: 'array', nullable: true, items: {
type: 'string',
} },
themeColor: { type: 'string', nullable: true },
themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' },
mascotImageUrl: { type: 'string', nullable: true },
bannerUrl: { type: 'string', nullable: true },
errorImageUrl: { type: 'string', nullable: true },

View file

@ -177,7 +177,7 @@ export default define(meta, paramDef, async (ps, user) => {
userId: user.id,
fileIds,
})
.orderBy('array_position(ARRAY[:...fileIds], "id")')
.orderBy('array_position(ARRAY[:...fileIds], "id"::text)')
.setParameters({ fileIds })
.getMany();
}

View file

@ -1,20 +1,25 @@
import { randomBytes } from 'node:crypto';
import Koa from 'koa';
import bcrypt from 'bcryptjs';
import * as speakeasy from 'speakeasy';
import signin from '../common/signin.js';
import { IsNull } from 'typeorm';
import config from '@/config/index.js';
import { Users, Signins, UserProfiles, UserSecurityKeys, AttestationChallenges } from '@/models/index.js';
import { ILocalUser } from '@/models/entities/user.js';
import { genId } from '@/misc/gen-id.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { verifyHcaptcha, verifyRecaptcha } from '@/misc/captcha.js';
import { verifyLogin, hash } from '../2fa.js';
import { randomBytes } from 'node:crypto';
import { IsNull } from 'typeorm';
import signin from '../common/signin.js';
export default async (ctx: Koa.Context) => {
ctx.set('Access-Control-Allow-Origin', config.url);
ctx.set('Access-Control-Allow-Credentials', 'true');
const body = ctx.request.body as any;
const instance = await fetchMeta(true);
const username = body['username'];
const password = body['password'];
const token = body['token'];
@ -79,6 +84,18 @@ export default async (ctx: Koa.Context) => {
}
if (!profile.twoFactorEnabled) {
if (instance.enableHcaptcha && instance.hcaptchaSecretKey) {
await verifyHcaptcha(instance.hcaptchaSecretKey, body['hcaptcha-response']).catch(e => {
ctx.throw(400, e);
});
}
if (instance.enableRecaptcha && instance.recaptchaSecretKey) {
await verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(e => {
ctx.throw(400, e);
});
}
if (same) {
signin(ctx, user);
return;
@ -155,7 +172,7 @@ export default async (ctx: Koa.Context) => {
body.credentialId
.replace(/-/g, '+')
.replace(/_/g, '/'),
'base64'
'base64',
).toString('hex'),
});

View file

@ -2,6 +2,7 @@
* Core Server
*/
import cluster from 'node:cluster';
import * as fs from 'node:fs';
import * as http from 'node:http';
import Koa from 'koa';
@ -142,5 +143,26 @@ export default () => new Promise(resolve => {
initializeStreamingServer(server);
server.on('error', e => {
switch ((e as any).code) {
case 'EACCES':
serverLogger.error(`You do not have permission to listen on port ${config.port}.`);
break;
case 'EADDRINUSE':
serverLogger.error(`Port ${config.port} is already in use by another process.`);
break;
default:
serverLogger.error(e);
break;
}
if (cluster.isWorker) {
process.send!('listenFailed');
} else {
// disableClustering
process.exit(1);
}
});
server.listen(config.port, resolve);
});

View file

@ -39,28 +39,24 @@ html {
width: 28px;
height: 28px;
transform: translateY(70px);
color: var(--accent);
}
#splashSpinner:before,
#splashSpinner:after {
content: " ";
display: block;
box-sizing: border-box;
width: 28px;
height: 28px;
border-radius: 50%;
border: solid 4px;
}
#splashSpinner:before {
border-color: currentColor;
opacity: 0.3;
}
#splashSpinner:after {
#splashSpinner > .spinner {
position: absolute;
top: 0;
border-color: currentColor transparent transparent transparent;
left: 0;
width: 28px;
height: 28px;
fill-rule: evenodd;
clip-rule: evenodd;
stroke-linecap: round;
stroke-linejoin: round;
stroke-miterlimit: 1.5;
}
#splashSpinner > .spinner.bg {
opacity: 0.275;
}
#splashSpinner > .spinner.fg {
animation: splashSpinner 0.5s linear infinite;
}

View file

@ -65,4 +65,14 @@ html
div#splash
img#splashIcon(src= icon || '/static-assets/splash.png')
div#splashSpinner
<svg class="spinner bg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1,0,0,1,12,12)">
<circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
</g>
</svg>
<svg class="spinner fg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1,0,0,1,12,12)">
<path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
</g>
</svg>
block content

View file

@ -1,7 +1,7 @@
import * as fs from 'node:fs';
import * as tmp from 'tmp';
import { IImage, convertToJpeg } from './image-processor.js';
import * as FFmpeg from 'fluent-ffmpeg';
import FFmpeg from 'fluent-ffmpeg';
export async function GenerateVideoThumbnail(path: string): Promise<IImage> {
const [outDir, cleanup] = await new Promise<[string, any]>((res, rej) => {

View file

@ -1,5 +1,6 @@
import { DOMWindow, JSDOM } from 'jsdom';
import fetch from 'node-fetch';
import tinycolor from 'tinycolor2';
import { getJson, getHtml, getAgentByUrl } from '@/misc/fetch.js';
import { Instance } from '@/models/entities/instance.js';
import { Instances } from '@/models/index.js';
@ -208,16 +209,11 @@ async function fetchIconUrl(instance: Instance, doc: DOMWindow['document'] | nul
}
async function getThemeColor(doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
if (doc) {
const themeColor = doc.querySelector('meta[name="theme-color"]')?.getAttribute('content');
const themeColor = doc?.querySelector('meta[name="theme-color"]')?.getAttribute('content') || manifest?.theme_color;
if (themeColor) {
return themeColor;
}
}
if (manifest) {
return manifest.theme_color;
if (themeColor) {
const color = new tinycolor(themeColor);
if (color.isValid()) return color.toHexString();
}
return null;

View file

@ -27,6 +27,11 @@ export default async (user: { id: User['id']; host: User['host']; }, note: Note,
}
}
// check visibility
if (!await Notes.isVisibleForMe(note, user)) {
throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.');
}
// TODO: cache
reaction = await toDbReaction(reaction, user.host);

View file

@ -1,14 +1,15 @@
import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import * as childProcess from 'child_process';
import * as http from 'node:http';
import { SIGKILL } from 'constants';
import * as WebSocket from 'ws';
import * as misskey from 'misskey-js';
import fetch from 'node-fetch';
import FormData from 'form-data';
import * as childProcess from 'child_process';
import * as http from 'node:http';
import { DataSource } from 'typeorm';
import loadConfig from '../src/config/load.js';
import { SIGKILL } from 'constants';
import { entities } from '../src/db/postgre.js';
const _filename = fileURLToPath(import.meta.url);
@ -27,29 +28,29 @@ export const async = (fn: Function) => (done: Function) => {
export const request = async (endpoint: string, params: any, me?: any): Promise<{ body: any, status: number }> => {
const auth = me ? {
i: me.token
i: me.token,
} : {};
const res = await fetch(`http://localhost:${port}/api${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
'Content-Type': 'application/json',
},
body: JSON.stringify(Object.assign(auth, params))
body: JSON.stringify(Object.assign(auth, params)),
});
const status = res.status;
const body = res.status !== 204 ? await res.json().catch() : null;
return {
body, status
body, status,
};
};
export const signup = async (params?: any): Promise<any> => {
const q = Object.assign({
username: 'test',
password: 'test'
password: 'test',
}, params);
const res = await request('/signup', q);
@ -59,7 +60,7 @@ export const signup = async (params?: any): Promise<any> => {
export const post = async (user: any, params?: misskey.Endpoints['notes/create']['req']): Promise<misskey.entities.Note> => {
const q = Object.assign({
text: 'test'
text: 'test',
}, params);
const res = await request('/notes/create', q, user);
@ -70,26 +71,26 @@ export const post = async (user: any, params?: misskey.Endpoints['notes/create']
export const react = async (user: any, note: any, reaction: string): Promise<any> => {
await request('/notes/reactions/create', {
noteId: note.id,
reaction: reaction
reaction: reaction,
}, user);
};
export const uploadFile = (user: any, path?: string): Promise<any> => {
const formData = new FormData();
formData.append('i', user.token);
formData.append('file', fs.createReadStream(path || _dirname + '/resources/Lenna.png'));
const formData = new FormData();
formData.append('i', user.token);
formData.append('file', fs.createReadStream(path || _dirname + '/resources/Lenna.png'));
return fetch(`http://localhost:${port}/api/drive/files/create`, {
method: 'post',
body: formData,
timeout: 30 * 1000,
}).then(res => {
if (!res.ok) {
throw `${res.status} ${res.statusText}`;
} else {
return res.json();
}
});
return fetch(`http://localhost:${port}/api/drive/files/create`, {
method: 'post',
body: formData,
timeout: 30 * 1000,
}).then(res => {
if (!res.ok) {
throw `${res.status} ${res.statusText}`;
} else {
return res.json();
}
});
};
export function connectStream(user: any, channel: string, listener: (message: Record<string, any>) => any, params?: any): Promise<WebSocket> {
@ -112,8 +113,8 @@ export function connectStream(user: any, channel: string, listener: (message: Re
channel: channel,
id: 'a',
pong: true,
params: params
}
params: params,
},
}));
});
});
@ -124,8 +125,8 @@ export const simpleGet = async (path: string, accept = '*/*'): Promise<{ status?
return await new Promise((resolve, reject) => {
const req = http.request(`http://localhost:${port}${path}`, {
headers: {
Accept: accept
}
Accept: accept,
},
}, res => {
if (res.statusCode! >= 400) {
reject(res);
@ -146,7 +147,7 @@ export function launchServer(callbackSpawnedProcess: (p: childProcess.ChildProce
return (done: (err?: Error) => any) => {
const p = childProcess.spawn('node', [_dirname + '/../index.js'], {
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
env: { NODE_ENV: 'test', PATH: process.env.PATH }
env: { NODE_ENV: 'test', PATH: process.env.PATH },
});
callbackSpawnedProcess(p);
p.on('message', message => {
@ -158,12 +159,7 @@ export function launchServer(callbackSpawnedProcess: (p: childProcess.ChildProce
export async function initTestDb(justBorrow = false, initEntities?: any[]) {
if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test';
try {
const conn = await getConnection();
await conn.close();
} catch (e) {}
return await createConnection({
return new DataSource({
type: 'postgres',
host: config.db.host,
port: config.db.port,
@ -172,7 +168,7 @@ export async function initTestDb(justBorrow = false, initEntities?: any[]) {
database: config.db.db,
synchronize: true && !justBorrow,
dropSchema: true && !justBorrow,
entities: initEntities || entities
entities: initEntities || entities,
});
}
@ -185,7 +181,7 @@ export function startServer(timeout = 30 * 1000): Promise<childProcess.ChildProc
const p = childProcess.spawn('node', [_dirname + '/../built/index.js'], {
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
env: { NODE_ENV: 'test', PATH: process.env.PATH }
env: { NODE_ENV: 'test', PATH: process.env.PATH },
});
p.on('error', e => rej(e));

View file

@ -705,11 +705,6 @@
resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.3.tgz#705bb349e789efa06f43f128cef51240753424cb"
integrity sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==
"@types/portscanner@2.1.1":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@types/portscanner/-/portscanner-2.1.1.tgz#89d5094e16f3d941f20f3889dfa5d3a164b3dd3b"
integrity sha512-1NsVIbgBKvrqxwtMN0V6CLji1ERwKSI/RWz0J3y++CzSwYNGBStCfpIFgxV3ZwxsDR5PoZqoUWhwraDm+Ztn0Q==
"@types/pug@2.0.6":
version "2.0.6"
resolved "https://registry.yarnpkg.com/@types/pug/-/pug-2.0.6.tgz#f830323c88172e66826d0bde413498b61054b5a6"
@ -1249,19 +1244,7 @@ assert-plus@1.0.0, assert-plus@^1.0.0:
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
async@>=0.2.9:
version "3.2.0"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==
async@^2.6.0:
version "2.6.3"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
dependencies:
lodash "^4.17.14"
async@^3.2.3:
async@>=0.2.9, async@^3.2.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9"
integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==
@ -1872,7 +1855,7 @@ color-support@^1.1.2:
resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2"
integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==
color@^4.2.3:
color@^4.0.1:
version "4.2.3"
resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a"
integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==
@ -2279,16 +2262,16 @@ detect-file@^1.0.0:
resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7"
integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=
detect-libc@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
detect-libc@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.0.tgz#c528bc09bc6d1aa30149228240917c225448f204"
integrity sha512-S55LzUl8HUav8l9E2PBTlC5PAJrHK7tkM+XXFGD+fbsbkTzhCpG6K05LxJcUOEWzMa4v6ptcMZ9s3fOdJDu0Zw==
detect-libc@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd"
integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==
detect-node@2.1.0, detect-node@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1"
@ -3861,13 +3844,6 @@ is-negative-zero@^2.0.1:
resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24"
integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==
is-number-like@^1.0.3:
version "1.0.8"
resolved "https://registry.yarnpkg.com/is-number-like/-/is-number-like-1.0.8.tgz#2e129620b50891042e44e9bbbb30593e75cfbbe3"
integrity sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA==
dependencies:
lodash.isfinite "^3.3.2"
is-number-object@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.6.tgz#6a7aaf838c7f0686a50b4553f7e54a96494e89f0"
@ -4498,11 +4474,6 @@ lodash.isequal@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
lodash.isfinite@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz#fb89b65a9a80281833f0b7478b3a5104f898ebb3"
integrity sha1-+4m2WpqAKBgz8LdHizpRBPiY67M=
lodash.isplainobject@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
@ -4548,7 +4519,7 @@ lodash.union@^4.6.0:
resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88"
integrity sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=
lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.19, lodash@^4.17.21:
lodash@^4.17.11, lodash@^4.17.19, lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@ -4985,7 +4956,7 @@ node-addon-api@^1.2.0:
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.7.2.tgz#3df30b95720b53c24e59948b49532b662444f54d"
integrity sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==
node-addon-api@^4.3.0:
node-addon-api@^4.2.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f"
integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==
@ -5541,14 +5512,6 @@ pngjs@^5.0.0:
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb"
integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==
portscanner@2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/portscanner/-/portscanner-2.2.0.tgz#6059189b3efa0965c9d96a56b958eb9508411cf1"
integrity sha512-IFroCz/59Lqa2uBvzK3bKDbDDIEaAY8XJ1jFxcLWTqosrsc32//P4VuSB2vZXoHiHqOmx8B5L5hnKOxL/7FlPw==
dependencies:
async "^2.6.0"
is-number-like "^1.0.3"
postcss@^8.3.11:
version "8.3.11"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.11.tgz#c3beca7ea811cd5e1c4a3ec6d2e7599ef1f8f858"
@ -5580,10 +5543,10 @@ postgres-interval@^1.1.0:
dependencies:
xtend "^4.0.0"
prebuild-install@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.0.1.tgz#c10075727c318efe72412f333e0ef625beaf3870"
integrity sha512-QBSab31WqkyxpnMWQxubYAHR5S9B2+r81ucocew34Fkl98FhvKIF50jIJnNOBmAZfyNV7vE5T6gd3hTVWgY6tg==
prebuild-install@^7.0.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.0.tgz#991b6ac16c81591ba40a6d5de93fb33673ac1370"
integrity sha512-CNcMgI1xBypOyGqjp3wOc8AAo1nMhZS3Cwd3iHIxOdAUbb+YxdNuM4Z5iIrZ8RLvOsf3F3bl7b7xGq6DjQoNYA==
dependencies:
detect-libc "^2.0.0"
expand-template "^2.0.3"
@ -6227,7 +6190,7 @@ seedrandom@3.0.5:
resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7"
integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==
semver@7.3.7, semver@^7.3.7:
semver@7.3.7:
version "7.3.7"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f"
integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==
@ -6288,17 +6251,17 @@ sha.js@^2.4.11:
inherits "^2.0.1"
safe-buffer "^5.0.1"
sharp@0.30.4:
version "0.30.4"
resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.30.4.tgz#73d9daa63bbc20da189c9328d75d5d395fc8fb73"
integrity sha512-3Onig53Y6lji4NIZo69s14mERXXY/GV++6CzOYx/Rd8bnTwbhFbL09WZd7Ag/CCnA0WxFID8tkY0QReyfL6v0Q==
sharp@0.29.3:
version "0.29.3"
resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.29.3.tgz#0da183d626094c974516a48fab9b3e4ba92eb5c2"
integrity sha512-fKWUuOw77E4nhpyzCCJR1ayrttHoFHBT2U/kR/qEMRhvPEcluG4BKj324+SCO1e84+knXHwhJ1HHJGnUt4ElGA==
dependencies:
color "^4.2.3"
detect-libc "^2.0.1"
node-addon-api "^4.3.0"
prebuild-install "^7.0.1"
semver "^7.3.7"
simple-get "^4.0.1"
color "^4.0.1"
detect-libc "^1.0.3"
node-addon-api "^4.2.0"
prebuild-install "^7.0.0"
semver "^7.3.5"
simple-get "^4.0.0"
tar-fs "^2.1.1"
tunnel-agent "^0.6.0"
@ -6343,7 +6306,7 @@ simple-concat@^1.0.0:
resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f"
integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==
simple-get@^4.0.0, simple-get@^4.0.1:
simple-get@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543"
integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==

View file

@ -42,7 +42,6 @@
"nested-property": "4.0.0",
"parse5": "6.0.1",
"photoswipe": "5.2.7",
"portscanner": "2.2.0",
"prismjs": "1.28.0",
"private-ip": "2.3.3",
"promise-limit": "2.7.0",
@ -75,6 +74,13 @@
"vuedraggable": "4.0.1",
"wavesurfer.js": "6.0.1",
"websocket": "1.0.34",
"@vitejs/plugin-vue": "2.3.3",
"@vue/compiler-sfc": "3.2.33",
"@rollup/plugin-alias": "3.1.9",
"@rollup/plugin-json": "4.1.0",
"rollup": "2.73.0",
"typescript": "4.6.4",
"vite": "2.9.9",
"ws": "8.6.0"
},
"devDependencies": {
@ -99,14 +105,6 @@
"@types/ws": "8.5.3",
"@typescript-eslint/eslint-plugin": "5.23.0",
"@typescript-eslint/parser": "5.23.0",
"@vitejs/plugin-vue": "2.3.3",
"@vue/compiler-sfc": "3.2.33",
"@rollup/plugin-alias": "3.1.9",
"@rollup/plugin-json": "4.1.0",
"rollup": "2.73.0",
"typescript": "4.6.4",
"vite": "2.9.9",
"eslint": "8.15.0",
"eslint-plugin-vue": "8.7.1",
"cross-env": "7.0.3",

View file

@ -1,6 +1,17 @@
<template>
<div class="yxspomdl" :class="{ inline, colored, mini }">
<div class="ring"></div>
<div class="container">
<svg class="spinner bg" viewBox="0 0 168 168" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1.125,0,0,1.125,12,12)">
<circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:21.33px;"/>
</g>
</svg>
<svg class="spinner fg" viewBox="0 0 168 168" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1.125,0,0,1.125,12,12)">
<path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:21.33px;"/>
</g>
</svg>
</div>
</div>
</template>
@ -19,7 +30,7 @@ const props = withDefaults(defineProps<{
</script>
<style lang="scss" scoped>
@keyframes ring {
@keyframes spinner {
0% {
transform: rotate(0deg);
}
@ -33,7 +44,7 @@ const props = withDefaults(defineProps<{
text-align: center;
cursor: wait;
--size: 48px;
--size: 40px;
&.colored {
color: var(--accent);
@ -50,32 +61,31 @@ const props = withDefaults(defineProps<{
--size: 32px;
}
> .ring {
> .container {
position: relative;
display: inline-block;
vertical-align: middle;
width: var(--size);
height: var(--size);
margin: 0 auto;
&:before,
&:after {
content: " ";
display: block;
box-sizing: border-box;
width: var(--size);
height: var(--size);
border-radius: 50%;
border: solid 4px;
}
&:before {
border-color: currentColor;
opacity: 0.3;
}
&:after {
> .spinner {
position: absolute;
top: 0;
border-color: currentColor transparent transparent transparent;
animation: ring 0.5s linear infinite;
left: 0;
width: var(--size);
height: var(--size);
fill-rule: evenodd;
clip-rule: evenodd;
stroke-linecap: round;
stroke-linejoin: round;
stroke-miterlimit: 1.5;
}
> .bg {
opacity: 0.275;
}
> .fg {
animation: spinner 0.5s linear infinite;
}
}
}

View file

@ -31,6 +31,32 @@ const props = withDefaults(defineProps<{
}
}
.mfm-x2 {
--mfm-zoom-size: 200%;
}
.mfm-x3 {
--mfm-zoom-size: 400%;
}
.mfm-x4 {
--mfm-zoom-size: 600%;
}
.mfm-x2, .mfm-x3, .mfm-x4 {
font-size: var(--mfm-zoom-size);
.mfm-x2, .mfm-x3, .mfm-x4 {
/* only half effective */
font-size: calc(var(--mfm-zoom-size) / 2 + 50%);
.mfm-x2, .mfm-x3, .mfm-x4 {
/* disabled */
font-size: 100%;
}
}
}
@keyframes mfm-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }

View file

@ -39,6 +39,19 @@ const bg = {
border-radius: 4px 0 0 4px;
overflow: hidden;
color: #fff;
text-shadow: /* .866 ≈ sin(60deg) */
1px 0 1px #000,
.866px .5px 1px #000,
.5px .866px 1px #000,
0 1px 1px #000,
-.5px .866px 1px #000,
-.866px .5px 1px #000,
-1px 0 1px #000,
-.866px -.5px 1px #000,
-.5px -.866px 1px #000,
0 -1px 1px #000,
.5px -.866px 1px #000,
.866px -.5px 1px #000;
> .icon {
height: 100%;

View file

@ -142,16 +142,19 @@ export default defineComponent({
break;
}
case 'x2': {
style = `font-size: 200%;`;
break;
return h('span', {
class: 'mfm-x2',
}, genEl(token.children));
}
case 'x3': {
style = `font-size: 400%;`;
break;
return h('span', {
class: 'mfm-x3',
}, genEl(token.children));
}
case 'x4': {
style = `font-size: 600%;`;
break;
return h('span', {
class: 'mfm-x4',
}, genEl(token.children));
}
case 'font': {
const family =

View file

@ -1,34 +1,22 @@
<template>
<div class="lzyxtsnt">
<img v-if="image" :src="image.url"/>
<ImgWithBlurhash v-if="image" :hash="image.blurhash" :src="image.url" :alt="image.comment" :title="image.comment" :cover="false"/>
</div>
</template>
<script lang="ts">
<script lang="ts" setup>
import { defineComponent, PropType } from 'vue';
import ImgWithBlurhash from '@/components/img-with-blurhash.vue';
import * as os from '@/os';
import { ImageBlock } from '@/scripts/hpml/block';
import { Hpml } from '@/scripts/hpml/evaluator';
export default defineComponent({
props: {
block: {
type: Object as PropType<ImageBlock>,
required: true
},
hpml: {
type: Object as PropType<Hpml>,
required: true
}
},
setup(props, ctx) {
const image = props.hpml.page.attachedFiles.find(x => x.id === props.block.fileId);
const props = defineProps<{
block: PropType<ImageBlock>,
hpml: PropType<Hpml>,
}>();
return {
image
};
}
});
const image = props.hpml.page.attachedFiles.find(x => x.id === props.block.fileId);
</script>
<style lang="scss" scoped>

View file

@ -7,8 +7,8 @@
:class="{ reacted: note.myReaction == reaction, canToggle }"
@click="toggleReaction()"
>
<XReactionIcon :reaction="reaction" :custom-emojis="note.emojis"/>
<span>{{ count }}</span>
<XReactionIcon class="icon" :reaction="reaction" :custom-emojis="note.emojis"/>
<span class="count">{{ count }}</span>
</button>
</template>
@ -141,12 +141,16 @@ export default defineComponent({
background: var(--accent);
}
> span {
> .count {
color: var(--fgOnAccent);
}
> .icon {
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.5));
}
}
> span {
> .count {
font-size: 0.9em;
line-height: 32px;
margin: 0 0 0 4px;

View file

@ -2,12 +2,12 @@
<XModalWindow ref="dialog"
:width="370"
:height="400"
@close="dialog.close()"
@close="onClose"
@closed="emit('closed')"
>
<template #header>{{ $ts.login }}</template>
<MkSignin :auto-set="autoSet" @login="onLogin"/>
<MkSignin :auto-set="autoSet" :message="message" @login="onLogin"/>
</XModalWindow>
</template>
@ -18,17 +18,25 @@ import MkSignin from './signin.vue';
const props = withDefaults(defineProps<{
autoSet?: boolean;
message?: string,
}>(), {
autoSet: false,
message: ''
});
const emit = defineEmits<{
(e: 'done'): void;
(e: 'closed'): void;
(ev: 'done'): void;
(ev: 'closed'): void;
(ev: 'cancelled'): void;
}>();
const dialog = $ref<InstanceType<typeof XModalWindow>>();
function onClose() {
emit('cancelled');
dialog.close();
}
function onLogin(res) {
emit('done', res);
dialog.close();

View file

@ -1,39 +1,44 @@
<template>
<form class="eppvobhk _monolithic_" :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
<div class="auth _section _formRoot">
<div v-show="withAvatar" class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null }"></div>
<div v-show="withAvatar" class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null, marginBottom: message ? '1.5em' : null }"></div>
<MkInfo v-if="message">
{{ message }}
</MkInfo>
<div v-if="!totpLogin" class="normal-signin">
<MkInput v-model="username" class="_formBlock" :placeholder="$ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
<MkInput v-model="username" class="_formBlock" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
</MkInput>
<MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" class="_formBlock" :placeholder="$ts.password" type="password" :with-password-toggle="true" required data-cy-signin-password>
<MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" class="_formBlock" :placeholder="i18n.ts.password" type="password" :with-password-toggle="true" required data-cy-signin-password>
<template #prefix><i class="fas fa-lock"></i></template>
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ $ts.forgotPassword }}</button></template>
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
</MkInput>
<MkButton class="_formBlock" type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? $ts.loggingIn : $ts.login }}</MkButton>
<MkCaptcha v-if="meta.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="meta.hcaptchaSiteKey"/>
<MkCaptcha v-if="meta.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="meta.recaptchaSiteKey"/>
<MkButton class="_formBlock" type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
</div>
<div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }">
<div v-if="user && user.securityKeys" class="twofa-group tap-group">
<p>{{ $ts.tapSecurityKey }}</p>
<p>{{ i18n.ts.tapSecurityKey }}</p>
<MkButton v-if="!queryingKey" @click="queryKey">
{{ $ts.retry }}
{{ i18n.ts.retry }}
</MkButton>
</div>
<div v-if="user && user.securityKeys" class="or-hr">
<p class="or-msg">{{ $ts.or }}</p>
<p class="or-msg">{{ i18n.ts.or }}</p>
</div>
<div class="twofa-group totp-group">
<p style="margin-bottom:0;">{{ $ts.twoStepAuthentication }}</p>
<p style="margin-bottom:0;">{{ i18n.ts.twoStepAuthentication }}</p>
<MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" :with-password-toggle="true" required>
<template #label>{{ $ts.password }}</template>
<template #label>{{ i18n.ts.password }}</template>
<template #prefix><i class="fas fa-lock"></i></template>
</MkInput>
<MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false" required>
<template #label>{{ $ts.token }}</template>
<template #label>{{ i18n.ts.token }}</template>
<template #prefix><i class="fas fa-gavel"></i></template>
</MkInput>
<MkButton type="submit" :disabled="signing" primary style="margin: 0 auto;">{{ signing ? $ts.loggingIn : $ts.login }}</MkButton>
<MkButton type="submit" :disabled="signing" primary style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
</div>
</div>
</div>
@ -45,190 +50,192 @@
</form>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
<script lang="ts" setup>
import { defineAsyncComponent } from 'vue';
import { toUnicode } from 'punycode/';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import { apiUrl, host } from '@/config';
import MkInfo from '@/components/ui/info.vue';
import { apiUrl, host as configHost } from '@/config';
import { byteify, hexify } from '@/scripts/2fa';
import * as os from '@/os';
import { login } from '@/account';
import { showSuspendedDialog } from '../scripts/show-suspended-dialog';
import { instance } from '@/instance';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkButton,
MkInput,
const MkCaptcha = defineAsyncComponent(() => import('./captcha.vue'));
let signing = $ref(false);
let user = $ref(null);
let username = $ref('');
let password = $ref('');
let token = $ref('');
let host = $ref(toUnicode(configHost));
let totpLogin = $ref(false);
let credential = $ref(null);
let challengeData = $ref(null);
let queryingKey = $ref(false);
let hCaptchaResponse = $ref(null);
let reCaptchaResponse = $ref(null);
const meta = $computed(() => instance);
const emit = defineEmits<{
(ev: 'login', v: any): void;
}>();
const props = defineProps({
withAvatar: {
type: Boolean,
required: false,
default: true
},
props: {
withAvatar: {
type: Boolean,
required: false,
default: true
},
autoSet: {
type: Boolean,
required: false,
default: false,
}
autoSet: {
type: Boolean,
required: false,
default: false,
},
emits: ['login'],
data() {
return {
signing: false,
user: null,
username: '',
password: '',
token: '',
apiUrl,
host: toUnicode(host),
totpLogin: false,
credential: null,
challengeData: null,
queryingKey: false,
};
},
computed: {
meta() {
return this.$instance;
},
},
methods: {
onUsernameChange() {
os.api('users/show', {
username: this.username
}).then(user => {
this.user = user;
}, () => {
this.user = null;
});
},
onLogin(res) {
if (this.autoSet) {
return login(res.i);
} else {
return;
}
},
queryKey() {
this.queryingKey = true;
return navigator.credentials.get({
publicKey: {
challenge: byteify(this.challengeData.challenge, 'base64'),
allowCredentials: this.challengeData.securityKeys.map(key => ({
id: byteify(key.id, 'hex'),
type: 'public-key',
transports: ['usb', 'nfc', 'ble', 'internal']
})),
timeout: 60 * 1000
}
}).catch(() => {
this.queryingKey = false;
return Promise.reject(null);
}).then(credential => {
this.queryingKey = false;
this.signing = true;
return os.api('signin', {
username: this.username,
password: this.password,
signature: hexify(credential.response.signature),
authenticatorData: hexify(credential.response.authenticatorData),
clientDataJSON: hexify(credential.response.clientDataJSON),
credentialId: credential.id,
challengeId: this.challengeData.challengeId
});
}).then(res => {
this.$emit('login', res);
return this.onLogin(res);
}).catch(err => {
if (err === null) return;
os.alert({
type: 'error',
text: this.$ts.signinFailed
});
this.signing = false;
});
},
onSubmit() {
this.signing = true;
if (!this.totpLogin && this.user && this.user.twoFactorEnabled) {
if (window.PublicKeyCredential && this.user.securityKeys) {
os.api('signin', {
username: this.username,
password: this.password
}).then(res => {
this.totpLogin = true;
this.signing = false;
this.challengeData = res;
return this.queryKey();
}).catch(this.loginFailed);
} else {
this.totpLogin = true;
this.signing = false;
}
} else {
os.api('signin', {
username: this.username,
password: this.password,
token: this.user && this.user.twoFactorEnabled ? this.token : undefined
}).then(res => {
this.$emit('login', res);
this.onLogin(res);
}).catch(this.loginFailed);
}
},
loginFailed(err) {
switch (err.id) {
case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
os.alert({
type: 'error',
title: this.$ts.loginFailed,
text: this.$ts.noSuchUser
});
break;
}
case '932c904e-9460-45b7-9ce6-7ed33be7eb2c': {
os.alert({
type: 'error',
title: this.$ts.loginFailed,
text: this.$ts.incorrectPassword,
});
break;
}
case 'e03a5f46-d309-4865-9b69-56282d94e1eb': {
showSuspendedDialog();
break;
}
default: {
os.alert({
type: 'error',
title: this.$ts.loginFailed,
text: JSON.stringify(err)
});
}
}
this.challengeData = null;
this.totpLogin = false;
this.signing = false;
},
resetPassword() {
os.popup(defineAsyncComponent(() => import('@/components/forgot-password.vue')), {}, {
}, 'closed');
}
message: {
type: String,
required: false,
default: ''
}
});
function onUsernameChange() {
os.api('users/show', {
username: username
}).then(user => {
user = user;
}, () => {
user = null;
});
}
function onLogin(res) {
if (props.autoSet) {
return login(res.i);
}
}
function queryKey() {
queryingKey = true;
return navigator.credentials.get({
publicKey: {
challenge: byteify(challengeData.challenge, 'base64'),
allowCredentials: challengeData.securityKeys.map(key => ({
id: byteify(key.id, 'hex'),
type: 'public-key',
transports: ['usb', 'nfc', 'ble', 'internal']
})),
timeout: 60 * 1000
}
}).catch(() => {
queryingKey = false;
return Promise.reject(null);
}).then(credential => {
queryingKey = false;
signing = true;
return os.api('signin', {
username,
password,
signature: hexify(credential.response.signature),
authenticatorData: hexify(credential.response.authenticatorData),
clientDataJSON: hexify(credential.response.clientDataJSON),
credentialId: credential.id,
challengeId: challengeData.challengeId,
'hcaptcha-response': hCaptchaResponse,
'g-recaptcha-response': reCaptchaResponse,
});
}).then(res => {
emit('login', res);
return onLogin(res);
}).catch(err => {
if (err === null) return;
os.alert({
type: 'error',
text: i18n.ts.signinFailed
});
signing = false;
});
}
function onSubmit() {
signing = true;
console.log('submit')
if (!totpLogin && user && user.twoFactorEnabled) {
if (window.PublicKeyCredential && user.securityKeys) {
os.api('signin', {
username,
password,
'hcaptcha-response': hCaptchaResponse,
'g-recaptcha-response': reCaptchaResponse,
}).then(res => {
totpLogin = true;
signing = false;
challengeData = res;
return queryKey();
}).catch(loginFailed);
} else {
totpLogin = true;
signing = false;
}
} else {
os.api('signin', {
username,
password,
'hcaptcha-response': hCaptchaResponse,
'g-recaptcha-response': reCaptchaResponse,
token: user && user.twoFactorEnabled ? token : undefined
}).then(res => {
emit('login', res);
onLogin(res);
}).catch(loginFailed);
}
}
function loginFailed(err) {
switch (err.id) {
case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
os.alert({
type: 'error',
title: i18n.ts.loginFailed,
text: i18n.ts.noSuchUser
});
break;
}
case '932c904e-9460-45b7-9ce6-7ed33be7eb2c': {
os.alert({
type: 'error',
title: i18n.ts.loginFailed,
text: i18n.ts.incorrectPassword,
});
break;
}
case 'e03a5f46-d309-4865-9b69-56282d94e1eb': {
showSuspendedDialog();
break;
}
default: {
console.log(err)
os.alert({
type: 'error',
title: i18n.ts.loginFailed,
text: JSON.stringify(err)
});
}
}
challengeData = null;
totpLogin = false;
signing = false;
}
function resetPassword() {
os.popup(defineAsyncComponent(() => import('@/components/forgot-password.vue')), {}, {
}, 'closed');
}
</script>
<style lang="scss" scoped>

View file

@ -58,8 +58,8 @@
</template>
</I18n>
</MkSwitch>
<captcha v-if="meta.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="meta.hcaptchaSiteKey"/>
<captcha v-if="meta.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="meta.recaptchaSiteKey"/>
<MkCaptcha v-if="meta.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="meta.hcaptchaSiteKey"/>
<MkCaptcha v-if="meta.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="meta.recaptchaSiteKey"/>
<MkButton class="_formBlock" type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ $ts.start }}</MkButton>
</template>
</form>
@ -81,7 +81,7 @@ export default defineComponent({
MkButton,
MkInput,
MkSwitch,
captcha: defineAsyncComponent(() => import('./captcha.vue')),
MkCaptcha: defineAsyncComponent(() => import('./captcha.vue')),
},
props: {

View file

@ -14,8 +14,14 @@
</div>
<div v-else ref="rootEl">
<div v-show="pagination.reversed && more" key="_more_" class="cxiknjgy _gap">
<MkButton v-if="!moreFetching" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMoreAhead">
{{ $ts.loadMore }}
</MkButton>
<MkLoading v-else class="loading"/>
</div>
<slot :items="items"></slot>
<div v-show="more" key="_more_" class="cxiknjgy _gap">
<div v-show="!pagination.reversed && more" key="_more_" class="cxiknjgy _gap">
<MkButton v-if="!moreFetching" v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
{{ $ts.loadMore }}
</MkButton>
@ -244,6 +250,11 @@ const append = (item: Item): void => {
items.value.push(item);
};
const removeItem = (finder: (item: Item) => boolean) => {
const i = items.value.findIndex(finder);
items.value.splice(i, 1);
};
const updateItem = (id: Item['id'], replacer: (old: Item) => Item): void => {
const i = items.value.findIndex(item => item.id === id);
items.value[i] = replacer(items.value[i]);
@ -273,9 +284,9 @@ defineExpose({
queue,
backed,
reload,
fetchMoreAhead,
prepend,
append,
removeItem,
updateItem,
});
</script>

View file

@ -150,6 +150,7 @@ const patrons = [
'Weeble',
'蝉暮せせせ',
'ThatOneCalculator',
'pixeldesu',
];
let easterEggReady = false;

View file

@ -27,85 +27,71 @@
</XModalWindow>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import XModalWindow from '@/components/ui/modal-window.vue';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/form/input.vue';
import * as os from '@/os';
import { unique } from '@/scripts/array';
import { i18n } from '@/i18n';
import { emojiCategories } from '@/instance';
export default defineComponent({
components: {
XModalWindow,
MkButton,
MkInput,
},
const props = defineProps<{
emoji: any,
}>();
props: {
emoji: {
required: true,
let dialog = $ref(null);
let name: string = $ref(props.emoji.name);
let category: string = $ref(props.emoji.category);
let aliases: string = $ref(props.emoji.aliases.join(' '));
let categories: string[] = $ref(emojiCategories);
const emit = defineEmits<{
(ev: 'done', v: { deleted?: boolean, updated?: any }): void,
(ev: 'closed'): void
}>();
function ok() {
update();
}
async function update() {
await os.apiWithDialog('admin/emoji/update', {
id: props.emoji.id,
name,
category,
aliases: aliases.split(' '),
});
emit('done', {
updated: {
id: props.emoji.id,
name,
category,
aliases: aliases.split(' '),
}
},
});
emits: ['done', 'closed'],
dialog.close();
}
data() {
return {
name: this.emoji.name,
category: this.emoji.category,
aliases: this.emoji.aliases?.join(' '),
categories: [],
}
},
async function del() {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.t('removeAreYouSure', { x: name }),
});
if (canceled) return;
created() {
os.api('meta', { detail: false }).then(({ emojis }) => {
this.categories = unique(emojis.map((x: any) => x.category || '').filter((x: string) => x !== ''));
os.api('admin/emoji/delete', {
id: props.emoji.id
}).then(() => {
emit('done', {
deleted: true
});
},
methods: {
ok() {
this.update();
},
async update() {
await os.apiWithDialog('admin/emoji/update', {
id: this.emoji.id,
name: this.name,
category: this.category,
aliases: this.aliases.split(' '),
});
this.$emit('done', {
updated: {
name: this.name,
category: this.category,
aliases: this.aliases.split(' '),
}
});
this.$refs.dialog.close();
},
async del() {
const { canceled } = await os.confirm({
type: 'warning',
text: this.$t('removeAreYouSure', { x: this.emoji.name }),
});
if (canceled) return;
os.api('admin/emoji/delete', {
id: this.emoji.id
}).then(() => {
this.$emit('done', {
deleted: true
});
this.$refs.dialog.close();
});
},
}
});
dialog.close();
});
}
</script>
<style lang="scss" scoped>

View file

@ -135,12 +135,12 @@ const edit = (emoji) => {
}, {
done: result => {
if (result.updated) {
emojisPaginationComponent.value.replaceItem(item => item.id === emoji.id, {
...emoji,
emojisPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({
...oldEmoji,
...result.updated
});
}));
} else if (result.deleted) {
emojisPaginationComponent.value.removeItem(item => item.id === emoji.id);
emojisPaginationComponent.value.removeItem((item) => item.id === emoji.id);
}
},
}, 'closed');

View file

@ -34,74 +34,52 @@
</XModalWindow>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import MkButton from '@/components/ui/button.vue';
import MkSwitch from '@/components/form/switch.vue';
import XModalWindow from '@/components/ui/modal-window.vue';
import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue';
import bytes from '@/filters/bytes';
import * as os from '@/os';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkButton,
MkSwitch,
XModalWindow,
MkDriveFileThumbnail,
},
let file: any = $ref(null);
let info: any = $ref(null);
let isSensitive: boolean = $ref(false);
props: {
fileId: {
required: true,
}
},
const props = defineProps<{
fileId: string,
}>();
emits: ['closed'],
async function fetch() {
file = await os.api('drive/files/show', { fileId: props.fileId });
info = await os.api('admin/drive/show-file', { fileId: props.fileId });
isSensitive = file.isSensitive;
}
data() {
return {
file: null,
info: null,
isSensitive: false,
};
},
fetch();
created() {
this.fetch();
},
function showUser() {
os.pageWindow(`/user-info/${file.userId}`);
}
methods: {
async fetch() {
this.file = await os.api('drive/files/show', { fileId: this.fileId });
this.info = await os.api('admin/drive/show-file', { fileId: this.fileId });
this.isSensitive = this.file.isSensitive;
},
async function del() {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.t('removeAreYouSure', { x: file.name }),
});
if (canceled) return;
showUser() {
os.pageWindow(`/user-info/${this.file.userId}`);
},
os.apiWithDialog('drive/files/delete', {
fileId: file.id
});
}
async del() {
const { canceled } = await os.confirm({
type: 'warning',
text: this.$t('removeAreYouSure', { x: this.file.name }),
});
if (canceled) return;
os.apiWithDialog('drive/files/delete', {
fileId: this.file.id
});
},
async toggleIsSensitive(v) {
await os.api('drive/files/update', { fileId: this.fileId, isSensitive: v });
this.isSensitive = v;
},
bytes
}
});
async function toggleIsSensitive(v) {
await os.api('drive/files/update', { fileId: props.fileId, isSensitive: v });
isSensitive = v;
}
</script>
<style lang="scss" scoped>

View file

@ -1,6 +1,6 @@
<template>
<div ref="el" class="hiyeyicy" :class="{ wide: !narrow }">
<div v-if="!narrow || page == null" class="nav">
<div v-if="!narrow || initialPage == null" class="nav">
<MkHeader :info="header"></MkHeader>
<MkSpacer :content-max="700" :margin-min="16">
@ -12,21 +12,21 @@
<MkInfo v-if="noMaintainerInformation" warn class="info">{{ $ts.noMaintainerInformationWarning }} <MkA to="/admin/settings" class="_link">{{ $ts.configure }}</MkA></MkInfo>
<MkInfo v-if="noBotProtection" warn class="info">{{ $ts.noBotProtectionWarning }} <MkA to="/admin/security" class="_link">{{ $ts.configure }}</MkA></MkInfo>
<MkSuperMenu :def="menuDef" :grid="page == null"></MkSuperMenu>
<MkSuperMenu :def="menuDef" :grid="initialPage == null"></MkSuperMenu>
</div>
</MkSpacer>
</div>
<div class="main">
<div v-if="!(narrow && initialPage == null)" class="main">
<MkStickyContainer>
<template #header><MkHeader v-if="childInfo && !childInfo.hideHeader" :info="childInfo"/></template>
<component :is="component" :ref="el => pageChanged(el)" :key="page" v-bind="pageProps"/>
<component :is="component" :ref="el => pageChanged(el)" :key="initialPage" v-bind="pageProps"/>
</MkStickyContainer>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineAsyncComponent, defineComponent, isRef, nextTick, onMounted, reactive, ref, watch } from 'vue';
<script lang="ts" setup>
import { defineAsyncComponent, nextTick, onMounted, onUnmounted, provide, watch } from 'vue';
import { i18n } from '@/i18n';
import MkSuperMenu from '@/components/ui/super-menu.vue';
import MkInfo from '@/components/ui/info.vue';
@ -35,292 +35,277 @@ import { instance } from '@/instance';
import * as symbols from '@/symbols';
import * as os from '@/os';
import { lookupUser } from '@/scripts/lookup-user';
import { MisskeyNavigator } from '@/scripts/navigate';
export default defineComponent({
components: {
MkSuperMenu,
MkInfo,
},
const isEmpty = (x: string | null) => x == null || x === '';
provide: {
shouldOmitHeaderTitle: false,
},
const nav = new MisskeyNavigator();
props: {
initialPage: {
type: String,
required: false
const indexInfo = {
title: i18n.ts.controlPanel,
icon: 'fas fa-cog',
bg: 'var(--bg)',
hideHeader: true,
};
const props = defineProps<{
initialPage?: string,
}>();
provide('shouldOmitHeaderTitle', false);
let INFO = $ref(indexInfo);
let childInfo = $ref(null);
let page = $ref(props.initialPage);
let narrow = $ref(false);
let view = $ref(null);
let el = $ref(null);
let pageProps = $ref({});
let noMaintainerInformation = isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail);
let noBotProtection = !instance.enableHcaptcha && !instance.enableRecaptcha;
const NARROW_THRESHOLD = 600;
const ro = new ResizeObserver((entries, observer) => {
if (entries.length === 0) return;
narrow = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD;
});
const menuDef = $computed(() => [{
title: i18n.ts.quickAction,
items: [{
type: 'button',
icon: 'fas fa-search',
text: i18n.ts.lookup,
action: lookup,
}, ...(instance.disableRegistration ? [{
type: 'button',
icon: 'fas fa-user',
text: i18n.ts.invite,
action: invite,
}] : [])],
}, {
title: i18n.ts.administration,
items: [{
icon: 'fas fa-tachometer-alt',
text: i18n.ts.dashboard,
to: '/admin/overview',
active: props.initialPage === 'overview',
}, {
icon: 'fas fa-users',
text: i18n.ts.users,
to: '/admin/users',
active: props.initialPage === 'users',
}, {
icon: 'fas fa-laugh',
text: i18n.ts.customEmojis,
to: '/admin/emojis',
active: props.initialPage === 'emojis',
}, {
icon: 'fas fa-globe',
text: i18n.ts.federation,
to: '/admin/federation',
active: props.initialPage === 'federation',
}, {
icon: 'fas fa-clipboard-list',
text: i18n.ts.jobQueue,
to: '/admin/queue',
active: props.initialPage === 'queue',
}, {
icon: 'fas fa-cloud',
text: i18n.ts.files,
to: '/admin/files',
active: props.initialPage === 'files',
}, {
icon: 'fas fa-broadcast-tower',
text: i18n.ts.announcements,
to: '/admin/announcements',
active: props.initialPage === 'announcements',
}, {
icon: 'fas fa-audio-description',
text: i18n.ts.ads,
to: '/admin/ads',
active: props.initialPage === 'ads',
}, {
icon: 'fas fa-exclamation-circle',
text: i18n.ts.abuseReports,
to: '/admin/abuses',
active: props.initialPage === 'abuses',
}],
}, {
title: i18n.ts.settings,
items: [{
icon: 'fas fa-cog',
text: i18n.ts.general,
to: '/admin/settings',
active: props.initialPage === 'settings',
}, {
icon: 'fas fa-envelope',
text: i18n.ts.emailServer,
to: '/admin/email-settings',
active: props.initialPage === 'email-settings',
}, {
icon: 'fas fa-cloud',
text: i18n.ts.objectStorage,
to: '/admin/object-storage',
active: props.initialPage === 'object-storage',
}, {
icon: 'fas fa-lock',
text: i18n.ts.security,
to: '/admin/security',
active: props.initialPage === 'security',
}, {
icon: 'fas fa-globe',
text: i18n.ts.relays,
to: '/admin/relays',
active: props.initialPage === 'relays',
}, {
icon: 'fas fa-share-alt',
text: i18n.ts.integration,
to: '/admin/integrations',
active: props.initialPage === 'integrations',
}, {
icon: 'fas fa-ban',
text: i18n.ts.instanceBlocking,
to: '/admin/instance-block',
active: props.initialPage === 'instance-block',
}, {
icon: 'fas fa-ghost',
text: i18n.ts.proxyAccount,
to: '/admin/proxy-account',
active: props.initialPage === 'proxy-account',
}, {
icon: 'fas fa-cogs',
text: i18n.ts.other,
to: '/admin/other-settings',
active: props.initialPage === 'other-settings',
}],
}, {
title: i18n.ts.info,
items: [{
icon: 'fas fa-database',
text: i18n.ts.database,
to: '/admin/database',
active: props.initialPage === 'database',
}],
}]);
const component = $computed(() => {
if (props.initialPage == null) return null;
switch (props.initialPage) {
case 'overview': return defineAsyncComponent(() => import('./overview.vue'));
case 'users': return defineAsyncComponent(() => import('./users.vue'));
case 'emojis': return defineAsyncComponent(() => import('./emojis.vue'));
case 'federation': return defineAsyncComponent(() => import('../federation.vue'));
case 'queue': return defineAsyncComponent(() => import('./queue.vue'));
case 'files': return defineAsyncComponent(() => import('./files.vue'));
case 'announcements': return defineAsyncComponent(() => import('./announcements.vue'));
case 'ads': return defineAsyncComponent(() => import('./ads.vue'));
case 'database': return defineAsyncComponent(() => import('./database.vue'));
case 'abuses': return defineAsyncComponent(() => import('./abuses.vue'));
case 'settings': return defineAsyncComponent(() => import('./settings.vue'));
case 'email-settings': return defineAsyncComponent(() => import('./email-settings.vue'));
case 'object-storage': return defineAsyncComponent(() => import('./object-storage.vue'));
case 'security': return defineAsyncComponent(() => import('./security.vue'));
case 'relays': return defineAsyncComponent(() => import('./relays.vue'));
case 'integrations': return defineAsyncComponent(() => import('./integrations.vue'));
case 'instance-block': return defineAsyncComponent(() => import('./instance-block.vue'));
case 'proxy-account': return defineAsyncComponent(() => import('./proxy-account.vue'));
case 'other-settings': return defineAsyncComponent(() => import('./other-settings.vue'));
}
});
watch(component, () => {
pageProps = {};
nextTick(() => {
scroll(el, { top: 0 });
});
}, { immediate: true });
watch(() => props.initialPage, () => {
if (props.initialPage == null && !narrow) {
nav.push('/admin/overview');
} else {
if (props.initialPage == null) {
INFO = indexInfo;
}
},
}
});
setup(props, context) {
const indexInfo = {
title: i18n.ts.controlPanel,
icon: 'fas fa-cog',
bg: 'var(--bg)',
hideHeader: true,
};
const INFO = ref(indexInfo);
const childInfo = ref(null);
const page = ref(props.initialPage);
const narrow = ref(false);
const view = ref(null);
const el = ref(null);
const pageChanged = (page) => {
if (page == null) return;
const viewInfo = page[symbols.PAGE_INFO];
if (isRef(viewInfo)) {
watch(viewInfo, () => {
childInfo.value = viewInfo.value;
}, { immediate: true });
} else {
childInfo.value = viewInfo;
}
};
const pageProps = ref({});
watch(narrow, () => {
if (props.initialPage == null && !narrow) {
nav.push('/admin/overview');
}
});
const isEmpty = (x: any) => x == null || x == '';
onMounted(() => {
ro.observe(el);
const noMaintainerInformation = ref(false);
const noBotProtection = ref(false);
narrow = el.offsetWidth < NARROW_THRESHOLD;
if (props.initialPage == null && !narrow) {
nav.push('/admin/overview');
}
});
os.api('meta', { detail: true }).then(meta => {
// TODO: meta
noMaintainerInformation.value = isEmpty(meta.maintainerName) || isEmpty(meta.maintainerEmail);
noBotProtection.value = !meta.enableHcaptcha && !meta.enableRecaptcha;
onUnmounted(() => {
ro.disconnect();
});
const pageChanged = (page) => {
if (page == null) {
childInfo = null;
} else {
childInfo = page[symbols.PAGE_INFO];
}
};
const invite = () => {
os.api('admin/invite').then(x => {
os.alert({
type: 'info',
text: x.code
});
const menuDef = computed(() => [{
title: i18n.ts.quickAction,
items: [{
type: 'button',
icon: 'fas fa-search',
text: i18n.ts.lookup,
action: lookup,
}, ...(instance.disableRegistration ? [{
type: 'button',
icon: 'fas fa-user',
text: i18n.ts.invite,
action: invite,
}] : [])],
}, {
title: i18n.ts.administration,
items: [{
icon: 'fas fa-tachometer-alt',
text: i18n.ts.dashboard,
to: '/admin/overview',
active: page.value === 'overview',
}, {
icon: 'fas fa-users',
text: i18n.ts.users,
to: '/admin/users',
active: page.value === 'users',
}, {
icon: 'fas fa-laugh',
text: i18n.ts.customEmojis,
to: '/admin/emojis',
active: page.value === 'emojis',
}, {
icon: 'fas fa-globe',
text: i18n.ts.federation,
to: '/admin/federation',
active: page.value === 'federation',
}, {
icon: 'fas fa-clipboard-list',
text: i18n.ts.jobQueue,
to: '/admin/queue',
active: page.value === 'queue',
}, {
icon: 'fas fa-cloud',
text: i18n.ts.files,
to: '/admin/files',
active: page.value === 'files',
}, {
icon: 'fas fa-broadcast-tower',
text: i18n.ts.announcements,
to: '/admin/announcements',
active: page.value === 'announcements',
}, {
icon: 'fas fa-audio-description',
text: i18n.ts.ads,
to: '/admin/ads',
active: page.value === 'ads',
}, {
icon: 'fas fa-exclamation-circle',
text: i18n.ts.abuseReports,
to: '/admin/abuses',
active: page.value === 'abuses',
}],
}, {
title: i18n.ts.settings,
items: [{
icon: 'fas fa-cog',
text: i18n.ts.general,
to: '/admin/settings',
active: page.value === 'settings',
}, {
icon: 'fas fa-envelope',
text: i18n.ts.emailServer,
to: '/admin/email-settings',
active: page.value === 'email-settings',
}, {
icon: 'fas fa-cloud',
text: i18n.ts.objectStorage,
to: '/admin/object-storage',
active: page.value === 'object-storage',
}, {
icon: 'fas fa-lock',
text: i18n.ts.security,
to: '/admin/security',
active: page.value === 'security',
}, {
icon: 'fas fa-globe',
text: i18n.ts.relays,
to: '/admin/relays',
active: page.value === 'relays',
}, {
icon: 'fas fa-share-alt',
text: i18n.ts.integration,
to: '/admin/integrations',
active: page.value === 'integrations',
}, {
icon: 'fas fa-ban',
text: i18n.ts.instanceBlocking,
to: '/admin/instance-block',
active: page.value === 'instance-block',
}, {
icon: 'fas fa-ghost',
text: i18n.ts.proxyAccount,
to: '/admin/proxy-account',
active: page.value === 'proxy-account',
}, {
icon: 'fas fa-cogs',
text: i18n.ts.other,
to: '/admin/other-settings',
active: page.value === 'other-settings',
}],
}, {
title: i18n.ts.info,
items: [{
icon: 'fas fa-database',
text: i18n.ts.database,
to: '/admin/database',
active: page.value === 'database',
}],
}]);
const component = computed(() => {
if (page.value == null) return null;
switch (page.value) {
case 'overview': return defineAsyncComponent(() => import('./overview.vue'));
case 'users': return defineAsyncComponent(() => import('./users.vue'));
case 'emojis': return defineAsyncComponent(() => import('./emojis.vue'));
case 'federation': return defineAsyncComponent(() => import('../federation.vue'));
case 'queue': return defineAsyncComponent(() => import('./queue.vue'));
case 'files': return defineAsyncComponent(() => import('./files.vue'));
case 'announcements': return defineAsyncComponent(() => import('./announcements.vue'));
case 'ads': return defineAsyncComponent(() => import('./ads.vue'));
case 'database': return defineAsyncComponent(() => import('./database.vue'));
case 'abuses': return defineAsyncComponent(() => import('./abuses.vue'));
case 'settings': return defineAsyncComponent(() => import('./settings.vue'));
case 'email-settings': return defineAsyncComponent(() => import('./email-settings.vue'));
case 'object-storage': return defineAsyncComponent(() => import('./object-storage.vue'));
case 'security': return defineAsyncComponent(() => import('./security.vue'));
case 'relays': return defineAsyncComponent(() => import('./relays.vue'));
case 'integrations': return defineAsyncComponent(() => import('./integrations.vue'));
case 'instance-block': return defineAsyncComponent(() => import('./instance-block.vue'));
case 'proxy-account': return defineAsyncComponent(() => import('./proxy-account.vue'));
case 'other-settings': return defineAsyncComponent(() => import('./other-settings.vue'));
}
}).catch(e => {
os.alert({
type: 'error',
text: e
});
});
};
watch(component, () => {
pageProps.value = {};
const lookup = (ev) => {
os.popupMenu([{
text: i18n.ts.user,
icon: 'fas fa-user',
action: () => {
lookupUser();
}
}, {
text: i18n.ts.note,
icon: 'fas fa-pencil-alt',
action: () => {
alert('TODO');
}
}, {
text: i18n.ts.file,
icon: 'fas fa-cloud',
action: () => {
alert('TODO');
}
}, {
text: i18n.ts.instance,
icon: 'fas fa-globe',
action: () => {
alert('TODO');
}
}], ev.currentTarget ?? ev.target);
};
nextTick(() => {
scroll(el.value, { top: 0 });
});
}, { immediate: true });
watch(() => props.initialPage, () => {
if (props.initialPage == null && !narrow.value) {
page.value = 'overview';
} else {
page.value = props.initialPage;
if (props.initialPage == null) {
INFO.value = indexInfo;
}
}
});
onMounted(() => {
narrow.value = el.value.offsetWidth < 800;
if (!narrow.value) {
page.value = 'overview';
}
});
const invite = () => {
os.api('admin/invite').then(x => {
os.alert({
type: 'info',
text: x.code
});
}).catch(e => {
os.alert({
type: 'error',
text: e
});
});
};
const lookup = (ev) => {
os.popupMenu([{
text: i18n.ts.user,
icon: 'fas fa-user',
action: () => {
lookupUser();
}
}, {
text: i18n.ts.note,
icon: 'fas fa-pencil-alt',
action: () => {
alert('TODO');
}
}, {
text: i18n.ts.file,
icon: 'fas fa-cloud',
action: () => {
alert('TODO');
}
}, {
text: i18n.ts.instance,
icon: 'fas fa-globe',
action: () => {
alert('TODO');
}
}], ev.currentTarget ?? ev.target);
};
return {
[symbols.PAGE_INFO]: INFO,
menuDef,
header: {
title: i18n.ts.controlPanel,
},
noMaintainerInformation,
noBotProtection,
page,
narrow,
view,
el,
pageChanged,
childInfo,
pageProps,
component,
invite,
lookup,
};
},
defineExpose({
[symbols.PAGE_INFO]: INFO,
header: {
title: i18n.ts.controlPanel,
}
});
</script>

View file

@ -2,57 +2,45 @@
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
<FormTextarea v-model="blockedHosts" class="_formBlock">
<span>{{ $ts.blockedInstances }}</span>
<template #caption>{{ $ts.blockedInstancesDescription }}</template>
<span>{{ i18n.ts.blockedInstances }}</span>
<template #caption>{{ i18n.ts.blockedInstancesDescription }}</template>
</FormTextarea>
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
</FormSuspense>
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import FormButton from '@/components/ui/button.vue';
import FormTextarea from '@/components/form/textarea.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormButton,
FormTextarea,
FormSuspense,
},
let blockedHosts: string = $ref('');
emits: ['info'],
async function init() {
const meta = await os.api('admin/meta');
blockedHosts = meta.blockedHosts.join('\n');
}
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.instanceBlocking,
icon: 'fas fa-ban',
bg: 'var(--bg)',
},
blockedHosts: '',
}
},
function save() {
os.apiWithDialog('admin/update-meta', {
blockedHosts: blockedHosts.split('\n') || [],
}).then(() => {
fetchInstance();
});
}
methods: {
async init() {
const meta = await os.api('admin/meta');
this.blockedHosts = meta.blockedHosts.join('\n');
},
save() {
os.apiWithDialog('admin/update-meta', {
blockedHosts: this.blockedHosts.split('\n') || [],
}).then(() => {
fetchInstance();
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.instanceBlocking,
icon: 'fas fa-ban',
bg: 'var(--bg)',
}
});
</script>

View file

@ -24,57 +24,36 @@
</FormSuspense>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import FormSwitch from '@/components/form/switch.vue';
import FormInput from '@/components/form/input.vue';
import FormButton from '@/components/ui/button.vue';
import FormInfo from '@/components/ui/info.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
export default defineComponent({
components: {
FormSwitch,
FormInput,
FormInfo,
FormButton,
FormSuspense,
},
let uri: string = $ref('');
let enableDiscordIntegration: boolean = $ref(false);
let discordClientId: string | null = $ref(null);
let discordClientSecret: string | null = $ref(null);
emits: ['info'],
async function init() {
const meta = await os.api('admin/meta');
uri = meta.uri;
enableDiscordIntegration = meta.enableDiscordIntegration;
discordClientId = meta.discordClientId;
discordClientSecret = meta.discordClientSecret;
}
data() {
return {
[symbols.PAGE_INFO]: {
title: 'Discord',
icon: 'fab fa-discord'
},
enableDiscordIntegration: false,
discordClientId: null,
discordClientSecret: null,
}
},
methods: {
async init() {
const meta = await os.api('admin/meta');
this.uri = meta.uri;
this.enableDiscordIntegration = meta.enableDiscordIntegration;
this.discordClientId = meta.discordClientId;
this.discordClientSecret = meta.discordClientSecret;
},
save() {
os.apiWithDialog('admin/update-meta', {
enableDiscordIntegration: this.enableDiscordIntegration,
discordClientId: this.discordClientId,
discordClientSecret: this.discordClientSecret,
}).then(() => {
fetchInstance();
});
}
}
});
function save() {
os.apiWithDialog('admin/update-meta', {
enableDiscordIntegration,
discordClientId,
discordClientSecret,
}).then(() => {
fetchInstance();
});
}
</script>

View file

@ -24,57 +24,36 @@
</FormSuspense>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import FormSwitch from '@/components/form/switch.vue';
import FormInput from '@/components/form/input.vue';
import FormButton from '@/components/ui/button.vue';
import FormInfo from '@/components/ui/info.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
export default defineComponent({
components: {
FormSwitch,
FormInput,
FormInfo,
FormButton,
FormSuspense,
},
let uri: string = $ref('');
let enableGithubIntegration: boolean = $ref(false);
let githubClientId: string | null = $ref(null);
let githubClientSecret: string | null = $ref(null);
emits: ['info'],
async function init() {
const meta = await os.api('admin/meta');
uri = meta.uri;
enableGithubIntegration = meta.enableGithubIntegration;
githubClientId = meta.githubClientId;
githubClientSecret = meta.githubClientSecret;
}
data() {
return {
[symbols.PAGE_INFO]: {
title: 'GitHub',
icon: 'fab fa-github'
},
enableGithubIntegration: false,
githubClientId: null,
githubClientSecret: null,
}
},
methods: {
async init() {
const meta = await os.api('admin/meta');
this.uri = meta.uri;
this.enableGithubIntegration = meta.enableGithubIntegration;
this.githubClientId = meta.githubClientId;
this.githubClientSecret = meta.githubClientSecret;
},
save() {
os.apiWithDialog('admin/update-meta', {
enableGithubIntegration: this.enableGithubIntegration,
githubClientId: this.githubClientId,
githubClientSecret: this.githubClientSecret,
}).then(() => {
fetchInstance();
});
}
}
});
function save() {
os.apiWithDialog('admin/update-meta', {
enableGithubIntegration,
githubClientId,
githubClientSecret,
}).then(() => {
fetchInstance();
});
}
</script>

View file

@ -24,7 +24,7 @@
</FormSuspense>
</template>
<script lang="ts">
<script lang="ts" setup>
import { defineComponent } from 'vue';
import FormSwitch from '@/components/form/switch.vue';
import FormInput from '@/components/form/input.vue';
@ -32,49 +32,28 @@ import FormButton from '@/components/ui/button.vue';
import FormInfo from '@/components/ui/info.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
export default defineComponent({
components: {
FormSwitch,
FormInput,
FormInfo,
FormButton,
FormSuspense,
},
let uri: string = $ref('');
let enableTwitterIntegration: boolean = $ref(false);
let twitterConsumerKey: string | null = $ref(null);
let twitterConsumerSecret: string | null = $ref(null);
emits: ['info'],
async function init() {
const meta = await os.api('admin/meta');
uri = meta.uri;
enableTwitterIntegration = meta.enableTwitterIntegration;
twitterConsumerKey = meta.twitterConsumerKey;
twitterConsumerSecret = meta.twitterConsumerSecret;
}
data() {
return {
[symbols.PAGE_INFO]: {
title: 'Twitter',
icon: 'fab fa-twitter'
},
enableTwitterIntegration: false,
twitterConsumerKey: null,
twitterConsumerSecret: null,
}
},
methods: {
async init() {
const meta = await os.api('admin/meta');
this.uri = meta.uri;
this.enableTwitterIntegration = meta.enableTwitterIntegration;
this.twitterConsumerKey = meta.twitterConsumerKey;
this.twitterConsumerSecret = meta.twitterConsumerSecret;
},
save() {
os.apiWithDialog('admin/update-meta', {
enableTwitterIntegration: this.enableTwitterIntegration,
twitterConsumerKey: this.twitterConsumerKey,
twitterConsumerSecret: this.twitterConsumerSecret,
}).then(() => {
fetchInstance();
});
}
}
});
function save() {
os.apiWithDialog('admin/update-meta', {
enableTwitterIntegration,
twitterConsumerKey,
twitterConsumerSecret,
}).then(() => {
fetchInstance();
});
}
</script>

View file

@ -4,69 +4,52 @@
<FormFolder class="_formBlock">
<template #icon><i class="fab fa-twitter"></i></template>
<template #label>Twitter</template>
<template #suffix>{{ enableTwitterIntegration ? $ts.enabled : $ts.disabled }}</template>
<template #suffix>{{ enableTwitterIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
<XTwitter/>
</FormFolder>
<FormFolder to="/admin/integrations/github" class="_formBlock">
<FormFolder class="_formBlock">
<template #icon><i class="fab fa-github"></i></template>
<template #label>GitHub</template>
<template #suffix>{{ enableGithubIntegration ? $ts.enabled : $ts.disabled }}</template>
<template #suffix>{{ enableGithubIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
<XGithub/>
</FormFolder>
<FormFolder to="/admin/integrations/discord" class="_formBlock">
<FormFolder class="_formBlock">
<template #icon><i class="fab fa-discord"></i></template>
<template #label>Discord</template>
<template #suffix>{{ enableDiscordIntegration ? $ts.enabled : $ts.disabled }}</template>
<template #suffix>{{ enableDiscordIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
<XDiscord/>
</FormFolder>
</FormSuspense>
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import FormFolder from '@/components/form/folder.vue';
import FormSecion from '@/components/form/section.vue';
import FormSuspense from '@/components/form/suspense.vue';
import XTwitter from './integrations.twitter.vue';
import XGithub from './integrations.github.vue';
import XDiscord from './integrations.discord.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormFolder,
FormSecion,
FormSuspense,
XTwitter,
XGithub,
XDiscord,
},
let enableTwitterIntegration: boolean = $ref(false);
let enableGithubIntegration: boolean = $ref(false);
let enableDiscordIntegration: boolean = $ref(false);
emits: ['info'],
async function init() {
const meta = await os.api('admin/meta');
enableTwitterIntegration = meta.enableTwitterIntegration;
enableGithubIntegration = meta.enableGithubIntegration;
enableDiscordIntegration = meta.enableDiscordIntegration;
}
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.integration,
icon: 'fas fa-share-alt',
bg: 'var(--bg)',
},
enableTwitterIntegration: false,
enableGithubIntegration: false,
enableDiscordIntegration: false,
}
},
methods: {
async init() {
const meta = await os.api('admin/meta');
this.enableTwitterIntegration = meta.enableTwitterIntegration;
this.enableGithubIntegration = meta.enableGithubIntegration;
this.enableDiscordIntegration = meta.enableDiscordIntegration;
},
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.integration,
icon: 'fas fa-share-alt',
bg: 'var(--bg)',
}
});
</script>

View file

@ -2,32 +2,32 @@
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
<div class="_formRoot">
<FormSwitch v-model="useObjectStorage" class="_formBlock">{{ $ts.useObjectStorage }}</FormSwitch>
<FormSwitch v-model="useObjectStorage" class="_formBlock">{{ i18n.ts.useObjectStorage }}</FormSwitch>
<template v-if="useObjectStorage">
<FormInput v-model="objectStorageBaseUrl" class="_formBlock">
<template #label>{{ $ts.objectStorageBaseUrl }}</template>
<template #caption>{{ $ts.objectStorageBaseUrlDesc }}</template>
<template #label>{{ i18n.ts.objectStorageBaseUrl }}</template>
<template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template>
</FormInput>
<FormInput v-model="objectStorageBucket" class="_formBlock">
<template #label>{{ $ts.objectStorageBucket }}</template>
<template #caption>{{ $ts.objectStorageBucketDesc }}</template>
<template #label>{{ i18n.ts.objectStorageBucket }}</template>
<template #caption>{{ i18n.ts.objectStorageBucketDesc }}</template>
</FormInput>
<FormInput v-model="objectStoragePrefix" class="_formBlock">
<template #label>{{ $ts.objectStoragePrefix }}</template>
<template #caption>{{ $ts.objectStoragePrefixDesc }}</template>
<template #label>{{ i18n.ts.objectStoragePrefix }}</template>
<template #caption>{{ i18n.ts.objectStoragePrefixDesc }}</template>
</FormInput>
<FormInput v-model="objectStorageEndpoint" class="_formBlock">
<template #label>{{ $ts.objectStorageEndpoint }}</template>
<template #caption>{{ $ts.objectStorageEndpointDesc }}</template>
<template #label>{{ i18n.ts.objectStorageEndpoint }}</template>
<template #caption>{{ i18n.ts.objectStorageEndpointDesc }}</template>
</FormInput>
<FormInput v-model="objectStorageRegion" class="_formBlock">
<template #label>{{ $ts.objectStorageRegion }}</template>
<template #caption>{{ $ts.objectStorageRegionDesc }}</template>
<template #label>{{ i18n.ts.objectStorageRegion }}</template>
<template #caption>{{ i18n.ts.objectStorageRegionDesc }}</template>
</FormInput>
<FormSplit :min-width="280">
@ -43,17 +43,17 @@
</FormSplit>
<FormSwitch v-model="objectStorageUseSSL" class="_formBlock">
<template #label>{{ $ts.objectStorageUseSSL }}</template>
<template #caption>{{ $ts.objectStorageUseSSLDesc }}</template>
<template #label>{{ i18n.ts.objectStorageUseSSL }}</template>
<template #caption>{{ i18n.ts.objectStorageUseSSLDesc }}</template>
</FormSwitch>
<FormSwitch v-model="objectStorageUseProxy" class="_formBlock">
<template #label>{{ $ts.objectStorageUseProxy }}</template>
<template #caption>{{ $ts.objectStorageUseProxyDesc }}</template>
<template #label>{{ i18n.ts.objectStorageUseProxy }}</template>
<template #caption>{{ i18n.ts.objectStorageUseProxyDesc }}</template>
</FormSwitch>
<FormSwitch v-model="objectStorageSetPublicRead" class="_formBlock">
<template #label>{{ $ts.objectStorageSetPublicRead }}</template>
<template #label>{{ i18n.ts.objectStorageSetPublicRead }}</template>
</FormSwitch>
<FormSwitch v-model="objectStorageS3ForcePathStyle" class="_formBlock">
@ -65,8 +65,8 @@
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import FormSwitch from '@/components/form/switch.vue';
import FormInput from '@/components/form/input.vue';
import FormGroup from '@/components/form/group.vue';
@ -76,84 +76,70 @@ import FormSection from '@/components/form/section.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormSwitch,
FormInput,
FormGroup,
FormSuspense,
FormSplit,
FormSection,
},
let useObjectStorage: boolean = $ref(false);
let objectStorageBaseUrl: string | null = $ref(null);
let objectStorageBucket: string | null = $ref(null);
let objectStoragePrefix: string | null = $ref(null);
let objectStorageEndpoint: string | null = $ref(null);
let objectStorageRegion: string | null = $ref(null);
let objectStoragePort: number | null = $ref(null);
let objectStorageAccessKey: string | null = $ref(null);
let objectStorageSecretKey: string | null = $ref(null);
let objectStorageUseSSL: boolean = $ref(false);
let objectStorageUseProxy: boolean = $ref(false);
let objectStorageSetPublicRead: boolean = $ref(false);
let objectStorageS3ForcePathStyle: boolean = $ref(true);
emits: ['info'],
async function init() {
const meta = await os.api('admin/meta');
useObjectStorage = meta.useObjectStorage;
objectStorageBaseUrl = meta.objectStorageBaseUrl;
objectStorageBucket = meta.objectStorageBucket;
objectStoragePrefix = meta.objectStoragePrefix;
objectStorageEndpoint = meta.objectStorageEndpoint;
objectStorageRegion = meta.objectStorageRegion;
objectStoragePort = meta.objectStoragePort;
objectStorageAccessKey = meta.objectStorageAccessKey;
objectStorageSecretKey = meta.objectStorageSecretKey;
objectStorageUseSSL = meta.objectStorageUseSSL;
objectStorageUseProxy = meta.objectStorageUseProxy;
objectStorageSetPublicRead = meta.objectStorageSetPublicRead;
objectStorageS3ForcePathStyle = meta.objectStorageS3ForcePathStyle;
}
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.objectStorage,
icon: 'fas fa-cloud',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-check',
text: this.$ts.save,
handler: this.save,
}],
},
useObjectStorage: false,
objectStorageBaseUrl: null,
objectStorageBucket: null,
objectStoragePrefix: null,
objectStorageEndpoint: null,
objectStorageRegion: null,
objectStoragePort: null,
objectStorageAccessKey: null,
objectStorageSecretKey: null,
objectStorageUseSSL: false,
objectStorageUseProxy: false,
objectStorageSetPublicRead: false,
objectStorageS3ForcePathStyle: true,
}
},
function save() {
os.apiWithDialog('admin/update-meta', {
useObjectStorage,
objectStorageBaseUrl,
objectStorageBucket,
objectStoragePrefix,
objectStorageEndpoint,
objectStorageRegion,
objectStoragePort,
objectStorageAccessKey,
objectStorageSecretKey,
objectStorageUseSSL,
objectStorageUseProxy,
objectStorageSetPublicRead,
objectStorageS3ForcePathStyle,
}).then(() => {
fetchInstance();
});
}
methods: {
async init() {
const meta = await os.api('admin/meta');
this.useObjectStorage = meta.useObjectStorage;
this.objectStorageBaseUrl = meta.objectStorageBaseUrl;
this.objectStorageBucket = meta.objectStorageBucket;
this.objectStoragePrefix = meta.objectStoragePrefix;
this.objectStorageEndpoint = meta.objectStorageEndpoint;
this.objectStorageRegion = meta.objectStorageRegion;
this.objectStoragePort = meta.objectStoragePort;
this.objectStorageAccessKey = meta.objectStorageAccessKey;
this.objectStorageSecretKey = meta.objectStorageSecretKey;
this.objectStorageUseSSL = meta.objectStorageUseSSL;
this.objectStorageUseProxy = meta.objectStorageUseProxy;
this.objectStorageSetPublicRead = meta.objectStorageSetPublicRead;
this.objectStorageS3ForcePathStyle = meta.objectStorageS3ForcePathStyle;
},
save() {
os.apiWithDialog('admin/update-meta', {
useObjectStorage: this.useObjectStorage,
objectStorageBaseUrl: this.objectStorageBaseUrl ? this.objectStorageBaseUrl : null,
objectStorageBucket: this.objectStorageBucket ? this.objectStorageBucket : null,
objectStoragePrefix: this.objectStoragePrefix ? this.objectStoragePrefix : null,
objectStorageEndpoint: this.objectStorageEndpoint ? this.objectStorageEndpoint : null,
objectStorageRegion: this.objectStorageRegion ? this.objectStorageRegion : null,
objectStoragePort: this.objectStoragePort ? this.objectStoragePort : null,
objectStorageAccessKey: this.objectStorageAccessKey ? this.objectStorageAccessKey : null,
objectStorageSecretKey: this.objectStorageSecretKey ? this.objectStorageSecretKey : null,
objectStorageUseSSL: this.objectStorageUseSSL,
objectStorageUseProxy: this.objectStorageUseProxy,
objectStorageSetPublicRead: this.objectStorageSetPublicRead,
objectStorageS3ForcePathStyle: this.objectStorageS3ForcePathStyle,
}).then(() => {
fetchInstance();
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.objectStorage,
icon: 'fas fa-cloud',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-check',
text: i18n.ts.save,
handler: save,
}],
}
});
</script>

View file

@ -1,19 +1,19 @@
<template>
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
<FormSuspense :p="init">
<MkInfo class="_formBlock">{{ $ts.proxyAccountDescription }}</MkInfo>
<MkInfo class="_formBlock">{{ i18n.ts.proxyAccountDescription }}</MkInfo>
<MkKeyValue class="_formBlock">
<template #key>{{ $ts.proxyAccount }}</template>
<template #value>{{ proxyAccount ? `@${proxyAccount.username}` : $ts.none }}</template>
<template #key>{{ i18n.ts.proxyAccount }}</template>
<template #value>{{ proxyAccount ? `@${proxyAccount.username}` : i18n.ts.none }}</template>
</MkKeyValue>
<FormButton primary class="_formBlock" @click="chooseProxyAccount">{{ $ts.selectAccount }}</FormButton>
<FormButton primary class="_formBlock" @click="chooseProxyAccount">{{ i18n.ts.selectAccount }}</FormButton>
</FormSuspense>
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import MkKeyValue from '@/components/key-value.vue';
import FormButton from '@/components/ui/button.vue';
import MkInfo from '@/components/ui/info.vue';
@ -21,53 +21,40 @@ import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkKeyValue,
FormButton,
MkInfo,
FormSuspense,
},
let proxyAccount: any = $ref(null);
let proxyAccountId: any = $ref(null);
emits: ['info'],
async function init() {
const meta = await os.api('admin/meta');
proxyAccountId = meta.proxyAccountId;
if (proxyAccountId) {
proxyAccount = await os.api('users/show', { userId: proxyAccountId });
}
}
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.proxyAccount,
icon: 'fas fa-ghost',
bg: 'var(--bg)',
},
proxyAccount: null,
proxyAccountId: null,
}
},
function chooseProxyAccount() {
os.selectUser().then(user => {
proxyAccount = user;
proxyAccountId = user.id;
save();
});
}
methods: {
async init() {
const meta = await os.api('admin/meta');
this.proxyAccountId = meta.proxyAccountId;
if (this.proxyAccountId) {
this.proxyAccount = await os.api('users/show', { userId: this.proxyAccountId });
}
},
function save() {
os.apiWithDialog('admin/update-meta', {
proxyAccountId: proxyAccountId,
}).then(() => {
fetchInstance();
});
}
chooseProxyAccount() {
os.selectUser().then(user => {
this.proxyAccount = user;
this.proxyAccountId = user.id;
this.save();
});
},
save() {
os.apiWithDialog('admin/update-meta', {
proxyAccountId: this.proxyAccountId,
}).then(() => {
fetchInstance();
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.proxyAccount,
icon: 'fas fa-ghost',
bg: 'var(--bg)',
}
});
</script>

View file

@ -26,62 +26,40 @@
</div>
</template>
<script lang="ts">
import { defineComponent, markRaw, onMounted, onUnmounted, ref } from 'vue';
<script lang="ts" setup>
import { onMounted, onUnmounted, ref } from 'vue';
import number from '@/filters/number';
import MkQueueChart from '@/components/queue-chart.vue';
import * as os from '@/os';
export default defineComponent({
components: {
MkQueueChart
},
const activeSincePrevTick = ref(0);
const active = ref(0);
const waiting = ref(0);
const delayed = ref(0);
const jobs = ref([]);
props: {
domain: {
type: String,
required: true,
},
connection: {
required: true,
},
},
const props = defineProps<{
domain: string,
connection: any,
}>();
setup(props) {
const activeSincePrevTick = ref(0);
const active = ref(0);
const waiting = ref(0);
const delayed = ref(0);
const jobs = ref([]);
onMounted(() => {
os.api(props.domain === 'inbox' ? 'admin/queue/inbox-delayed' : props.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(result => {
jobs.value = result;
});
onMounted(() => {
os.api(props.domain === 'inbox' ? 'admin/queue/inbox-delayed' : props.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(result => {
jobs.value = result;
});
const onStats = (stats) => {
activeSincePrevTick.value = stats[props.domain].activeSincePrevTick;
active.value = stats[props.domain].active;
waiting.value = stats[props.domain].waiting;
delayed.value = stats[props.domain].delayed;
};
const onStats = (stats) => {
activeSincePrevTick.value = stats[props.domain].activeSincePrevTick;
active.value = stats[props.domain].active;
waiting.value = stats[props.domain].waiting;
delayed.value = stats[props.domain].delayed;
};
props.connection.on('stats', onStats);
props.connection.on('stats', onStats);
onUnmounted(() => {
props.connection.off('stats', onStats);
});
});
return {
jobs,
activeSincePrevTick,
active,
waiting,
delayed,
number,
};
},
onUnmounted(() => {
props.connection.off('stats', onStats);
});
});
</script>

View file

@ -6,71 +6,60 @@
<XQueue :connection="connection" domain="deliver">
<template #title>Out</template>
</XQueue>
<MkButton danger @click="clear()"><i class="fas fa-trash-alt"></i> {{ $ts.clearQueue }}</MkButton>
<MkButton danger @click="clear()"><i class="fas fa-trash-alt"></i> {{ i18n.ts.clearQueue }}</MkButton>
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent, markRaw } from 'vue';
<script lang="ts" setup>
import { markRaw, onMounted, onBeforeUnmount, nextTick } from 'vue';
import MkButton from '@/components/ui/button.vue';
import XQueue from './queue.chart.vue';
import * as os from '@/os';
import { stream } from '@/stream';
import * as symbols from '@/symbols';
import * as config from '@/config';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkButton,
XQueue,
},
const connection = markRaw(stream.useChannel('queueStats'))
emits: ['info'],
function clear() {
os.confirm({
type: 'warning',
title: i18n.ts.clearQueueConfirmTitle,
text: i18n.ts.clearQueueConfirmText,
}).then(({ canceled }) => {
if (canceled) return;
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.jobQueue,
icon: 'fas fa-clipboard-list',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-up-right-from-square',
text: this.$ts.dashboard,
handler: () => {
window.open(config.url + '/queue', '_blank');
},
}],
},
connection: markRaw(stream.useChannel('queueStats')),
}
},
os.apiWithDialog('admin/queue/clear');
});
}
mounted() {
this.$nextTick(() => {
this.connection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
length: 200
});
onMounted(() => {
nextTick(() => {
connection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
length: 200
});
},
});
})
beforeUnmount() {
this.connection.dispose();
},
onBeforeUnmount(() => {
connection.dispose();
});
methods: {
clear() {
os.confirm({
type: 'warning',
title: this.$ts.clearQueueConfirmTitle,
text: this.$ts.clearQueueConfirmText,
}).then(({ canceled }) => {
if (canceled) return;
os.apiWithDialog('admin/queue/clear', {});
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.jobQueue,
icon: 'fas fa-clipboard-list',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-up-right-from-square',
text: i18n.ts.dashboard,
handler: () => {
window.open(config.url + '/queue', '_blank');
},
}],
}
});
</script>

View file

@ -8,84 +8,71 @@
<i v-else class="fas fa-clock icon requesting"></i>
<span>{{ $t(`_relayStatus.${relay.status}`) }}</span>
</div>
<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton>
</div>
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import MkButton from '@/components/ui/button.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkButton,
},
let relays: any[] = $ref([]);
emits: ['info'],
async function addRelay() {
const { canceled, result: inbox } = await os.inputText({
title: i18n.ts.addRelay,
type: 'url',
placeholder: i18n.ts.inboxUrl
});
if (canceled) return;
os.api('admin/relays/add', {
inbox
}).then((relay: any) => {
refresh();
}).catch((err: any) => {
os.alert({
type: 'error',
text: err.message || err
});
});
}
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.relays,
icon: 'fas fa-globe',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-plus',
text: this.$ts.addRelay,
handler: this.addRelay,
}],
},
relays: [],
inbox: '',
}
},
function remove(inbox: string) {
os.api('admin/relays/remove', {
inbox
}).then(() => {
refresh();
}).catch((err: any) => {
os.alert({
type: 'error',
text: err.message || err
});
});
}
created() {
this.refresh();
},
function refresh() {
os.api('admin/relays/list').then((relayList: any) => {
relays = relayList;
});
}
methods: {
async addRelay() {
const { canceled, result: inbox } = await os.inputText({
title: this.$ts.addRelay,
type: 'url',
placeholder: this.$ts.inboxUrl
});
if (canceled) return;
os.api('admin/relays/add', {
inbox
}).then((relay: any) => {
this.refresh();
}).catch((e: any) => {
os.alert({
type: 'error',
text: e.message || e
});
});
},
refresh();
remove(inbox: string) {
os.api('admin/relays/remove', {
inbox
}).then(() => {
this.refresh();
}).catch((e: any) => {
os.alert({
type: 'error',
text: e.message || e
});
});
},
refresh() {
os.api('admin/relays/list').then((relays: any) => {
this.relays = relays;
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.relays,
icon: 'fas fa-globe',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-plus',
text: i18n.ts.addRelay,
handler: addRelay,
}],
}
});
</script>

View file

@ -3,104 +3,104 @@
<FormSuspense :p="init">
<div class="_formRoot">
<FormInput v-model="name" class="_formBlock">
<template #label>{{ $ts.instanceName }}</template>
<template #label>{{ i18n.ts.instanceName }}</template>
</FormInput>
<FormTextarea v-model="description" class="_formBlock">
<template #label>{{ $ts.instanceDescription }}</template>
<template #label>{{ i18n.ts.instanceDescription }}</template>
</FormTextarea>
<FormInput v-model="tosUrl" class="_formBlock">
<template #prefix><i class="fas fa-link"></i></template>
<template #label>{{ $ts.tosUrl }}</template>
<template #label>{{ i18n.ts.tosUrl }}</template>
</FormInput>
<FormSplit :min-width="300">
<FormInput v-model="maintainerName" class="_formBlock">
<template #label>{{ $ts.maintainerName }}</template>
<template #label>{{ i18n.ts.maintainerName }}</template>
</FormInput>
<FormInput v-model="maintainerEmail" type="email" class="_formBlock">
<template #prefix><i class="fas fa-envelope"></i></template>
<template #label>{{ $ts.maintainerEmail }}</template>
<template #label>{{ i18n.ts.maintainerEmail }}</template>
</FormInput>
</FormSplit>
<FormTextarea v-model="pinnedUsers" class="_formBlock">
<template #label>{{ $ts.pinnedUsers }}</template>
<template #caption>{{ $ts.pinnedUsersDescription }}</template>
<template #label>{{ i18n.ts.pinnedUsers }}</template>
<template #caption>{{ i18n.ts.pinnedUsersDescription }}</template>
</FormTextarea>
<FormSection>
<FormSwitch v-model="enableRegistration" class="_formBlock">
<template #label>{{ $ts.enableRegistration }}</template>
<template #label>{{ i18n.ts.enableRegistration }}</template>
</FormSwitch>
<FormSwitch v-model="emailRequiredForSignup" class="_formBlock">
<template #label>{{ $ts.emailRequiredForSignup }}</template>
<template #label>{{ i18n.ts.emailRequiredForSignup }}</template>
</FormSwitch>
</FormSection>
<FormSection>
<FormSwitch v-model="enableLocalTimeline" class="_formBlock">{{ $ts.enableLocalTimeline }}</FormSwitch>
<FormSwitch v-model="enableGlobalTimeline" class="_formBlock">{{ $ts.enableGlobalTimeline }}</FormSwitch>
<FormInfo class="_formBlock">{{ $ts.disablingTimelinesInfo }}</FormInfo>
<FormSwitch v-model="enableLocalTimeline" class="_formBlock">{{ i18n.ts.enableLocalTimeline }}</FormSwitch>
<FormSwitch v-model="enableGlobalTimeline" class="_formBlock">{{ i18n.ts.enableGlobalTimeline }}</FormSwitch>
<FormInfo class="_formBlock">{{ i18n.ts.disablingTimelinesInfo }}</FormInfo>
</FormSection>
<FormSection>
<template #label>{{ $ts.theme }}</template>
<template #label>{{ i18n.ts.theme }}</template>
<FormInput v-model="iconUrl" class="_formBlock">
<template #prefix><i class="fas fa-link"></i></template>
<template #label>{{ $ts.iconUrl }}</template>
<template #label>{{ i18n.ts.iconUrl }}</template>
</FormInput>
<FormInput v-model="bannerUrl" class="_formBlock">
<template #prefix><i class="fas fa-link"></i></template>
<template #label>{{ $ts.bannerUrl }}</template>
<template #label>{{ i18n.ts.bannerUrl }}</template>
</FormInput>
<FormInput v-model="backgroundImageUrl" class="_formBlock">
<template #prefix><i class="fas fa-link"></i></template>
<template #label>{{ $ts.backgroundImageUrl }}</template>
<template #label>{{ i18n.ts.backgroundImageUrl }}</template>
</FormInput>
<FormInput v-model="themeColor" class="_formBlock">
<template #prefix><i class="fas fa-palette"></i></template>
<template #label>{{ $ts.themeColor }}</template>
<template #label>{{ i18n.ts.themeColor }}</template>
<template #caption>#RRGGBB</template>
</FormInput>
<FormTextarea v-model="defaultLightTheme" class="_formBlock">
<template #label>{{ $ts.instanceDefaultLightTheme }}</template>
<template #caption>{{ $ts.instanceDefaultThemeDescription }}</template>
<template #label>{{ i18n.ts.instanceDefaultLightTheme }}</template>
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
</FormTextarea>
<FormTextarea v-model="defaultDarkTheme" class="_formBlock">
<template #label>{{ $ts.instanceDefaultDarkTheme }}</template>
<template #caption>{{ $ts.instanceDefaultThemeDescription }}</template>
<template #label>{{ i18n.ts.instanceDefaultDarkTheme }}</template>
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
</FormTextarea>
</FormSection>
<FormSection>
<template #label>{{ $ts.files }}</template>
<template #label>{{ i18n.ts.files }}</template>
<FormSwitch v-model="cacheRemoteFiles" class="_formBlock">
<template #label>{{ $ts.cacheRemoteFiles }}</template>
<template #caption>{{ $ts.cacheRemoteFilesDescription }}</template>
<template #label>{{ i18n.ts.cacheRemoteFiles }}</template>
<template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}</template>
</FormSwitch>
<FormSplit :min-width="280">
<FormInput v-model="localDriveCapacityMb" type="number" class="_formBlock">
<template #label>{{ $ts.driveCapacityPerLocalAccount }}</template>
<template #label>{{ i18n.ts.driveCapacityPerLocalAccount }}</template>
<template #suffix>MB</template>
<template #caption>{{ $ts.inMb }}</template>
<template #caption>{{ i18n.ts.inMb }}</template>
</FormInput>
<FormInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" class="_formBlock">
<template #label>{{ $ts.driveCapacityPerRemoteAccount }}</template>
<template #label>{{ i18n.ts.driveCapacityPerRemoteAccount }}</template>
<template #suffix>MB</template>
<template #caption>{{ $ts.inMb }}</template>
<template #caption>{{ i18n.ts.inMb }}</template>
</FormInput>
</FormSplit>
</FormSection>
@ -109,8 +109,8 @@
<template #label>ServiceWorker</template>
<FormSwitch v-model="enableServiceWorker" class="_formBlock">
<template #label>{{ $ts.enableServiceworker }}</template>
<template #caption>{{ $ts.serviceworkerInfo }}</template>
<template #label>{{ i18n.ts.enableServiceworker }}</template>
<template #caption>{{ i18n.ts.serviceworkerInfo }}</template>
</FormSwitch>
<template v-if="enableServiceWorker">
@ -142,8 +142,8 @@
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import FormSwitch from '@/components/form/switch.vue';
import FormInput from '@/components/form/input.vue';
import FormTextarea from '@/components/form/textarea.vue';
@ -154,119 +154,103 @@ import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os';
import * as symbols from '@/symbols';
import { fetchInstance } from '@/instance';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
FormSwitch,
FormInput,
FormSuspense,
FormTextarea,
FormInfo,
FormSection,
FormSplit,
},
let name: string | null = $ref(null);
let description: string | null = $ref(null);
let tosUrl: string | null = $ref(null);
let maintainerName: string | null = $ref(null);
let maintainerEmail: string | null = $ref(null);
let iconUrl: string | null = $ref(null);
let bannerUrl: string | null = $ref(null);
let backgroundImageUrl: string | null = $ref(null);
let themeColor: any = $ref(null);
let defaultLightTheme: any = $ref(null);
let defaultDarkTheme: any = $ref(null);
let enableLocalTimeline: boolean = $ref(false);
let enableGlobalTimeline: boolean = $ref(false);
let pinnedUsers: string = $ref('');
let cacheRemoteFiles: boolean = $ref(false);
let localDriveCapacityMb: any = $ref(0);
let remoteDriveCapacityMb: any = $ref(0);
let enableRegistration: boolean = $ref(false);
let emailRequiredForSignup: boolean = $ref(false);
let enableServiceWorker: boolean = $ref(false);
let swPublicKey: any = $ref(null);
let swPrivateKey: any = $ref(null);
let deeplAuthKey: string = $ref('');
let deeplIsPro: boolean = $ref(false);
emits: ['info'],
async function init() {
const meta = await os.api('admin/meta');
name = meta.name;
description = meta.description;
tosUrl = meta.tosUrl;
iconUrl = meta.iconUrl;
bannerUrl = meta.bannerUrl;
backgroundImageUrl = meta.backgroundImageUrl;
themeColor = meta.themeColor;
defaultLightTheme = meta.defaultLightTheme;
defaultDarkTheme = meta.defaultDarkTheme;
maintainerName = meta.maintainerName;
maintainerEmail = meta.maintainerEmail;
enableLocalTimeline = !meta.disableLocalTimeline;
enableGlobalTimeline = !meta.disableGlobalTimeline;
pinnedUsers = meta.pinnedUsers.join('\n');
cacheRemoteFiles = meta.cacheRemoteFiles;
localDriveCapacityMb = meta.driveCapacityPerLocalUserMb;
remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb;
enableRegistration = !meta.disableRegistration;
emailRequiredForSignup = meta.emailRequiredForSignup;
enableServiceWorker = meta.enableServiceWorker;
swPublicKey = meta.swPublickey;
swPrivateKey = meta.swPrivateKey;
deeplAuthKey = meta.deeplAuthKey;
deeplIsPro = meta.deeplIsPro;
}
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.general,
icon: 'fas fa-cog',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-check',
text: this.$ts.save,
handler: this.save,
}],
},
name: null,
description: null,
tosUrl: null as string | null,
maintainerName: null,
maintainerEmail: null,
iconUrl: null,
bannerUrl: null,
backgroundImageUrl: null,
themeColor: null,
defaultLightTheme: null,
defaultDarkTheme: null,
enableLocalTimeline: false,
enableGlobalTimeline: false,
pinnedUsers: '',
cacheRemoteFiles: false,
localDriveCapacityMb: 0,
remoteDriveCapacityMb: 0,
enableRegistration: false,
emailRequiredForSignup: false,
enableServiceWorker: false,
swPublicKey: null,
swPrivateKey: null,
deeplAuthKey: '',
deeplIsPro: false,
}
},
function save() {
os.apiWithDialog('admin/update-meta', {
name,
description,
tosUrl,
iconUrl,
bannerUrl,
backgroundImageUrl,
themeColor: themeColor === '' ? null : themeColor,
defaultLightTheme: defaultLightTheme === '' ? null : defaultLightTheme,
defaultDarkTheme: defaultDarkTheme === '' ? null : defaultDarkTheme,
maintainerName,
maintainerEmail,
disableLocalTimeline: !enableLocalTimeline,
disableGlobalTimeline: !enableGlobalTimeline,
pinnedUsers: pinnedUsers.split('\n'),
cacheRemoteFiles,
localDriveCapacityMb: parseInt(localDriveCapacityMb, 10),
remoteDriveCapacityMb: parseInt(remoteDriveCapacityMb, 10),
disableRegistration: !enableRegistration,
emailRequiredForSignup,
enableServiceWorker,
swPublicKey,
swPrivateKey,
deeplAuthKey,
deeplIsPro,
}).then(() => {
fetchInstance();
});
}
methods: {
async init() {
const meta = await os.api('admin/meta');
this.name = meta.name;
this.description = meta.description;
this.tosUrl = meta.tosUrl;
this.iconUrl = meta.iconUrl;
this.bannerUrl = meta.bannerUrl;
this.backgroundImageUrl = meta.backgroundImageUrl;
this.themeColor = meta.themeColor;
this.defaultLightTheme = meta.defaultLightTheme;
this.defaultDarkTheme = meta.defaultDarkTheme;
this.maintainerName = meta.maintainerName;
this.maintainerEmail = meta.maintainerEmail;
this.enableLocalTimeline = !meta.disableLocalTimeline;
this.enableGlobalTimeline = !meta.disableGlobalTimeline;
this.pinnedUsers = meta.pinnedUsers.join('\n');
this.cacheRemoteFiles = meta.cacheRemoteFiles;
this.localDriveCapacityMb = meta.driveCapacityPerLocalUserMb;
this.remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb;
this.enableRegistration = !meta.disableRegistration;
this.emailRequiredForSignup = meta.emailRequiredForSignup;
this.enableServiceWorker = meta.enableServiceWorker;
this.swPublicKey = meta.swPublickey;
this.swPrivateKey = meta.swPrivateKey;
this.deeplAuthKey = meta.deeplAuthKey;
this.deeplIsPro = meta.deeplIsPro;
},
save() {
os.apiWithDialog('admin/update-meta', {
name: this.name,
description: this.description,
tosUrl: this.tosUrl,
iconUrl: this.iconUrl,
bannerUrl: this.bannerUrl,
backgroundImageUrl: this.backgroundImageUrl,
themeColor: this.themeColor === '' ? null : this.themeColor,
defaultLightTheme: this.defaultLightTheme === '' ? null : this.defaultLightTheme,
defaultDarkTheme: this.defaultDarkTheme === '' ? null : this.defaultDarkTheme,
maintainerName: this.maintainerName,
maintainerEmail: this.maintainerEmail,
disableLocalTimeline: !this.enableLocalTimeline,
disableGlobalTimeline: !this.enableGlobalTimeline,
pinnedUsers: this.pinnedUsers.split('\n'),
cacheRemoteFiles: this.cacheRemoteFiles,
localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10),
remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
disableRegistration: !this.enableRegistration,
emailRequiredForSignup: this.emailRequiredForSignup,
enableServiceWorker: this.enableServiceWorker,
swPublicKey: this.swPublicKey,
swPrivateKey: this.swPrivateKey,
deeplAuthKey: this.deeplAuthKey,
deeplIsPro: this.deeplIsPro,
}).then(() => {
fetchInstance();
});
}
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.general,
icon: 'fas fa-cog',
bg: 'var(--bg)',
actions: [{
asFullButton: true,
icon: 'fas fa-check',
text: i18n.ts.save,
handler: save,
}],
}
});
</script>

View file

@ -1,7 +1,7 @@
<template>
<MkSpacer :content-max="700">
<div class="ieepwinx">
<MkButton :link="true" to="/my/antennas/create" primary class="add"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
<MkButton :link="true" to="/my/antennas/create" primary class="add"><i class="fas fa-plus"></i> {{ i18n.ts.add }}</MkButton>
<div class="">
<MkPagination v-slot="{items}" ref="list" :pagination="pagination">
@ -14,36 +14,25 @@
</MkSpacer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import { } from 'vue';
import MkPagination from '@/components/ui/pagination.vue';
import MkButton from '@/components/ui/button.vue';
import * as symbols from '@/symbols';
import { i18n } from '@/i18n';
export default defineComponent({
components: {
MkPagination,
MkButton,
},
const pagination = {
endpoint: 'antennas/list' as const,
limit: 10,
};
data() {
return {
[symbols.PAGE_INFO]: {
title: this.$ts.manageAntennas,
icon: 'fas fa-satellite',
bg: 'var(--bg)',
action: {
icon: 'fas fa-plus',
handler: this.create
}
},
pagination: {
endpoint: 'antennas/list' as const,
limit: 10,
},
};
},
});
defineExpose({
[symbols.PAGE_INFO]: {
title: i18n.ts.manageAntennas,
icon: 'fas fa-satellite',
bg: 'var(--bg)'
}
})
</script>
<style lang="scss" scoped>

View file

@ -108,6 +108,10 @@ export default defineComponent({
},
methods: {
fetch() {
this.hasPrev = false;
this.hasNext = false;
this.showPrev = false;
this.showNext = false;
this.note = null;
os.api('notes/show', {
noteId: this.noteId

View file

@ -1,14 +1,21 @@
import { defineAsyncComponent } from 'vue';
import { $i } from '@/account';
import { i18n } from '@/i18n';
import { alert } from '@/os';
import { popup } from '@/os';
export function pleaseLogin() {
export function pleaseLogin(path?: string) {
if ($i) return;
alert({
title: i18n.ts.signinRequired,
text: null
});
popup(defineAsyncComponent(() => import('@/components/signin-dialog.vue')), {
autoSet: true,
message: i18n.ts.signinRequired
}, {
cancelled: () => {
if (path) {
window.location.href = path;
}
},
}, 'closed');
throw new Error('signin required');
}

View file

@ -22,8 +22,8 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
(e: 'loaded'): void;
(e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
(ev: 'loaded'): void;
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
}>();
let timeline = $ref<InstanceType<typeof XTimeline>>();

View file

@ -29,7 +29,7 @@ defineProps<{
}>();
const emit = defineEmits<{
(e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
}>();
/*

View file

@ -61,8 +61,8 @@ const props = withDefaults(defineProps<{
});
const emit = defineEmits<{
(e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
(e: 'change-active-state', v: boolean): void;
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
(ev: 'change-active-state', v: boolean): void;
}>();
let body = $ref<HTMLDivElement>();
@ -193,9 +193,9 @@ function goTop() {
});
}
function onDragstart(e) {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData(_DATA_TRANSFER_DECK_COLUMN_, props.column.id);
function onDragstart(ev) {
ev.dataTransfer.effectAllowed = 'move';
ev.dataTransfer.setData(_DATA_TRANSFER_DECK_COLUMN_, props.column.id);
// ChromeDragstartDOM(=)Drag
// SEE: https://stackoverflow.com/questions/19639969/html5-dragend-event-firing-immediately
@ -204,21 +204,21 @@ function onDragstart(e) {
}, 10);
}
function onDragend(e) {
function onDragend(ev) {
dragging = false;
}
function onDragover(e) {
function onDragover(ev) {
//
if (dragging) {
//
e.dataTransfer.dropEffect = 'none';
ev.dataTransfer.dropEffect = 'none';
return;
}
const isDeckColumn = e.dataTransfer.types[0] == _DATA_TRANSFER_DECK_COLUMN_;
const isDeckColumn = ev.dataTransfer.types[0] === _DATA_TRANSFER_DECK_COLUMN_;
e.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none';
ev.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none';
if (!dragging && isDeckColumn) draghover = true;
}
@ -227,12 +227,12 @@ function onDragleave() {
draghover = false;
}
function onDrop(e) {
function onDrop(ev) {
draghover = false;
os.deckGlobalEvents.emit('column.dragEnd');
const id = e.dataTransfer.getData(_DATA_TRANSFER_DECK_COLUMN_);
if (id != null && id != '') {
const id = ev.dataTransfer.getData(_DATA_TRANSFER_DECK_COLUMN_);
if (id != null && id !== '') {
swapColumn(props.column.id, id);
}
}

View file

@ -72,8 +72,8 @@ export const loadDeck = async () => {
scope: ['client', 'deck', 'profiles'],
key: deckStore.state.profile,
});
} catch (e) {
if (e.code === 'NO_SUCH_KEY') {
} catch (err) {
if (err.code === 'NO_SUCH_KEY') {
// 後方互換性のため
if (deckStore.state.profile === 'default') {
saveDeck();
@ -94,7 +94,7 @@ export const loadDeck = async () => {
deckStore.set('layout', [['a'], ['b']]);
return;
}
throw e;
throw err;
}
deckStore.set('columns', deck.columns);
@ -114,7 +114,7 @@ export const saveDeck = throttle(1000, () => {
});
export function addColumn(column: Column) {
if (column.name == undefined) column.name = null;
if (column.name === undefined) column.name = null;
deckStore.push('columns', column);
deckStore.push('layout', [column.id]);
saveDeck();
@ -129,10 +129,10 @@ export function removeColumn(id: Column['id']) {
}
export function swapColumn(a: Column['id'], b: Column['id']) {
const aX = deckStore.state.layout.findIndex(ids => ids.indexOf(a) != -1);
const aY = deckStore.state.layout[aX].findIndex(id => id == a);
const bX = deckStore.state.layout.findIndex(ids => ids.indexOf(b) != -1);
const bY = deckStore.state.layout[bX].findIndex(id => id == b);
const aX = deckStore.state.layout.findIndex(ids => ids.indexOf(a) !== -1);
const aY = deckStore.state.layout[aX].findIndex(id => id === a);
const bX = deckStore.state.layout.findIndex(ids => ids.indexOf(b) !== -1);
const bY = deckStore.state.layout[bX].findIndex(id => id === b);
const layout = copy(deckStore.state.layout);
layout[aX][aY] = b;
layout[bX][bY] = a;
@ -259,7 +259,7 @@ export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
const column = copy(deckStore.state.columns[columnIndex]);
if (column == null) return;
column.widgets = column.widgets.filter(w => w.id != widget.id);
column.widgets = column.widgets.filter(w => w.id !== widget.id);
columns[columnIndex] = column;
deckStore.set('columns', columns);
saveDeck();

View file

@ -18,7 +18,7 @@ defineProps<{
}>();
const emit = defineEmits<{
(e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
}>();
const pagination = {

View file

@ -22,8 +22,8 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
(e: 'loaded'): void;
(e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
(ev: 'loaded'): void;
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
}>();
let timeline = $ref<InstanceType<typeof XTimeline>>();

View file

@ -35,7 +35,7 @@ defineProps<{
}>();
const emit = defineEmits<{
(e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
}>();
let pageInfo = $ref<Record<string, any> | null>(null);

View file

@ -18,7 +18,7 @@ defineProps<{
}>();
const emit = defineEmits<{
(e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
}>();
const pagination = {

View file

@ -20,7 +20,7 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
(e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
}>();
function func() {

View file

@ -35,8 +35,8 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
(e: 'loaded'): void;
(e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
(ev: 'loaded'): void;
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
}>();
let disabled = $ref(false);

View file

@ -3,7 +3,7 @@
<template #header><i class="fas fa-window-maximize" style="margin-right: 8px;"></i>{{ column.name }}</template>
<div class="wtdtxvec">
<XWidgets v-if="column.widgets" :edit="edit" :widgets="column.widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/>
<XWidgets :edit="edit" :widgets="column.widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/>
</div>
</XColumn>
</template>
@ -20,7 +20,7 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
(e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
}>();
let edit = $ref(false);

View file

@ -751,17 +751,10 @@ astral-regex@^2.0.0:
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
async@^2.6.0:
version "2.6.3"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
dependencies:
lodash "^4.17.14"
async@^3.2.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.1.tgz#d3274ec66d107a47476a4c49136aacdb00665fc8"
integrity sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg==
version "3.2.3"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9"
integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==
asynckit@^0.4.0:
version "0.4.0"
@ -2519,13 +2512,6 @@ is-negative-zero@^2.0.1:
resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24"
integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==
is-number-like@^1.0.3:
version "1.0.8"
resolved "https://registry.yarnpkg.com/is-number-like/-/is-number-like-1.0.8.tgz#2e129620b50891042e44e9bbbb30593e75cfbbe3"
integrity sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA==
dependencies:
lodash.isfinite "^3.3.2"
is-number-object@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.6.tgz#6a7aaf838c7f0686a50b4553f7e54a96494e89f0"
@ -2789,11 +2775,6 @@ locate-path@^6.0.0:
dependencies:
p-locate "^5.0.0"
lodash.isfinite@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz#fb89b65a9a80281833f0b7478b3a5104f898ebb3"
integrity sha1-+4m2WpqAKBgz8LdHizpRBPiY67M=
lodash.merge@^4.6.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
@ -2804,7 +2785,7 @@ lodash.once@^4.1.1:
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
lodash@^4.17.14, lodash@^4.17.19, lodash@^4.17.21:
lodash@^4.17.19, lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@ -3303,14 +3284,6 @@ pngjs@^5.0.0:
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb"
integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==
portscanner@2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/portscanner/-/portscanner-2.2.0.tgz#6059189b3efa0965c9d96a56b958eb9508411cf1"
integrity sha512-IFroCz/59Lqa2uBvzK3bKDbDDIEaAY8XJ1jFxcLWTqosrsc32//P4VuSB2vZXoHiHqOmx8B5L5hnKOxL/7FlPw==
dependencies:
async "^2.6.0"
is-number-like "^1.0.3"
postcss-selector-parser@^6.0.9:
version "6.0.9"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.9.tgz#ee71c3b9ff63d9cd130838876c13a2ec1a992b2f"

View file

@ -382,7 +382,7 @@ arr-union@^3.1.0:
array-each@^1.0.0, array-each@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/array-each/-/array-each-1.0.1.tgz#a794af0c05ab1752846ee753a1f211a05ba0c44f"
integrity sha1-p5SvDAWrF1KEbudTofIRoFugxE8=
integrity sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==
array-initial@^1.0.0:
version "1.1.0"
@ -468,9 +468,9 @@ async-settle@^1.0.0:
async-done "^1.2.2"
async@^3.2.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.1.tgz#d3274ec66d107a47476a4c49136aacdb00665fc8"
integrity sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg==
version "3.2.3"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9"
integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==
asynckit@^0.4.0:
version "0.4.0"
@ -1005,12 +1005,12 @@ copy-descriptor@^0.1.0:
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
copy-props@^2.0.1:
version "2.0.4"
resolved "https://registry.yarnpkg.com/copy-props/-/copy-props-2.0.4.tgz#93bb1cadfafd31da5bb8a9d4b41f471ec3a72dfe"
integrity sha512-7cjuUME+p+S3HZlbllgsn2CDwS+5eCCX16qBgNC4jgSTf49qR1VKy/Zhl400m0IQXl/bPGEVqncgUUMjrr4s8A==
version "2.0.5"
resolved "https://registry.yarnpkg.com/copy-props/-/copy-props-2.0.5.tgz#03cf9ae328d4ebb36f8f1d804448a6af9ee3f2d2"
integrity sha512-XBlx8HSqrT0ObQwmSzM7WE5k8FxTV75h1DX1Z3n6NhQ/UYYAvInWYmG06vFt7hQZArE2fuO62aihiWIVQwh1sw==
dependencies:
each-props "^1.3.0"
is-plain-object "^2.0.1"
each-props "^1.3.2"
is-plain-object "^5.0.0"
core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2"
@ -1261,7 +1261,7 @@ duplexify@^3.6.0:
readable-stream "^2.0.0"
stream-shift "^1.0.0"
each-props@^1.3.0:
each-props@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/each-props/-/each-props-1.3.2.tgz#ea45a414d16dd5cfa419b1a81720d5ca06892333"
integrity sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA==
@ -2001,9 +2001,9 @@ homedir-polyfill@^1.0.1:
parse-passwd "^1.0.0"
hosted-git-info@^2.1.4:
version "2.8.8"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
version "2.8.9"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
html-comment-regex@^1.1.0:
version "1.1.2"
@ -2276,6 +2276,11 @@ is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
dependencies:
isobject "^3.0.1"
is-plain-object@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
is-relative@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d"
@ -2969,9 +2974,9 @@ path-key@^3.0.0, path-key@^3.1.0:
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
path-parse@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
path-root-regex@^0.1.0:
version "0.1.2"