Merge branch 'yarn-3' of https://github.com/ThatOneCalculator/misskey into yarn-3
|
@ -57,6 +57,7 @@ db:
|
||||||
redis:
|
redis:
|
||||||
host: localhost
|
host: localhost
|
||||||
port: 6379
|
port: 6379
|
||||||
|
#family: 0 # 0=Both, 4=IPv4, 6=IPv6
|
||||||
#pass: example-pass
|
#pass: example-pass
|
||||||
#prefix: example-prefix
|
#prefix: example-prefix
|
||||||
#db: 1
|
#db: 1
|
||||||
|
|
12
CHANGELOG.md
|
@ -12,14 +12,24 @@ You should also include the user name that made the change.
|
||||||
## 12.x.x (unreleased)
|
## 12.x.x (unreleased)
|
||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
|
- Server: Allow GET method for some endpoints @syuilo
|
||||||
- Server: Add rate limit to i/notifications @tamaina
|
- Server: Add rate limit to i/notifications @tamaina
|
||||||
- Client: Improve files page of control panel @syuilo
|
- Client: Improve control panel @syuilo
|
||||||
|
- Client: Show warning in control panel when there is an unresolved abuse report @syuilo
|
||||||
|
- Make possible to delete an account by admin @syuilo
|
||||||
|
- Improve player detection in URL preview @mei23
|
||||||
|
- Add Badge Image to Push Notification #8012 @tamaina
|
||||||
|
- Client: Removing entries from a clip @futchitwo
|
||||||
|
- Server: Supports IPv6 on Redis transport. @mei23
|
||||||
|
IPv4/IPv6 is used by default. You can tune this behavior via `redis.family`.
|
||||||
- Migrate to Yarn Berry (v3.2.1) @ThatOneCalculator
|
- Migrate to Yarn Berry (v3.2.1) @ThatOneCalculator
|
||||||
- You may have to `yarn run clean-all` and `yarn set version berry` before running `yarn install` if you're still on yarn classic
|
- You may have to `yarn run clean-all` and `yarn set version berry` before running `yarn install` if you're still on yarn classic
|
||||||
|
|
||||||
### Bugfixes
|
### Bugfixes
|
||||||
- Server: Fix GenerateVideoThumbnail failed @mei23
|
- Server: Fix GenerateVideoThumbnail failed @mei23
|
||||||
- Server: Ensure temp directory cleanup @Johann150
|
- Server: Ensure temp directory cleanup @Johann150
|
||||||
|
- favicons of federated instances not showing @syuilo
|
||||||
|
- Admin: The checkbox for blocking an instance works again @Johann150
|
||||||
|
|
||||||
## 12.111.1 (2022/06/13)
|
## 12.111.1 (2022/06/13)
|
||||||
|
|
||||||
|
|
|
@ -643,6 +643,8 @@ clip: "クリップ"
|
||||||
createNew: "新規作成"
|
createNew: "新規作成"
|
||||||
optional: "任意"
|
optional: "任意"
|
||||||
createNewClip: "新しいクリップを作成"
|
createNewClip: "新しいクリップを作成"
|
||||||
|
unclip: "クリップ解除"
|
||||||
|
confirmToUnclipAlreadyClippedNote: "このノートはすでにクリップ「{name}」に含まれています。ノートをこのクリップから除外しますか?"
|
||||||
public: "パブリック"
|
public: "パブリック"
|
||||||
i18nInfo: "Misskeyは有志によって様々な言語に翻訳されています。{link}で翻訳に協力できます。"
|
i18nInfo: "Misskeyは有志によって様々な言語に翻訳されています。{link}で翻訳に協力できます。"
|
||||||
manageAccessTokens: "アクセストークンの管理"
|
manageAccessTokens: "アクセストークンの管理"
|
||||||
|
@ -845,6 +847,16 @@ failedToFetchAccountInformation: "アカウント情報の取得に失敗しま
|
||||||
rateLimitExceeded: "レート制限を超えました"
|
rateLimitExceeded: "レート制限を超えました"
|
||||||
cropImage: "画像のクロップ"
|
cropImage: "画像のクロップ"
|
||||||
cropImageAsk: "画像をクロップしますか?"
|
cropImageAsk: "画像をクロップしますか?"
|
||||||
|
file: "ファイル"
|
||||||
|
recentNHours: "直近{n}時間"
|
||||||
|
recentNDays: "直近{n}日"
|
||||||
|
noEmailServerWarning: "メールサーバーの設定がされていません。"
|
||||||
|
thereIsUnresolvedAbuseReportWarning: "未対応の通報があります。"
|
||||||
|
recommended: "推奨"
|
||||||
|
check: "チェック"
|
||||||
|
isSystemAccount: "システムにより自動で作成・管理されているアカウントです。"
|
||||||
|
typeToConfirm: "この操作を行うには {x} と入力してください"
|
||||||
|
deleteAccount: "アカウント削除"
|
||||||
|
|
||||||
_emailUnavailable:
|
_emailUnavailable:
|
||||||
used: "既に使用されています"
|
used: "既に使用されています"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "12.111.1",
|
"version": "12.112.0-beta.7",
|
||||||
"codename": "indigo",
|
"codename": "indigo",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -5,6 +5,6 @@
|
||||||
"loader=./test/loader.js"
|
"loader=./test/loader.js"
|
||||||
],
|
],
|
||||||
"slow": 1000,
|
"slow": 1000,
|
||||||
"timeout": 10000,
|
"timeout": 30000,
|
||||||
"exit": true
|
"exit": true
|
||||||
}
|
}
|
||||||
|
|
5
packages/backend/assets/notification-badges/LICENSE
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
Font Awesome Icons
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
Ⓒ Font Awesome
|
||||||
|
CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
|
BIN
packages/backend/assets/notification-badges/at.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
packages/backend/assets/notification-badges/check.png
Normal file
After Width: | Height: | Size: 577 B |
After Width: | Height: | Size: 1.4 KiB |
BIN
packages/backend/assets/notification-badges/clock.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
packages/backend/assets/notification-badges/comments.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
packages/backend/assets/notification-badges/id-card-alt.png
Normal file
After Width: | Height: | Size: 844 B |
BIN
packages/backend/assets/notification-badges/null.png
Normal file
After Width: | Height: | Size: 174 B |
BIN
packages/backend/assets/notification-badges/plus.png
Normal file
After Width: | Height: | Size: 507 B |
BIN
packages/backend/assets/notification-badges/poll-h.png
Normal file
After Width: | Height: | Size: 689 B |
BIN
packages/backend/assets/notification-badges/quote-right.png
Normal file
After Width: | Height: | Size: 772 B |
BIN
packages/backend/assets/notification-badges/reply.png
Normal file
After Width: | Height: | Size: 930 B |
BIN
packages/backend/assets/notification-badges/retweet.png
Normal file
After Width: | Height: | Size: 798 B |
BIN
packages/backend/assets/notification-badges/user-plus.png
Normal file
After Width: | Height: | Size: 991 B |
|
@ -53,6 +53,7 @@
|
||||||
"fluent-ffmpeg": "2.1.2",
|
"fluent-ffmpeg": "2.1.2",
|
||||||
"got": "12.1.0",
|
"got": "12.1.0",
|
||||||
"hpagent": "0.1.2",
|
"hpagent": "0.1.2",
|
||||||
|
"ioredis": "4.28.5",
|
||||||
"ip-cidr": "3.0.10",
|
"ip-cidr": "3.0.10",
|
||||||
"is-svg": "4.3.2",
|
"is-svg": "4.3.2",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
|
@ -60,7 +61,7 @@
|
||||||
"json5": "2.2.1",
|
"json5": "2.2.1",
|
||||||
"json5-loader": "4.0.1",
|
"json5-loader": "4.0.1",
|
||||||
"jsonld": "6.0.0",
|
"jsonld": "6.0.0",
|
||||||
"jsrsasign": "10.5.24",
|
"jsrsasign": "10.5.25",
|
||||||
"koa": "2.13.4",
|
"koa": "2.13.4",
|
||||||
"koa-bodyparser": "4.3.0",
|
"koa-bodyparser": "4.3.0",
|
||||||
"koa-favicon": "2.1.0",
|
"koa-favicon": "2.1.0",
|
||||||
|
@ -93,7 +94,6 @@
|
||||||
"random-seed": "0.3.0",
|
"random-seed": "0.3.0",
|
||||||
"ratelimiter": "3.4.1",
|
"ratelimiter": "3.4.1",
|
||||||
"re2": "1.17.4",
|
"re2": "1.17.4",
|
||||||
"redis": "3.1.2",
|
|
||||||
"redis-lock": "0.1.4",
|
"redis-lock": "0.1.4",
|
||||||
"reflect-metadata": "0.1.13",
|
"reflect-metadata": "0.1.13",
|
||||||
"rename": "1.0.4",
|
"rename": "1.0.4",
|
||||||
|
@ -107,7 +107,7 @@
|
||||||
"strict-event-emitter-types": "2.0.0",
|
"strict-event-emitter-types": "2.0.0",
|
||||||
"stringz": "2.1.0",
|
"stringz": "2.1.0",
|
||||||
"style-loader": "3.3.1",
|
"style-loader": "3.3.1",
|
||||||
"summaly": "2.5.1",
|
"summaly": "2.6.0",
|
||||||
"syslog-pro": "1.0.0",
|
"syslog-pro": "1.0.0",
|
||||||
"systeminformation": "5.11.16",
|
"systeminformation": "5.11.16",
|
||||||
"tinycolor2": "1.4.2",
|
"tinycolor2": "1.4.2",
|
||||||
|
|
|
@ -19,6 +19,7 @@ export type Source = {
|
||||||
redis: {
|
redis: {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
|
family?: number;
|
||||||
pass: string;
|
pass: string;
|
||||||
db?: number;
|
db?: number;
|
||||||
prefix?: string;
|
prefix?: string;
|
||||||
|
|
|
@ -192,12 +192,13 @@ export const db = new DataSource({
|
||||||
synchronize: process.env.NODE_ENV === 'test',
|
synchronize: process.env.NODE_ENV === 'test',
|
||||||
dropSchema: process.env.NODE_ENV === 'test',
|
dropSchema: process.env.NODE_ENV === 'test',
|
||||||
cache: !config.db.disableCache ? {
|
cache: !config.db.disableCache ? {
|
||||||
type: 'redis',
|
type: 'ioredis',
|
||||||
options: {
|
options: {
|
||||||
host: config.redis.host,
|
host: config.redis.host,
|
||||||
port: config.redis.port,
|
port: config.redis.port,
|
||||||
|
family: config.redis.family == null ? 0 : config.redis.family,
|
||||||
password: config.redis.pass,
|
password: config.redis.pass,
|
||||||
prefix: `${config.redis.prefix}:query:`,
|
keyPrefix: `${config.redis.prefix}:query:`,
|
||||||
db: config.redis.db || 0,
|
db: config.redis.db || 0,
|
||||||
},
|
},
|
||||||
} : false,
|
} : false,
|
||||||
|
@ -226,7 +227,7 @@ export async function initDb(force = false) {
|
||||||
|
|
||||||
export async function resetDb() {
|
export async function resetDb() {
|
||||||
const reset = async () => {
|
const reset = async () => {
|
||||||
await redisClient.FLUSHDB();
|
await redisClient.flushdb();
|
||||||
const tables = await db.query(`SELECT relname AS "table"
|
const tables = await db.query(`SELECT relname AS "table"
|
||||||
FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
|
FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
|
||||||
WHERE nspname NOT IN ('pg_catalog', 'information_schema')
|
WHERE nspname NOT IN ('pg_catalog', 'information_schema')
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
import * as redis from 'redis';
|
import Redis from 'ioredis';
|
||||||
import config from '@/config/index.js';
|
import config from '@/config/index.js';
|
||||||
|
|
||||||
export function createConnection() {
|
export function createConnection() {
|
||||||
return redis.createClient(
|
return new Redis({
|
||||||
config.redis.port,
|
port: config.redis.port,
|
||||||
config.redis.host,
|
host: config.redis.host,
|
||||||
{
|
family: config.redis.family == null ? 0 : config.redis.family,
|
||||||
password: config.redis.pass,
|
password: config.redis.pass,
|
||||||
prefix: config.redis.prefix,
|
keyPrefix: `${config.redis.prefix}:`,
|
||||||
db: config.redis.db || 0,
|
db: config.redis.db || 0,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const subsdcriber = createConnection();
|
export const subsdcriber = createConnection();
|
||||||
|
|
|
@ -16,11 +16,13 @@ export async function checkWordMute(note: NoteLike, me: UserLike | null | undefi
|
||||||
if (me && (note.userId === me.id)) return false;
|
if (me && (note.userId === me.id)) return false;
|
||||||
|
|
||||||
if (mutedWords.length > 0) {
|
if (mutedWords.length > 0) {
|
||||||
if (note.text == null) return false;
|
const text = ((note.cw ?? '') + '\n' + (note.text ?? '')).trim();
|
||||||
|
|
||||||
|
if (text === '') return false;
|
||||||
|
|
||||||
const matched = mutedWords.some(filter => {
|
const matched = mutedWords.some(filter => {
|
||||||
if (Array.isArray(filter)) {
|
if (Array.isArray(filter)) {
|
||||||
return filter.every(keyword => note.text!.includes(keyword));
|
return filter.every(keyword => text.includes(keyword));
|
||||||
} else {
|
} else {
|
||||||
// represents RegExp
|
// represents RegExp
|
||||||
const regexp = filter.match(/^\/(.+)\/(.*)$/);
|
const regexp = filter.match(/^\/(.+)\/(.*)$/);
|
||||||
|
@ -29,7 +31,7 @@ export async function checkWordMute(note: NoteLike, me: UserLike | null | undefi
|
||||||
if (!regexp) return false;
|
if (!regexp) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return new RE2(regexp[1], regexp[2]).test(note.text!);
|
return new RE2(regexp[1], regexp[2]).test(text);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// This should never happen due to input sanitisation.
|
// This should never happen due to input sanitisation.
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
export function isBlockerUserRelated(note: any, blockerUserIds: Set<string>): boolean {
|
|
||||||
if (blockerUserIds.has(note.userId)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (note.reply != null && blockerUserIds.has(note.reply.userId)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (note.renote != null && blockerUserIds.has(note.renote.userId)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
8
packages/backend/src/misc/is-mime-image.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
|
||||||
|
|
||||||
|
const dictionary = {
|
||||||
|
'safe-file': FILE_TYPE_BROWSERSAFE,
|
||||||
|
'sharp-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/svg+xml'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isMimeImage = (mime: string, type: keyof typeof dictionary): boolean => dictionary[type].includes(mime);
|
|
@ -1,15 +0,0 @@
|
||||||
export function isMutedUserRelated(note: any, mutedUserIds: Set<string>): boolean {
|
|
||||||
if (mutedUserIds.has(note.userId)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (note.reply != null && mutedUserIds.has(note.reply.userId)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (note.renote != null && mutedUserIds.has(note.renote.userId)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
15
packages/backend/src/misc/is-user-related.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
export function isUserRelated(note: any, userIds: Set<string>): boolean {
|
||||||
|
if (userIds.has(note.userId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note.reply != null && userIds.has(note.reply.userId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note.renote != null && userIds.has(note.renote.userId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
|
@ -1,11 +1,13 @@
|
||||||
import { db } from '@/db/postgre.js';
|
import { db } from '@/db/postgre.js';
|
||||||
import { Instance } from '@/models/entities/instance.js';
|
import { Instance } from '@/models/entities/instance.js';
|
||||||
import { Packed } from '@/misc/schema.js';
|
import { Packed } from '@/misc/schema.js';
|
||||||
|
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||||
|
|
||||||
export const InstanceRepository = db.getRepository(Instance).extend({
|
export const InstanceRepository = db.getRepository(Instance).extend({
|
||||||
async pack(
|
async pack(
|
||||||
instance: Instance,
|
instance: Instance,
|
||||||
): Promise<Packed<'FederationInstance'>> {
|
): Promise<Packed<'FederationInstance'>> {
|
||||||
|
const meta = await fetchMeta();
|
||||||
return {
|
return {
|
||||||
id: instance.id,
|
id: instance.id,
|
||||||
caughtAt: instance.caughtAt.toISOString(),
|
caughtAt: instance.caughtAt.toISOString(),
|
||||||
|
@ -18,6 +20,7 @@ export const InstanceRepository = db.getRepository(Instance).extend({
|
||||||
lastCommunicatedAt: instance.lastCommunicatedAt.toISOString(),
|
lastCommunicatedAt: instance.lastCommunicatedAt.toISOString(),
|
||||||
isNotResponding: instance.isNotResponding,
|
isNotResponding: instance.isNotResponding,
|
||||||
isSuspended: instance.isSuspended,
|
isSuspended: instance.isSuspended,
|
||||||
|
isBlocked: meta.blockedHosts.includes(instance.host),
|
||||||
softwareName: instance.softwareName,
|
softwareName: instance.softwareName,
|
||||||
softwareVersion: instance.softwareVersion,
|
softwareVersion: instance.softwareVersion,
|
||||||
openRegistrations: instance.openRegistrations,
|
openRegistrations: instance.openRegistrations,
|
||||||
|
@ -26,6 +29,8 @@ export const InstanceRepository = db.getRepository(Instance).extend({
|
||||||
maintainerName: instance.maintainerName,
|
maintainerName: instance.maintainerName,
|
||||||
maintainerEmail: instance.maintainerEmail,
|
maintainerEmail: instance.maintainerEmail,
|
||||||
iconUrl: instance.iconUrl,
|
iconUrl: instance.iconUrl,
|
||||||
|
faviconUrl: instance.faviconUrl,
|
||||||
|
themeColor: instance.themeColor,
|
||||||
infoUpdatedAt: instance.infoUpdatedAt ? instance.infoUpdatedAt.toISOString() : null,
|
infoUpdatedAt: instance.infoUpdatedAt ? instance.infoUpdatedAt.toISOString() : null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
@ -52,6 +52,10 @@ export const packedFederationInstanceSchema = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
isBlocked: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
softwareName: {
|
softwareName: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
|
@ -88,6 +92,15 @@ export const packedFederationInstanceSchema = {
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
format: 'url',
|
format: 'url',
|
||||||
},
|
},
|
||||||
|
faviconUrl: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
format: 'url',
|
||||||
|
},
|
||||||
|
themeColor: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
infoUpdatedAt: {
|
infoUpdatedAt: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
|
|
|
@ -6,6 +6,7 @@ export function initialize<T>(name: string, limitPerSec = -1) {
|
||||||
redis: {
|
redis: {
|
||||||
port: config.redis.port,
|
port: config.redis.port,
|
||||||
host: config.redis.host,
|
host: config.redis.host,
|
||||||
|
family: config.redis.family == null ? 0 : config.redis.family,
|
||||||
password: config.redis.pass,
|
password: config.redis.pass,
|
||||||
db: config.redis.db || 0,
|
db: config.redis.db || 0,
|
||||||
},
|
},
|
||||||
|
|
|
@ -201,7 +201,7 @@ export interface IApMention extends IObject {
|
||||||
href: string;
|
href: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isMention = (object: IObject): object is IApMention=>
|
export const isMention = (object: IObject): object is IApMention =>
|
||||||
getApType(object) === 'Mention' &&
|
getApType(object) === 'Mention' &&
|
||||||
typeof object.href === 'string';
|
typeof object.href === 'string';
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,11 @@ import call from './call.js';
|
||||||
import { ApiError } from './error.js';
|
import { ApiError } from './error.js';
|
||||||
|
|
||||||
export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise<void>((res) => {
|
export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise<void>((res) => {
|
||||||
const body = ctx.request.body;
|
const body = ctx.is('multipart/form-data')
|
||||||
|
? (ctx.request as any).body
|
||||||
|
: ctx.method === 'GET'
|
||||||
|
? ctx.query
|
||||||
|
: ctx.request.body;
|
||||||
|
|
||||||
const reply = (x?: any, y?: ApiError) => {
|
const reply = (x?: any, y?: ApiError) => {
|
||||||
if (x == null) {
|
if (x == null) {
|
||||||
|
@ -33,6 +37,9 @@ export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise<void>((res
|
||||||
authenticate(body['i']).then(([user, app]) => {
|
authenticate(body['i']).then(([user, app]) => {
|
||||||
// API invoking
|
// API invoking
|
||||||
call(endpoint.name, user, app, body, ctx).then((res: any) => {
|
call(endpoint.name, user, app, body, ctx).then((res: any) => {
|
||||||
|
if (ctx.method === 'GET' && endpoint.meta.cacheSec && !body['i'] && !user) {
|
||||||
|
ctx.set('Cache-Control', `public, max-age=${endpoint.meta.cacheSec}`);
|
||||||
|
}
|
||||||
reply(res);
|
reply(res);
|
||||||
}).catch((e: ApiError) => {
|
}).catch((e: ApiError) => {
|
||||||
reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e);
|
reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e);
|
||||||
|
|
|
@ -94,7 +94,7 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cast non JSON input
|
// Cast non JSON input
|
||||||
if (ep.meta.requireFile && ep.params.properties) {
|
if ((ep.meta.requireFile || ctx?.method === 'GET') && ep.params.properties) {
|
||||||
for (const k of Object.keys(ep.params.properties)) {
|
for (const k of Object.keys(ep.params.properties)) {
|
||||||
const param = ep.params.properties![k];
|
const param = ep.params.properties![k];
|
||||||
if (['boolean', 'number', 'integer'].includes(param.type ?? '') && typeof data[k] === 'string') {
|
if (['boolean', 'number', 'integer'].includes(param.type ?? '') && typeof data[k] === 'string') {
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
import { User } from '@/models/entities/user.js';
|
|
||||||
import { id } from '@/models/id.js';
|
|
||||||
import { UserProfiles } from '@/models/index.js';
|
|
||||||
import { SelectQueryBuilder, Brackets } from 'typeorm';
|
|
||||||
|
|
||||||
function createMutesQuery(id: string) {
|
|
||||||
return UserProfiles.createQueryBuilder('user_profile')
|
|
||||||
.select('user_profile.mutedInstances')
|
|
||||||
.where('user_profile.userId = :muterId', { muterId: id });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateMutedInstanceQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }) {
|
|
||||||
const mutingQuery = createMutesQuery(me.id);
|
|
||||||
|
|
||||||
q
|
|
||||||
.andWhere(new Brackets(qb => { qb
|
|
||||||
.andWhere('note.userHost IS NULL')
|
|
||||||
.orWhere(`NOT((${ mutingQuery.getQuery() })::jsonb ? note.userHost)`);
|
|
||||||
}))
|
|
||||||
.andWhere(new Brackets(qb => { qb
|
|
||||||
.where(`note.replyUserHost IS NULL`)
|
|
||||||
.orWhere(`NOT ((${ mutingQuery.getQuery() })::jsonb ? note.replyUserHost)`);
|
|
||||||
}))
|
|
||||||
.andWhere(new Brackets(qb => { qb
|
|
||||||
.where(`note.renoteUserHost IS NULL`)
|
|
||||||
.orWhere(`NOT ((${ mutingQuery.getQuery() })::jsonb ? note.renoteUserHost)`);
|
|
||||||
}));
|
|
||||||
q.setParameters(mutingQuery.getParameters());
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateMutedInstanceNotificationQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }) {
|
|
||||||
const mutingQuery = createMutesQuery(me.id);
|
|
||||||
|
|
||||||
q.andWhere(new Brackets(qb => { qb
|
|
||||||
.andWhere('notifier.host IS NULL')
|
|
||||||
.orWhere(`NOT (( ${mutingQuery.getQuery()} )::jsonb ? notifier.host)`);
|
|
||||||
}));
|
|
||||||
|
|
||||||
q.setParameters(mutingQuery.getParameters());
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { User } from '@/models/entities/user.js';
|
|
||||||
import { Mutings } from '@/models/index.js';
|
|
||||||
import { SelectQueryBuilder, Brackets } from 'typeorm';
|
import { SelectQueryBuilder, Brackets } from 'typeorm';
|
||||||
|
import { User } from '@/models/entities/user.js';
|
||||||
|
import { Mutings, UserProfiles } from '@/models/index.js';
|
||||||
|
|
||||||
export function generateMutedUserQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }, exclude?: User) {
|
export function generateMutedUserQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }, exclude?: User) {
|
||||||
const mutingQuery = Mutings.createQueryBuilder('muting')
|
const mutingQuery = Mutings.createQueryBuilder('muting')
|
||||||
|
@ -11,21 +11,39 @@ export function generateMutedUserQuery(q: SelectQueryBuilder<any>, me: { id: Use
|
||||||
mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: exclude.id });
|
mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: exclude.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mutingInstanceQuery = UserProfiles.createQueryBuilder('user_profile')
|
||||||
|
.select('user_profile.mutedInstances')
|
||||||
|
.where('user_profile.userId = :muterId', { muterId: me.id });
|
||||||
|
|
||||||
// 投稿の作者をミュートしていない かつ
|
// 投稿の作者をミュートしていない かつ
|
||||||
// 投稿の返信先の作者をミュートしていない かつ
|
// 投稿の返信先の作者をミュートしていない かつ
|
||||||
// 投稿の引用元の作者をミュートしていない
|
// 投稿の引用元の作者をミュートしていない
|
||||||
q
|
q
|
||||||
.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`)
|
.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`)
|
||||||
.andWhere(new Brackets(qb => { qb
|
.andWhere(new Brackets(qb => { qb
|
||||||
.where(`note.replyUserId IS NULL`)
|
.where('note.replyUserId IS NULL')
|
||||||
.orWhere(`note.replyUserId NOT IN (${ mutingQuery.getQuery() })`);
|
.orWhere(`note.replyUserId NOT IN (${ mutingQuery.getQuery() })`);
|
||||||
}))
|
}))
|
||||||
.andWhere(new Brackets(qb => { qb
|
.andWhere(new Brackets(qb => { qb
|
||||||
.where(`note.renoteUserId IS NULL`)
|
.where('note.renoteUserId IS NULL')
|
||||||
.orWhere(`note.renoteUserId NOT IN (${ mutingQuery.getQuery() })`);
|
.orWhere(`note.renoteUserId NOT IN (${ mutingQuery.getQuery() })`);
|
||||||
|
}))
|
||||||
|
// mute instances
|
||||||
|
.andWhere(new Brackets(qb => { qb
|
||||||
|
.andWhere('note.userHost IS NULL')
|
||||||
|
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.userHost)`);
|
||||||
|
}))
|
||||||
|
.andWhere(new Brackets(qb => { qb
|
||||||
|
.where('note.replyUserHost IS NULL')
|
||||||
|
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.replyUserHost)`);
|
||||||
|
}))
|
||||||
|
.andWhere(new Brackets(qb => { qb
|
||||||
|
.where('note.renoteUserHost IS NULL')
|
||||||
|
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
q.setParameters(mutingQuery.getParameters());
|
q.setParameters(mutingQuery.getParameters());
|
||||||
|
q.setParameters(mutingInstanceQuery.getParameters());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateMutedUserQueryForUsers(q: SelectQueryBuilder<any>, me: { id: User['id'] }) {
|
export function generateMutedUserQueryForUsers(q: SelectQueryBuilder<any>, me: { id: User['id'] }) {
|
||||||
|
@ -33,8 +51,26 @@ export function generateMutedUserQueryForUsers(q: SelectQueryBuilder<any>, me: {
|
||||||
.select('muting.muteeId')
|
.select('muting.muteeId')
|
||||||
.where('muting.muterId = :muterId', { muterId: me.id });
|
.where('muting.muterId = :muterId', { muterId: me.id });
|
||||||
|
|
||||||
|
const mutingInstanceQuery = UserProfiles.createQueryBuilder('user_profile')
|
||||||
|
.select('user_profile.mutedInstances')
|
||||||
|
.where('user_profile.userId = :muterId', { muterId: me.id });
|
||||||
|
|
||||||
q
|
q
|
||||||
.andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`);
|
.andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`)
|
||||||
|
// mute instances
|
||||||
|
.andWhere(new Brackets(qb => { qb
|
||||||
|
.andWhere('note.userHost IS NULL')
|
||||||
|
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.userHost)`);
|
||||||
|
}))
|
||||||
|
.andWhere(new Brackets(qb => { qb
|
||||||
|
.where('note.replyUserHost IS NULL')
|
||||||
|
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.replyUserHost)`);
|
||||||
|
}))
|
||||||
|
.andWhere(new Brackets(qb => { qb
|
||||||
|
.where('note.renoteUserHost IS NULL')
|
||||||
|
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`);
|
||||||
|
}));
|
||||||
|
|
||||||
q.setParameters(mutingQuery.getParameters());
|
q.setParameters(mutingQuery.getParameters());
|
||||||
|
q.setParameters(mutingInstanceQuery.getParameters());
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,6 @@ ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/);
|
||||||
|
|
||||||
export default function <T extends IEndpointMeta, Ps extends Schema>(meta: T, paramDef: Ps, cb: executor<T, Ps>)
|
export default function <T extends IEndpointMeta, Ps extends Schema>(meta: T, paramDef: Ps, cb: executor<T, Ps>)
|
||||||
: (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any) => Promise<any> {
|
: (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any) => Promise<any> {
|
||||||
|
|
||||||
const validate = ajv.compile(paramDef);
|
const validate = ajv.compile(paramDef);
|
||||||
|
|
||||||
return (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any) => {
|
return (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any) => {
|
||||||
|
|
|
@ -59,6 +59,7 @@ import * as ep___admin_unsilenceUser from './endpoints/admin/unsilence-user.js';
|
||||||
import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js';
|
import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js';
|
||||||
import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js';
|
import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js';
|
||||||
import * as ep___admin_vacuum from './endpoints/admin/vacuum.js';
|
import * as ep___admin_vacuum from './endpoints/admin/vacuum.js';
|
||||||
|
import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js';
|
||||||
import * as ep___announcements from './endpoints/announcements.js';
|
import * as ep___announcements from './endpoints/announcements.js';
|
||||||
import * as ep___antennas_create from './endpoints/antennas/create.js';
|
import * as ep___antennas_create from './endpoints/antennas/create.js';
|
||||||
import * as ep___antennas_delete from './endpoints/antennas/delete.js';
|
import * as ep___antennas_delete from './endpoints/antennas/delete.js';
|
||||||
|
@ -99,6 +100,7 @@ import * as ep___charts_user_notes from './endpoints/charts/user/notes.js';
|
||||||
import * as ep___charts_user_reactions from './endpoints/charts/user/reactions.js';
|
import * as ep___charts_user_reactions from './endpoints/charts/user/reactions.js';
|
||||||
import * as ep___charts_users from './endpoints/charts/users.js';
|
import * as ep___charts_users from './endpoints/charts/users.js';
|
||||||
import * as ep___clips_addNote from './endpoints/clips/add-note.js';
|
import * as ep___clips_addNote from './endpoints/clips/add-note.js';
|
||||||
|
import * as ep___clips_removeNote from './endpoints/clips/remove-note.js';
|
||||||
import * as ep___clips_create from './endpoints/clips/create.js';
|
import * as ep___clips_create from './endpoints/clips/create.js';
|
||||||
import * as ep___clips_delete from './endpoints/clips/delete.js';
|
import * as ep___clips_delete from './endpoints/clips/delete.js';
|
||||||
import * as ep___clips_list from './endpoints/clips/list.js';
|
import * as ep___clips_list from './endpoints/clips/list.js';
|
||||||
|
@ -133,6 +135,7 @@ import * as ep___federation_instances from './endpoints/federation/instances.js'
|
||||||
import * as ep___federation_showInstance from './endpoints/federation/show-instance.js';
|
import * as ep___federation_showInstance from './endpoints/federation/show-instance.js';
|
||||||
import * as ep___federation_updateRemoteUser from './endpoints/federation/update-remote-user.js';
|
import * as ep___federation_updateRemoteUser from './endpoints/federation/update-remote-user.js';
|
||||||
import * as ep___federation_users from './endpoints/federation/users.js';
|
import * as ep___federation_users from './endpoints/federation/users.js';
|
||||||
|
import * as ep___federation_stats from './endpoints/federation/stats.js';
|
||||||
import * as ep___following_create from './endpoints/following/create.js';
|
import * as ep___following_create from './endpoints/following/create.js';
|
||||||
import * as ep___following_delete from './endpoints/following/delete.js';
|
import * as ep___following_delete from './endpoints/following/delete.js';
|
||||||
import * as ep___following_invalidate from './endpoints/following/invalidate.js';
|
import * as ep___following_invalidate from './endpoints/following/invalidate.js';
|
||||||
|
@ -369,6 +372,7 @@ const eps = [
|
||||||
['admin/unsuspend-user', ep___admin_unsuspendUser],
|
['admin/unsuspend-user', ep___admin_unsuspendUser],
|
||||||
['admin/update-meta', ep___admin_updateMeta],
|
['admin/update-meta', ep___admin_updateMeta],
|
||||||
['admin/vacuum', ep___admin_vacuum],
|
['admin/vacuum', ep___admin_vacuum],
|
||||||
|
['admin/delete-account', ep___admin_deleteAccount],
|
||||||
['announcements', ep___announcements],
|
['announcements', ep___announcements],
|
||||||
['antennas/create', ep___antennas_create],
|
['antennas/create', ep___antennas_create],
|
||||||
['antennas/delete', ep___antennas_delete],
|
['antennas/delete', ep___antennas_delete],
|
||||||
|
@ -409,6 +413,7 @@ const eps = [
|
||||||
['charts/user/reactions', ep___charts_user_reactions],
|
['charts/user/reactions', ep___charts_user_reactions],
|
||||||
['charts/users', ep___charts_users],
|
['charts/users', ep___charts_users],
|
||||||
['clips/add-note', ep___clips_addNote],
|
['clips/add-note', ep___clips_addNote],
|
||||||
|
['clips/remove-note', ep___clips_removeNote],
|
||||||
['clips/create', ep___clips_create],
|
['clips/create', ep___clips_create],
|
||||||
['clips/delete', ep___clips_delete],
|
['clips/delete', ep___clips_delete],
|
||||||
['clips/list', ep___clips_list],
|
['clips/list', ep___clips_list],
|
||||||
|
@ -443,6 +448,7 @@ const eps = [
|
||||||
['federation/show-instance', ep___federation_showInstance],
|
['federation/show-instance', ep___federation_showInstance],
|
||||||
['federation/update-remote-user', ep___federation_updateRemoteUser],
|
['federation/update-remote-user', ep___federation_updateRemoteUser],
|
||||||
['federation/users', ep___federation_users],
|
['federation/users', ep___federation_users],
|
||||||
|
['federation/stats', ep___federation_stats],
|
||||||
['following/create', ep___following_create],
|
['following/create', ep___following_create],
|
||||||
['following/delete', ep___following_delete],
|
['following/delete', ep___following_delete],
|
||||||
['following/invalidate', ep___following_invalidate],
|
['following/invalidate', ep___following_invalidate],
|
||||||
|
@ -699,6 +705,16 @@ export interface IEndpointMeta {
|
||||||
readonly kind?: string;
|
readonly kind?: string;
|
||||||
|
|
||||||
readonly description?: string;
|
readonly description?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GETでのリクエストを許容するか否か
|
||||||
|
*/
|
||||||
|
readonly allowGet?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 正常応答をキャッシュ (Cache-Control: public) する秒数
|
||||||
|
*/
|
||||||
|
readonly cacheSec?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IEndpoint {
|
export interface IEndpoint {
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { Users } from '@/models/index.js';
|
||||||
|
import { deleteAccount } from '@/services/delete-account.js';
|
||||||
|
import define from '../../define.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['admin'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
requireAdmin: true,
|
||||||
|
|
||||||
|
res: {
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
userId: { type: 'string', format: 'misskey:id' },
|
||||||
|
},
|
||||||
|
required: ['userId'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default define(meta, paramDef, async (ps) => {
|
||||||
|
const user = await Users.findOneByOrFail({ id: ps.userId });
|
||||||
|
if (user.isDeleted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteAccount(user);
|
||||||
|
});
|
|
@ -1,5 +1,5 @@
|
||||||
import define from '../../../define.js';
|
|
||||||
import { DriveFiles } from '@/models/index.js';
|
import { DriveFiles } from '@/models/index.js';
|
||||||
|
import define from '../../../define.js';
|
||||||
import { makePaginationQuery } from '../../../common/make-pagination-query.js';
|
import { makePaginationQuery } from '../../../common/make-pagination-query.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
|
@ -25,8 +25,9 @@ export const paramDef = {
|
||||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||||
sinceId: { type: 'string', format: 'misskey:id' },
|
sinceId: { type: 'string', format: 'misskey:id' },
|
||||||
untilId: { type: 'string', format: 'misskey:id' },
|
untilId: { type: 'string', format: 'misskey:id' },
|
||||||
|
userId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||||
type: { type: 'string', nullable: true, pattern: /^[a-zA-Z0-9\/\-*]+$/.toString().slice(1, -1) },
|
type: { type: 'string', nullable: true, pattern: /^[a-zA-Z0-9\/\-*]+$/.toString().slice(1, -1) },
|
||||||
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: "local" },
|
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' },
|
||||||
hostname: {
|
hostname: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
nullable: true,
|
nullable: true,
|
||||||
|
@ -41,14 +42,18 @@ export const paramDef = {
|
||||||
export default define(meta, paramDef, async (ps, me) => {
|
export default define(meta, paramDef, async (ps, me) => {
|
||||||
const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId);
|
const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId);
|
||||||
|
|
||||||
if (ps.origin === 'local') {
|
if (ps.userId) {
|
||||||
query.andWhere('file.userHost IS NULL');
|
query.andWhere('file.userId = :userId', { userId: ps.userId });
|
||||||
} else if (ps.origin === 'remote') {
|
} else {
|
||||||
query.andWhere('file.userHost IS NOT NULL');
|
if (ps.origin === 'local') {
|
||||||
}
|
query.andWhere('file.userHost IS NULL');
|
||||||
|
} else if (ps.origin === 'remote') {
|
||||||
|
query.andWhere('file.userHost IS NOT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
if (ps.hostname) {
|
if (ps.hostname) {
|
||||||
query.andWhere('file.userHost = :hostname', { hostname: ps.hostname });
|
query.andWhere('file.userHost = :hostname', { hostname: ps.hostname });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ps.type) {
|
if (ps.type) {
|
||||||
|
|
|
@ -99,12 +99,16 @@ export default define(meta, paramDef, async () => {
|
||||||
const fsStats = await si.fsSize();
|
const fsStats = await si.fsSize();
|
||||||
const netInterface = await si.networkInterfaceDefault();
|
const netInterface = await si.networkInterfaceDefault();
|
||||||
|
|
||||||
|
const redisServerInfo = await redisClient.info('Server');
|
||||||
|
const m = redisServerInfo.match(new RegExp('^redis_version:(.*)', 'm'));
|
||||||
|
const redis_version = m?.[1];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
machine: os.hostname(),
|
machine: os.hostname(),
|
||||||
os: os.platform(),
|
os: os.platform(),
|
||||||
node: process.version,
|
node: process.version,
|
||||||
psql: await db.query('SHOW server_version').then(x => x[0].server_version),
|
psql: await db.query('SHOW server_version').then(x => x[0].server_version),
|
||||||
redis: redisClient.server_info.redis_version,
|
redis: redis_version,
|
||||||
cpu: {
|
cpu: {
|
||||||
model: os.cpus()[0].model,
|
model: os.cpus()[0].model,
|
||||||
cores: os.cpus().length,
|
cores: os.cpus().length,
|
||||||
|
|
|
@ -2,12 +2,13 @@ import define from '../../define.js';
|
||||||
import config from '@/config/index.js';
|
import config from '@/config/index.js';
|
||||||
import { createPerson } from '@/remote/activitypub/models/person.js';
|
import { createPerson } from '@/remote/activitypub/models/person.js';
|
||||||
import { createNote } from '@/remote/activitypub/models/note.js';
|
import { createNote } from '@/remote/activitypub/models/note.js';
|
||||||
|
import DbResolver from '@/remote/activitypub/db-resolver.js';
|
||||||
import Resolver from '@/remote/activitypub/resolver.js';
|
import Resolver from '@/remote/activitypub/resolver.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
import { extractDbHost } from '@/misc/convert-host.js';
|
import { extractDbHost } from '@/misc/convert-host.js';
|
||||||
import { Users, Notes } from '@/models/index.js';
|
import { Users, Notes } from '@/models/index.js';
|
||||||
import { Note } from '@/models/entities/note.js';
|
import { Note } from '@/models/entities/note.js';
|
||||||
import { User } from '@/models/entities/user.js';
|
import { CacheableLocalUser, User } from '@/models/entities/user.js';
|
||||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||||
import { isActor, isPost, getApId } from '@/remote/activitypub/type.js';
|
import { isActor, isPost, getApId } from '@/remote/activitypub/type.js';
|
||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
|
@ -77,8 +78,8 @@ export const paramDef = {
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default define(meta, paramDef, async (ps) => {
|
export default define(meta, paramDef, async (ps, me) => {
|
||||||
const object = await fetchAny(ps.uri);
|
const object = await fetchAny(ps.uri, me);
|
||||||
if (object) {
|
if (object) {
|
||||||
return object;
|
return object;
|
||||||
} else {
|
} else {
|
||||||
|
@ -89,48 +90,18 @@ export default define(meta, paramDef, async (ps) => {
|
||||||
/***
|
/***
|
||||||
* URIからUserかNoteを解決する
|
* URIからUserかNoteを解決する
|
||||||
*/
|
*/
|
||||||
async function fetchAny(uri: string): Promise<SchemaType<typeof meta['res']> | null> {
|
async function fetchAny(uri: string, me: CacheableLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> {
|
||||||
// URIがこのサーバーを指しているなら、ローカルユーザーIDとしてDBからフェッチ
|
|
||||||
if (uri.startsWith(config.url + '/')) {
|
|
||||||
const parts = uri.split('/');
|
|
||||||
const id = parts.pop();
|
|
||||||
const type = parts.pop();
|
|
||||||
|
|
||||||
if (type === 'notes') {
|
|
||||||
const note = await Notes.findOneBy({ id });
|
|
||||||
|
|
||||||
if (note) {
|
|
||||||
return {
|
|
||||||
type: 'Note',
|
|
||||||
object: await Notes.pack(note, null, { detail: true }),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else if (type === 'users') {
|
|
||||||
const user = await Users.findOneBy({ id });
|
|
||||||
|
|
||||||
if (user) {
|
|
||||||
return {
|
|
||||||
type: 'User',
|
|
||||||
object: await Users.pack(user, null, { detail: true }),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ブロックしてたら中断
|
// ブロックしてたら中断
|
||||||
const fetchedMeta = await fetchMeta();
|
const fetchedMeta = await fetchMeta();
|
||||||
if (fetchedMeta.blockedHosts.includes(extractDbHost(uri))) return null;
|
if (fetchedMeta.blockedHosts.includes(extractDbHost(uri))) return null;
|
||||||
|
|
||||||
// URI(AP Object id)としてDB検索
|
const dbResolver = new DbResolver();
|
||||||
{
|
|
||||||
const [user, note] = await Promise.all([
|
|
||||||
Users.findOneBy({ uri: uri }),
|
|
||||||
Notes.findOneBy({ uri: uri }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const packed = await mergePack(user, note);
|
let local = await mergePack(me, ...await Promise.all([
|
||||||
if (packed !== null) return packed;
|
dbResolver.getUserFromApId(uri),
|
||||||
}
|
dbResolver.getNoteFromApId(uri),
|
||||||
|
]));
|
||||||
|
if (local != null) return local;
|
||||||
|
|
||||||
// リモートから一旦オブジェクトフェッチ
|
// リモートから一旦オブジェクトフェッチ
|
||||||
const resolver = new Resolver();
|
const resolver = new Resolver();
|
||||||
|
@ -139,74 +110,37 @@ async function fetchAny(uri: string): Promise<SchemaType<typeof meta['res']> | n
|
||||||
// /@user のような正規id以外で取得できるURIが指定されていた場合、ここで初めて正規URIが確定する
|
// /@user のような正規id以外で取得できるURIが指定されていた場合、ここで初めて正規URIが確定する
|
||||||
// これはDBに存在する可能性があるため再度DB検索
|
// これはDBに存在する可能性があるため再度DB検索
|
||||||
if (uri !== object.id) {
|
if (uri !== object.id) {
|
||||||
if (object.id.startsWith(config.url + '/')) {
|
local = await mergePack(me, ...await Promise.all([
|
||||||
const parts = object.id.split('/');
|
dbResolver.getUserFromApId(object.id),
|
||||||
const id = parts.pop();
|
dbResolver.getNoteFromApId(object.id),
|
||||||
const type = parts.pop();
|
]));
|
||||||
|
if (local != null) return local;
|
||||||
if (type === 'notes') {
|
|
||||||
const note = await Notes.findOneBy({ id });
|
|
||||||
|
|
||||||
if (note) {
|
|
||||||
return {
|
|
||||||
type: 'Note',
|
|
||||||
object: await Notes.pack(note, null, { detail: true }),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else if (type === 'users') {
|
|
||||||
const user = await Users.findOneBy({ id });
|
|
||||||
|
|
||||||
if (user) {
|
|
||||||
return {
|
|
||||||
type: 'User',
|
|
||||||
object: await Users.pack(user, null, { detail: true }),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [user, note] = await Promise.all([
|
|
||||||
Users.findOneBy({ uri: object.id }),
|
|
||||||
Notes.findOneBy({ uri: object.id }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const packed = await mergePack(user, note);
|
|
||||||
if (packed !== null) return packed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// それでもみつからなければ新規であるため登録
|
return await mergePack(
|
||||||
if (isActor(object)) {
|
me,
|
||||||
const user = await createPerson(getApId(object));
|
isActor(object) ? await createPerson(getApId(object)) : null,
|
||||||
return {
|
isPost(object) ? await createNote(getApId(object), undefined, true) : null,
|
||||||
type: 'User',
|
);
|
||||||
object: await Users.pack(user, null, { detail: true }),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPost(object)) {
|
|
||||||
const note = await createNote(getApId(object), undefined, true);
|
|
||||||
return {
|
|
||||||
type: 'Note',
|
|
||||||
object: await Notes.pack(note!, null, { detail: true }),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mergePack(user: User | null | undefined, note: Note | null | undefined): Promise<SchemaType<typeof meta.res> | null> {
|
async function mergePack(me: CacheableLocalUser | null | undefined, user: User | null | undefined, note: Note | null | undefined): Promise<SchemaType<typeof meta.res> | null> {
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
return {
|
return {
|
||||||
type: 'User',
|
type: 'User',
|
||||||
object: await Users.pack(user, null, { detail: true }),
|
object: await Users.pack(user, me, { detail: true }),
|
||||||
};
|
};
|
||||||
}
|
} else if (note != null) {
|
||||||
|
try {
|
||||||
|
const object = await Notes.pack(note, me, { detail: true });
|
||||||
|
|
||||||
if (note != null) {
|
return {
|
||||||
return {
|
type: 'Note',
|
||||||
type: 'Note',
|
object,
|
||||||
object: await Notes.pack(note, null, { detail: true }),
|
};
|
||||||
};
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import define from '../../define.js';
|
|
||||||
import { getJsonSchema } from '@/services/chart/core.js';
|
import { getJsonSchema } from '@/services/chart/core.js';
|
||||||
import { activeUsersChart } from '@/services/chart/index.js';
|
import { activeUsersChart } from '@/services/chart/index.js';
|
||||||
|
import define from '../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['charts', 'users'],
|
tags: ['charts', 'users'],
|
||||||
|
|
||||||
res: getJsonSchema(activeUsersChart.schema),
|
res: getJsonSchema(activeUsersChart.schema),
|
||||||
|
|
||||||
|
allowGet: true,
|
||||||
|
cacheSec: 60 * 60,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import define from '../../define.js';
|
|
||||||
import { getJsonSchema } from '@/services/chart/core.js';
|
import { getJsonSchema } from '@/services/chart/core.js';
|
||||||
import { apRequestChart } from '@/services/chart/index.js';
|
import { apRequestChart } from '@/services/chart/index.js';
|
||||||
|
import define from '../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['charts'],
|
tags: ['charts'],
|
||||||
|
|
||||||
res: getJsonSchema(apRequestChart.schema),
|
res: getJsonSchema(apRequestChart.schema),
|
||||||
|
|
||||||
|
allowGet: true,
|
||||||
|
cacheSec: 60 * 60,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import define from '../../define.js';
|
|
||||||
import { getJsonSchema } from '@/services/chart/core.js';
|
import { getJsonSchema } from '@/services/chart/core.js';
|
||||||
import { driveChart } from '@/services/chart/index.js';
|
import { driveChart } from '@/services/chart/index.js';
|
||||||
|
import define from '../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['charts', 'drive'],
|
tags: ['charts', 'drive'],
|
||||||
|
|
||||||
res: getJsonSchema(driveChart.schema),
|
res: getJsonSchema(driveChart.schema),
|
||||||
|
|
||||||
|
allowGet: true,
|
||||||
|
cacheSec: 60 * 60,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import define from '../../define.js';
|
|
||||||
import { getJsonSchema } from '@/services/chart/core.js';
|
import { getJsonSchema } from '@/services/chart/core.js';
|
||||||
import { federationChart } from '@/services/chart/index.js';
|
import { federationChart } from '@/services/chart/index.js';
|
||||||
|
import define from '../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['charts'],
|
tags: ['charts'],
|
||||||
|
|
||||||
res: getJsonSchema(federationChart.schema),
|
res: getJsonSchema(federationChart.schema),
|
||||||
|
|
||||||
|
allowGet: true,
|
||||||
|
cacheSec: 60 * 60,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import define from '../../define.js';
|
|
||||||
import { getJsonSchema } from '@/services/chart/core.js';
|
import { getJsonSchema } from '@/services/chart/core.js';
|
||||||
import { hashtagChart } from '@/services/chart/index.js';
|
import { hashtagChart } from '@/services/chart/index.js';
|
||||||
|
import define from '../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['charts', 'hashtags'],
|
tags: ['charts', 'hashtags'],
|
||||||
|
|
||||||
res: getJsonSchema(hashtagChart.schema),
|
res: getJsonSchema(hashtagChart.schema),
|
||||||
|
|
||||||
|
allowGet: true,
|
||||||
|
cacheSec: 60 * 60,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import define from '../../define.js';
|
|
||||||
import { getJsonSchema } from '@/services/chart/core.js';
|
import { getJsonSchema } from '@/services/chart/core.js';
|
||||||
import { instanceChart } from '@/services/chart/index.js';
|
import { instanceChart } from '@/services/chart/index.js';
|
||||||
|
import define from '../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['charts'],
|
tags: ['charts'],
|
||||||
|
|
||||||
res: getJsonSchema(instanceChart.schema),
|
res: getJsonSchema(instanceChart.schema),
|
||||||
|
|
||||||
|
allowGet: true,
|
||||||
|
cacheSec: 60 * 60,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import define from '../../define.js';
|
|
||||||
import { getJsonSchema } from '@/services/chart/core.js';
|
import { getJsonSchema } from '@/services/chart/core.js';
|
||||||
import { notesChart } from '@/services/chart/index.js';
|
import { notesChart } from '@/services/chart/index.js';
|
||||||
|
import define from '../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['charts', 'notes'],
|
tags: ['charts', 'notes'],
|
||||||
|
|
||||||
res: getJsonSchema(notesChart.schema),
|
res: getJsonSchema(notesChart.schema),
|
||||||
|
|
||||||
|
allowGet: true,
|
||||||
|
cacheSec: 60 * 60,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import define from '../../../define.js';
|
|
||||||
import { getJsonSchema } from '@/services/chart/core.js';
|
import { getJsonSchema } from '@/services/chart/core.js';
|
||||||
import { perUserDriveChart } from '@/services/chart/index.js';
|
import { perUserDriveChart } from '@/services/chart/index.js';
|
||||||
|
import define from '../../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['charts', 'drive', 'users'],
|
tags: ['charts', 'drive', 'users'],
|
||||||
|
|
||||||
res: getJsonSchema(perUserDriveChart.schema),
|
res: getJsonSchema(perUserDriveChart.schema),
|
||||||
|
|
||||||
|
allowGet: true,
|
||||||
|
cacheSec: 60 * 60,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
@ -6,6 +6,9 @@ export const meta = {
|
||||||
tags: ['charts', 'users', 'following'],
|
tags: ['charts', 'users', 'following'],
|
||||||
|
|
||||||
res: getJsonSchema(perUserFollowingChart.schema),
|
res: getJsonSchema(perUserFollowingChart.schema),
|
||||||
|
|
||||||
|
allowGet: true,
|
||||||
|
cacheSec: 60 * 60,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import define from '../../../define.js';
|
|
||||||
import { getJsonSchema } from '@/services/chart/core.js';
|
import { getJsonSchema } from '@/services/chart/core.js';
|
||||||
import { perUserNotesChart } from '@/services/chart/index.js';
|
import { perUserNotesChart } from '@/services/chart/index.js';
|
||||||
|
import define from '../../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['charts', 'users', 'notes'],
|
tags: ['charts', 'users', 'notes'],
|
||||||
|
|
||||||
res: getJsonSchema(perUserNotesChart.schema),
|
res: getJsonSchema(perUserNotesChart.schema),
|
||||||
|
|
||||||
|
allowGet: true,
|
||||||
|
cacheSec: 60 * 60,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import define from '../../../define.js';
|
|
||||||
import { getJsonSchema } from '@/services/chart/core.js';
|
import { getJsonSchema } from '@/services/chart/core.js';
|
||||||
import { perUserReactionsChart } from '@/services/chart/index.js';
|
import { perUserReactionsChart } from '@/services/chart/index.js';
|
||||||
|
import define from '../../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['charts', 'users', 'reactions'],
|
tags: ['charts', 'users', 'reactions'],
|
||||||
|
|
||||||
res: getJsonSchema(perUserReactionsChart.schema),
|
res: getJsonSchema(perUserReactionsChart.schema),
|
||||||
|
|
||||||
|
allowGet: true,
|
||||||
|
cacheSec: 60 * 60,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import define from '../../define.js';
|
|
||||||
import { getJsonSchema } from '@/services/chart/core.js';
|
import { getJsonSchema } from '@/services/chart/core.js';
|
||||||
import { usersChart } from '@/services/chart/index.js';
|
import { usersChart } from '@/services/chart/index.js';
|
||||||
|
import define from '../../define.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['charts', 'users'],
|
tags: ['charts', 'users'],
|
||||||
|
|
||||||
res: getJsonSchema(usersChart.schema),
|
res: getJsonSchema(usersChart.schema),
|
||||||
|
|
||||||
|
allowGet: true,
|
||||||
|
cacheSec: 60 * 60,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
import define from '../../define.js';
|
||||||
|
import { ClipNotes, Clips } from '@/models/index.js';
|
||||||
|
import { ApiError } from '../../error.js';
|
||||||
|
import { getNote } from '../../common/getters.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['account', 'notes', 'clips'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
kind: 'write:account',
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchClip: {
|
||||||
|
message: 'No such clip.',
|
||||||
|
code: 'NO_SUCH_CLIP',
|
||||||
|
id: 'b80525c6-97f7-49d7-a42d-ebccd49cfd52',
|
||||||
|
},
|
||||||
|
|
||||||
|
noSuchNote: {
|
||||||
|
message: 'No such note.',
|
||||||
|
code: 'NO_SUCH_NOTE',
|
||||||
|
id: 'aff017de-190e-434b-893e-33a9ff5049d8',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
clipId: { type: 'string', format: 'misskey:id' },
|
||||||
|
noteId: { type: 'string', format: 'misskey:id' },
|
||||||
|
},
|
||||||
|
required: ['clipId', 'noteId'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default define(meta, paramDef, async (ps, user) => {
|
||||||
|
const clip = await Clips.findOneBy({
|
||||||
|
id: ps.clipId,
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (clip == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchClip);
|
||||||
|
}
|
||||||
|
|
||||||
|
const note = await getNote(ps.noteId).catch(e => {
|
||||||
|
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
|
||||||
|
await ClipNotes.delete({
|
||||||
|
noteId: note.id,
|
||||||
|
clipId: clip.id,
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { IsNull, MoreThan, Not } from 'typeorm';
|
||||||
|
import { Followings, Instances } from '@/models/index.js';
|
||||||
|
import { awaitAll } from '@/prelude/await-all.js';
|
||||||
|
import define from '../../define.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['federation'],
|
||||||
|
|
||||||
|
requireCredential: false,
|
||||||
|
|
||||||
|
allowGet: true,
|
||||||
|
cacheSec: 60 * 60,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
},
|
||||||
|
required: [],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default define(meta, paramDef, async (ps) => {
|
||||||
|
const [topSubInstances, topPubInstances, allSubCount, allPubCount] = await Promise.all([
|
||||||
|
Instances.find({
|
||||||
|
where: {
|
||||||
|
followersCount: MoreThan(0),
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
followersCount: 'DESC',
|
||||||
|
},
|
||||||
|
take: 10,
|
||||||
|
}),
|
||||||
|
Instances.find({
|
||||||
|
where: {
|
||||||
|
followingCount: MoreThan(0),
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
followingCount: 'DESC',
|
||||||
|
},
|
||||||
|
take: 10,
|
||||||
|
}),
|
||||||
|
Followings.count({
|
||||||
|
where: {
|
||||||
|
followeeHost: Not(IsNull()),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
Followings.count({
|
||||||
|
where: {
|
||||||
|
followerHost: Not(IsNull()),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const gotSubCount = topSubInstances.map(x => x.followersCount).reduce((a, b) => a + b, 0);
|
||||||
|
const gotPubCount = topSubInstances.map(x => x.followingCount).reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
|
return await awaitAll({
|
||||||
|
topSubInstances: Instances.packMany(topSubInstances),
|
||||||
|
otherFollowersCount: Math.max(0, allSubCount - gotSubCount),
|
||||||
|
topPubInstances: Instances.packMany(topPubInstances),
|
||||||
|
otherFollowingCount: Math.max(0, allPubCount - gotPubCount),
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,9 +1,7 @@
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import define from '../../define.js';
|
|
||||||
import { UserProfiles, Users } from '@/models/index.js';
|
import { UserProfiles, Users } from '@/models/index.js';
|
||||||
import { doPostSuspend } from '@/services/suspend-user.js';
|
import { deleteAccount } from '@/services/delete-account.js';
|
||||||
import { publishUserEvent } from '@/services/stream.js';
|
import define from '../../define.js';
|
||||||
import { createDeleteAccountJob } from '@/queue/index.js';
|
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
|
@ -34,17 +32,5 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
throw new Error('incorrect password');
|
throw new Error('incorrect password');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 物理削除する前にDelete activityを送信する
|
await deleteAccount(user);
|
||||||
await doPostSuspend(user).catch(e => {});
|
|
||||||
|
|
||||||
createDeleteAccountJob(user, {
|
|
||||||
soft: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
await Users.update(user.id, {
|
|
||||||
isDeleted: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Terminate streaming
|
|
||||||
publishUserEvent(user.id, 'terminate', {});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { Brackets } from 'typeorm';
|
import { Brackets } from 'typeorm';
|
||||||
import { Notifications, Followings, Mutings, Users } from '@/models/index.js';
|
import { Notifications, Followings, Mutings, Users, UserProfiles } from '@/models/index.js';
|
||||||
import { notificationTypes } from '@/types.js';
|
import { notificationTypes } from '@/types.js';
|
||||||
import read from '@/services/note/read.js';
|
import read from '@/services/note/read.js';
|
||||||
import { readNotification } from '../../common/read-notification.js';
|
import { readNotification } from '../../common/read-notification.js';
|
||||||
import define from '../../define.js';
|
import define from '../../define.js';
|
||||||
import { makePaginationQuery } from '../../common/make-pagination-query.js';
|
import { makePaginationQuery } from '../../common/make-pagination-query.js';
|
||||||
import { generateMutedInstanceNotificationQuery } from '../../common/generate-muted-instance-query.js';
|
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['account', 'notifications'],
|
tags: ['account', 'notifications'],
|
||||||
|
@ -67,6 +66,10 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
.select('muting.muteeId')
|
.select('muting.muteeId')
|
||||||
.where('muting.muterId = :muterId', { muterId: user.id });
|
.where('muting.muterId = :muterId', { muterId: user.id });
|
||||||
|
|
||||||
|
const mutingInstanceQuery = UserProfiles.createQueryBuilder('user_profile')
|
||||||
|
.select('user_profile.mutedInstances')
|
||||||
|
.where('user_profile.userId = :muterId', { muterId: user.id });
|
||||||
|
|
||||||
const suspendedQuery = Users.createQueryBuilder('users')
|
const suspendedQuery = Users.createQueryBuilder('users')
|
||||||
.select('users.id')
|
.select('users.id')
|
||||||
.where('users.isSuspended = TRUE');
|
.where('users.isSuspended = TRUE');
|
||||||
|
@ -89,14 +92,21 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
||||||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
|
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
|
||||||
|
|
||||||
|
// muted users
|
||||||
query.andWhere(new Brackets(qb => { qb
|
query.andWhere(new Brackets(qb => { qb
|
||||||
.where(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`)
|
.where(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`)
|
||||||
.orWhere('notification.notifierId IS NULL');
|
.orWhere('notification.notifierId IS NULL');
|
||||||
}));
|
}));
|
||||||
query.setParameters(mutingQuery.getParameters());
|
query.setParameters(mutingQuery.getParameters());
|
||||||
|
|
||||||
generateMutedInstanceNotificationQuery(query, user);
|
// muted instances
|
||||||
|
query.andWhere(new Brackets(qb => { qb
|
||||||
|
.andWhere('notifier.host IS NULL')
|
||||||
|
.orWhere(`NOT (( ${mutingInstanceQuery.getQuery()} )::jsonb ? notifier.host)`);
|
||||||
|
}));
|
||||||
|
query.setParameters(mutingInstanceQuery.getParameters());
|
||||||
|
|
||||||
|
// suspended users
|
||||||
query.andWhere(new Brackets(qb => { qb
|
query.andWhere(new Brackets(qb => { qb
|
||||||
.where(`notification.notifierId NOT IN (${ suspendedQuery.getQuery() })`)
|
.where(`notification.notifierId NOT IN (${ suspendedQuery.getQuery() })`)
|
||||||
.orWhere('notification.notifierId IS NULL');
|
.orWhere('notification.notifierId IS NULL');
|
||||||
|
|
|
@ -5,7 +5,6 @@ import { makePaginationQuery } from '../../common/make-pagination-query.js';
|
||||||
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
|
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
|
||||||
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
|
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
|
||||||
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
|
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
|
||||||
import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query.js';
|
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['notes'],
|
tags: ['notes'],
|
||||||
|
@ -61,9 +60,10 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
|
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
|
||||||
|
|
||||||
generateVisibilityQuery(query, user);
|
generateVisibilityQuery(query, user);
|
||||||
if (user) generateMutedUserQuery(query, user);
|
if (user) {
|
||||||
if (user) generateBlockedUserQuery(query, user);
|
generateMutedUserQuery(query, user);
|
||||||
if (user) generateMutedInstanceQuery(query, user);
|
generateBlockedUserQuery(query, user);
|
||||||
|
}
|
||||||
|
|
||||||
const notes = await query.take(ps.limit).getMany();
|
const notes = await query.take(ps.limit).getMany();
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||||
import { Notes, Users } from '@/models/index.js';
|
import { Notes } from '@/models/index.js';
|
||||||
import { activeUsersChart } from '@/services/chart/index.js';
|
import { activeUsersChart } from '@/services/chart/index.js';
|
||||||
import define from '../../define.js';
|
import define from '../../define.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
import { makePaginationQuery } from '../../common/make-pagination-query.js';
|
import { makePaginationQuery } from '../../common/make-pagination-query.js';
|
||||||
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
|
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
|
||||||
import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query.js';
|
|
||||||
import { generateRepliesQuery } from '../../common/generate-replies-query.js';
|
import { generateRepliesQuery } from '../../common/generate-replies-query.js';
|
||||||
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
|
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
|
||||||
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
|
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
|
||||||
|
@ -76,10 +75,11 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
|
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
|
||||||
|
|
||||||
generateRepliesQuery(query, user);
|
generateRepliesQuery(query, user);
|
||||||
if (user) generateMutedUserQuery(query, user);
|
if (user) {
|
||||||
if (user) generateMutedNoteQuery(query, user);
|
generateMutedUserQuery(query, user);
|
||||||
if (user) generateBlockedUserQuery(query, user);
|
generateMutedNoteQuery(query, user);
|
||||||
if (user) generateMutedInstanceQuery(query, user);
|
generateBlockedUserQuery(query, user);
|
||||||
|
}
|
||||||
|
|
||||||
if (ps.withFiles) {
|
if (ps.withFiles) {
|
||||||
query.andWhere('note.fileIds != \'{}\'');
|
query.andWhere('note.fileIds != \'{}\'');
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import { Brackets } from 'typeorm';
|
import { Brackets } from 'typeorm';
|
||||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||||
import { Followings, Notes, Users } from '@/models/index.js';
|
import { Followings, Notes } from '@/models/index.js';
|
||||||
import { activeUsersChart } from '@/services/chart/index.js';
|
import { activeUsersChart } from '@/services/chart/index.js';
|
||||||
import define from '../../define.js';
|
import define from '../../define.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
import { makePaginationQuery } from '../../common/make-pagination-query.js';
|
import { makePaginationQuery } from '../../common/make-pagination-query.js';
|
||||||
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
|
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
|
||||||
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
|
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
|
||||||
import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query.js';
|
|
||||||
import { generateRepliesQuery } from '../../common/generate-replies-query.js';
|
import { generateRepliesQuery } from '../../common/generate-replies-query.js';
|
||||||
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
|
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
|
||||||
import { generateChannelQuery } from '../../common/generate-channel-query.js';
|
import { generateChannelQuery } from '../../common/generate-channel-query.js';
|
||||||
|
@ -92,7 +91,6 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
generateRepliesQuery(query, user);
|
generateRepliesQuery(query, user);
|
||||||
generateVisibilityQuery(query, user);
|
generateVisibilityQuery(query, user);
|
||||||
generateMutedUserQuery(query, user);
|
generateMutedUserQuery(query, user);
|
||||||
generateMutedInstanceQuery(query, user);
|
|
||||||
generateMutedNoteQuery(query, user);
|
generateMutedNoteQuery(query, user);
|
||||||
generateBlockedUserQuery(query, user);
|
generateBlockedUserQuery(query, user);
|
||||||
|
|
||||||
|
@ -134,9 +132,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
const timeline = await query.take(ps.limit).getMany();
|
const timeline = await query.take(ps.limit).getMany();
|
||||||
|
|
||||||
process.nextTick(() => {
|
process.nextTick(() => {
|
||||||
if (user) {
|
activeUsersChart.read(user);
|
||||||
activeUsersChart.read(user);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return await Notes.packMany(timeline, user);
|
return await Notes.packMany(timeline, user);
|
||||||
|
|
|
@ -5,7 +5,6 @@ import define from '../../define.js';
|
||||||
import { makePaginationQuery } from '../../common/make-pagination-query.js';
|
import { makePaginationQuery } from '../../common/make-pagination-query.js';
|
||||||
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
|
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
|
||||||
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
|
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
|
||||||
import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query.js';
|
|
||||||
import { generateRepliesQuery } from '../../common/generate-replies-query.js';
|
import { generateRepliesQuery } from '../../common/generate-replies-query.js';
|
||||||
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
|
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
|
||||||
import { generateChannelQuery } from '../../common/generate-channel-query.js';
|
import { generateChannelQuery } from '../../common/generate-channel-query.js';
|
||||||
|
@ -84,7 +83,6 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
generateRepliesQuery(query, user);
|
generateRepliesQuery(query, user);
|
||||||
generateVisibilityQuery(query, user);
|
generateVisibilityQuery(query, user);
|
||||||
generateMutedUserQuery(query, user);
|
generateMutedUserQuery(query, user);
|
||||||
generateMutedInstanceQuery(query, user);
|
|
||||||
generateMutedNoteQuery(query, user);
|
generateMutedNoteQuery(query, user);
|
||||||
generateBlockedUserQuery(query, user);
|
generateBlockedUserQuery(query, user);
|
||||||
|
|
||||||
|
@ -126,9 +124,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
const timeline = await query.take(ps.limit).getMany();
|
const timeline = await query.take(ps.limit).getMany();
|
||||||
|
|
||||||
process.nextTick(() => {
|
process.nextTick(() => {
|
||||||
if (user) {
|
activeUsersChart.read(user);
|
||||||
activeUsersChart.read(user);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return await Notes.packMany(timeline, user);
|
return await Notes.packMany(timeline, user);
|
||||||
|
|
|
@ -7,7 +7,6 @@ import { makePaginationQuery } from '../../common/make-pagination-query.js';
|
||||||
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
|
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
|
||||||
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
|
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
|
||||||
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
|
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
|
||||||
import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query.js';
|
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['users', 'notes'],
|
tags: ['users', 'notes'],
|
||||||
|
@ -77,9 +76,10 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
|
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
|
||||||
|
|
||||||
generateVisibilityQuery(query, me);
|
generateVisibilityQuery(query, me);
|
||||||
if (me) generateMutedUserQuery(query, me, user);
|
if (me) {
|
||||||
if (me) generateBlockedUserQuery(query, me);
|
generateMutedUserQuery(query, me, user);
|
||||||
if (me) generateMutedInstanceQuery(query, me);
|
generateBlockedUserQuery(query, me);
|
||||||
|
}
|
||||||
|
|
||||||
if (ps.withFiles) {
|
if (ps.withFiles) {
|
||||||
query.andWhere('note.fileIds != \'{}\'');
|
query.andWhere('note.fileIds != \'{}\'');
|
||||||
|
|
|
@ -8,6 +8,8 @@ import multer from '@koa/multer';
|
||||||
import bodyParser from 'koa-bodyparser';
|
import bodyParser from 'koa-bodyparser';
|
||||||
import cors from '@koa/cors';
|
import cors from '@koa/cors';
|
||||||
|
|
||||||
|
import { Instances, AccessTokens, Users } from '@/models/index.js';
|
||||||
|
import config from '@/config/index.js';
|
||||||
import endpoints from './endpoints.js';
|
import endpoints from './endpoints.js';
|
||||||
import handler from './api-handler.js';
|
import handler from './api-handler.js';
|
||||||
import signup from './private/signup.js';
|
import signup from './private/signup.js';
|
||||||
|
@ -16,8 +18,6 @@ import signupPending from './private/signup-pending.js';
|
||||||
import discord from './service/discord.js';
|
import discord from './service/discord.js';
|
||||||
import github from './service/github.js';
|
import github from './service/github.js';
|
||||||
import twitter from './service/twitter.js';
|
import twitter from './service/twitter.js';
|
||||||
import { Instances, AccessTokens, Users } from '@/models/index.js';
|
|
||||||
import config from '@/config/index.js';
|
|
||||||
|
|
||||||
// Init app
|
// Init app
|
||||||
const app = new Koa();
|
const app = new Koa();
|
||||||
|
@ -56,11 +56,24 @@ for (const endpoint of endpoints) {
|
||||||
if (endpoint.meta.requireFile) {
|
if (endpoint.meta.requireFile) {
|
||||||
router.post(`/${endpoint.name}`, upload.single('file'), handler.bind(null, endpoint));
|
router.post(`/${endpoint.name}`, upload.single('file'), handler.bind(null, endpoint));
|
||||||
} else {
|
} else {
|
||||||
|
// 後方互換性のため
|
||||||
if (endpoint.name.includes('-')) {
|
if (endpoint.name.includes('-')) {
|
||||||
// 後方互換性のため
|
|
||||||
router.post(`/${endpoint.name.replace(/-/g, '_')}`, handler.bind(null, endpoint));
|
router.post(`/${endpoint.name.replace(/-/g, '_')}`, handler.bind(null, endpoint));
|
||||||
|
|
||||||
|
if (endpoint.meta.allowGet) {
|
||||||
|
router.get(`/${endpoint.name.replace(/-/g, '_')}`, handler.bind(null, endpoint));
|
||||||
|
} else {
|
||||||
|
router.get(`/${endpoint.name.replace(/-/g, '_')}`, async ctx => { ctx.status = 405; });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
router.post(`/${endpoint.name}`, handler.bind(null, endpoint));
|
router.post(`/${endpoint.name}`, handler.bind(null, endpoint));
|
||||||
|
|
||||||
|
if (endpoint.meta.allowGet) {
|
||||||
|
router.get(`/${endpoint.name}`, handler.bind(null, endpoint));
|
||||||
|
} else {
|
||||||
|
router.get(`/${endpoint.name}`, async ctx => { ctx.status = 405; });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,8 @@ import { IEndpointMeta } from './endpoints.js';
|
||||||
const logger = new Logger('limiter');
|
const logger = new Logger('limiter');
|
||||||
|
|
||||||
export const limiter = (limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string) => new Promise<void>((ok, reject) => {
|
export const limiter = (limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string) => new Promise<void>((ok, reject) => {
|
||||||
|
if (process.env.NODE_ENV === 'test') ok();
|
||||||
|
|
||||||
const hasShortTermLimit = typeof limitation.minInterval === 'number';
|
const hasShortTermLimit = typeof limitation.minInterval === 'number';
|
||||||
|
|
||||||
const hasLongTermLimit =
|
const hasLongTermLimit =
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import Channel from '../channel.js';
|
import Channel from '../channel.js';
|
||||||
import { Notes } from '@/models/index.js';
|
import { Notes } from '@/models/index.js';
|
||||||
import { isMutedUserRelated } from '@/misc/is-muted-user-related.js';
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related.js';
|
|
||||||
import { StreamMessages } from '../types.js';
|
import { StreamMessages } from '../types.js';
|
||||||
|
|
||||||
export default class extends Channel {
|
export default class extends Channel {
|
||||||
|
@ -27,9 +26,9 @@ export default class extends Channel {
|
||||||
const note = await Notes.pack(data.body.id, this.user, { detail: true });
|
const note = await Notes.pack(data.body.id, this.user, { detail: true });
|
||||||
|
|
||||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
if (isMutedUserRelated(note, this.muting)) return;
|
if (isUserRelated(note, this.muting)) return;
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isBlockerUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.blocking)) return;
|
||||||
|
|
||||||
this.connection.cacheNote(note);
|
this.connection.cacheNote(note);
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import Channel from '../channel.js';
|
import Channel from '../channel.js';
|
||||||
import { Notes, Users } from '@/models/index.js';
|
import { Notes, Users } from '@/models/index.js';
|
||||||
import { isMutedUserRelated } from '@/misc/is-muted-user-related.js';
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related.js';
|
|
||||||
import { User } from '@/models/entities/user.js';
|
import { User } from '@/models/entities/user.js';
|
||||||
import { StreamMessages } from '../types.js';
|
import { StreamMessages } from '../types.js';
|
||||||
import { Packed } from '@/misc/schema.js';
|
import { Packed } from '@/misc/schema.js';
|
||||||
|
@ -45,9 +44,9 @@ export default class extends Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
if (isMutedUserRelated(note, this.muting)) return;
|
if (isUserRelated(note, this.muting)) return;
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isBlockerUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.blocking)) return;
|
||||||
|
|
||||||
this.connection.cacheNote(note);
|
this.connection.cacheNote(note);
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { isMutedUserRelated } from '@/misc/is-muted-user-related.js';
|
|
||||||
import Channel from '../channel.js';
|
import Channel from '../channel.js';
|
||||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||||
import { Notes } from '@/models/index.js';
|
import { Notes } from '@/models/index.js';
|
||||||
import { checkWordMute } from '@/misc/check-word-mute.js';
|
import { checkWordMute } from '@/misc/check-word-mute.js';
|
||||||
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related.js';
|
|
||||||
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
|
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
|
||||||
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
import { Packed } from '@/misc/schema.js';
|
import { Packed } from '@/misc/schema.js';
|
||||||
|
|
||||||
export default class extends Channel {
|
export default class extends Channel {
|
||||||
|
@ -55,9 +54,9 @@ export default class extends Channel {
|
||||||
if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;
|
if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;
|
||||||
|
|
||||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
if (isMutedUserRelated(note, this.muting)) return;
|
if (isUserRelated(note, this.muting)) return;
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isBlockerUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.blocking)) return;
|
||||||
|
|
||||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { isMutedUserRelated } from '@/misc/is-muted-user-related.js';
|
|
||||||
import Channel from '../channel.js';
|
import Channel from '../channel.js';
|
||||||
import { Notes } from '@/models/index.js';
|
import { Notes } from '@/models/index.js';
|
||||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||||
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related.js';
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
import { Packed } from '@/misc/schema.js';
|
import { Packed } from '@/misc/schema.js';
|
||||||
|
|
||||||
export default class extends Channel {
|
export default class extends Channel {
|
||||||
|
@ -38,9 +37,9 @@ export default class extends Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
if (isMutedUserRelated(note, this.muting)) return;
|
if (isUserRelated(note, this.muting)) return;
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isBlockerUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.blocking)) return;
|
||||||
|
|
||||||
this.connection.cacheNote(note);
|
this.connection.cacheNote(note);
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { isMutedUserRelated } from '@/misc/is-muted-user-related.js';
|
|
||||||
import Channel from '../channel.js';
|
import Channel from '../channel.js';
|
||||||
import { Notes } from '@/models/index.js';
|
import { Notes } from '@/models/index.js';
|
||||||
import { checkWordMute } from '@/misc/check-word-mute.js';
|
import { checkWordMute } from '@/misc/check-word-mute.js';
|
||||||
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related.js';
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
|
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
|
||||||
import { Packed } from '@/misc/schema.js';
|
import { Packed } from '@/misc/schema.js';
|
||||||
|
|
||||||
|
@ -63,9 +62,9 @@ export default class extends Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
if (isMutedUserRelated(note, this.muting)) return;
|
if (isUserRelated(note, this.muting)) return;
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isBlockerUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.blocking)) return;
|
||||||
|
|
||||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { isMutedUserRelated } from '@/misc/is-muted-user-related.js';
|
|
||||||
import Channel from '../channel.js';
|
import Channel from '../channel.js';
|
||||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||||
import { Notes } from '@/models/index.js';
|
import { Notes } from '@/models/index.js';
|
||||||
import { checkWordMute } from '@/misc/check-word-mute.js';
|
import { checkWordMute } from '@/misc/check-word-mute.js';
|
||||||
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related.js';
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
|
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
|
||||||
import { Packed } from '@/misc/schema.js';
|
import { Packed } from '@/misc/schema.js';
|
||||||
|
|
||||||
|
@ -71,9 +70,9 @@ export default class extends Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
if (isMutedUserRelated(note, this.muting)) return;
|
if (isUserRelated(note, this.muting)) return;
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isBlockerUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.blocking)) return;
|
||||||
|
|
||||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { isMutedUserRelated } from '@/misc/is-muted-user-related.js';
|
|
||||||
import Channel from '../channel.js';
|
import Channel from '../channel.js';
|
||||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||||
import { Notes } from '@/models/index.js';
|
import { Notes } from '@/models/index.js';
|
||||||
import { checkWordMute } from '@/misc/check-word-mute.js';
|
import { checkWordMute } from '@/misc/check-word-mute.js';
|
||||||
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related.js';
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
import { Packed } from '@/misc/schema.js';
|
import { Packed } from '@/misc/schema.js';
|
||||||
|
|
||||||
export default class extends Channel {
|
export default class extends Channel {
|
||||||
|
@ -52,9 +51,9 @@ export default class extends Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
if (isMutedUserRelated(note, this.muting)) return;
|
if (iUserRelated(note, this.muting)) return;
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isBlockerUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.blocking)) return;
|
||||||
|
|
||||||
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
// 流れてきたNoteがミュートすべきNoteだったら無視する
|
||||||
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import Channel from '../channel.js';
|
import Channel from '../channel.js';
|
||||||
import { Notes, UserListJoinings, UserLists } from '@/models/index.js';
|
import { Notes, UserListJoinings, UserLists } from '@/models/index.js';
|
||||||
import { isMutedUserRelated } from '@/misc/is-muted-user-related.js';
|
|
||||||
import { User } from '@/models/entities/user.js';
|
import { User } from '@/models/entities/user.js';
|
||||||
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related.js';
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
import { Packed } from '@/misc/schema.js';
|
import { Packed } from '@/misc/schema.js';
|
||||||
|
|
||||||
export default class extends Channel {
|
export default class extends Channel {
|
||||||
|
@ -76,9 +75,9 @@ export default class extends Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
if (isMutedUserRelated(note, this.muting)) return;
|
if (isUserRelated(note, this.muting)) return;
|
||||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||||
if (isBlockerUserRelated(note, this.blocking)) return;
|
if (isUserRelated(note, this.blocking)) return;
|
||||||
|
|
||||||
this.send('note', note);
|
this.send('note', note);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import Koa from 'koa';
|
import Koa from 'koa';
|
||||||
import { serverLogger } from '../index.js';
|
import sharp from 'sharp';
|
||||||
import { IImage, convertToWebp } from '@/services/drive/image-processor.js';
|
import { IImage, convertToWebp } from '@/services/drive/image-processor.js';
|
||||||
import { createTemp } from '@/misc/create-temp.js';
|
import { createTemp } from '@/misc/create-temp.js';
|
||||||
import { downloadUrl } from '@/misc/download-url.js';
|
import { downloadUrl } from '@/misc/download-url.js';
|
||||||
import { detectType } from '@/misc/get-file-info.js';
|
import { detectType } from '@/misc/get-file-info.js';
|
||||||
import { StatusError } from '@/misc/fetch.js';
|
import { StatusError } from '@/misc/fetch.js';
|
||||||
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
|
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
|
||||||
|
import { serverLogger } from '../index.js';
|
||||||
|
import { isMimeImage } from '@/misc/is-mime-image.js';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
export async function proxyMedia(ctx: Koa.Context) {
|
export async function proxyMedia(ctx: Koa.Context) {
|
||||||
const url = 'url' in ctx.query ? ctx.query.url : 'https://' + ctx.params.url;
|
const url = 'url' in ctx.query ? ctx.query.url : 'https://' + ctx.params.url;
|
||||||
|
|
||||||
|
@ -23,14 +26,50 @@ export async function proxyMedia(ctx: Koa.Context) {
|
||||||
await downloadUrl(url, path);
|
await downloadUrl(url, path);
|
||||||
|
|
||||||
const { mime, ext } = await detectType(path);
|
const { mime, ext } = await detectType(path);
|
||||||
|
const isConvertibleImage = isMimeImage(mime, 'sharp-convertible-image');
|
||||||
|
|
||||||
let image: IImage;
|
let image: IImage;
|
||||||
|
|
||||||
if ('static' in ctx.query && ['image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/svg+xml'].includes(mime)) {
|
if ('static' in ctx.query && isConvertibleImage) {
|
||||||
image = await convertToWebp(path, 498, 280);
|
image = await convertToWebp(path, 498, 280);
|
||||||
} else if ('preview' in ctx.query && ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/svg+xml'].includes(mime)) {
|
} else if ('preview' in ctx.query && isConvertibleImage) {
|
||||||
image = await convertToWebp(path, 200, 200);
|
image = await convertToWebp(path, 200, 200);
|
||||||
} else if (['image/svg+xml'].includes(mime)) {
|
} else if ('badge' in ctx.query) {
|
||||||
|
if (!isConvertibleImage) {
|
||||||
|
// 画像でないなら404でお茶を濁す
|
||||||
|
throw new StatusError('Unexpected mime', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mask = sharp(path)
|
||||||
|
.resize(96, 96, {
|
||||||
|
fit: 'inside',
|
||||||
|
withoutEnlargement: false,
|
||||||
|
})
|
||||||
|
.greyscale()
|
||||||
|
.normalise()
|
||||||
|
.linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast
|
||||||
|
.flatten({ background: '#000' })
|
||||||
|
.toColorspace('b-w');
|
||||||
|
|
||||||
|
const stats = await mask.clone().stats();
|
||||||
|
|
||||||
|
if (stats.entropy < 0.1) {
|
||||||
|
// エントロピーがあまりない場合は404にする
|
||||||
|
throw new StatusError('Skip to provide badge', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = sharp({
|
||||||
|
create: { width: 96, height: 96, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } },
|
||||||
|
})
|
||||||
|
.pipelineColorspace('b-w')
|
||||||
|
.boolean(await mask.png().toBuffer(), 'eor');
|
||||||
|
|
||||||
|
image = {
|
||||||
|
data: await data.png().toBuffer(),
|
||||||
|
ext: 'png',
|
||||||
|
type: 'image/png',
|
||||||
|
};
|
||||||
|
} else if (mime === 'image/svg+xml') {
|
||||||
image = await convertToWebp(path, 2048, 2048, 1);
|
image = await convertToWebp(path, 2048, 2048, 1);
|
||||||
} else if (!mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(mime)) {
|
} else if (!mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(mime)) {
|
||||||
throw new StatusError('Rejected type', 403, 'Rejected type');
|
throw new StatusError('Rejected type', 403, 'Rejected type');
|
||||||
|
@ -48,7 +87,7 @@ export async function proxyMedia(ctx: Koa.Context) {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
serverLogger.error(`${e}`);
|
serverLogger.error(`${e}`);
|
||||||
|
|
||||||
if (e instanceof StatusError && e.isClientError) {
|
if (e instanceof StatusError && (e.statusCode === 302 || e.isClientError)) {
|
||||||
ctx.status = e.statusCode;
|
ctx.status = e.statusCode;
|
||||||
} else {
|
} else {
|
||||||
ctx.status = 500;
|
ctx.status = 500;
|
||||||
|
|
|
@ -14,10 +14,10 @@
|
||||||
// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
|
// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
|
||||||
(async () => {
|
(async () => {
|
||||||
window.onerror = (e) => {
|
window.onerror = (e) => {
|
||||||
renderError('SOMETHING_HAPPENED', e.toString());
|
renderError('SOMETHING_HAPPENED', e);
|
||||||
};
|
};
|
||||||
window.onunhandledrejection = (e) => {
|
window.onunhandledrejection = (e) => {
|
||||||
renderError('SOMETHING_HAPPENED_IN_PROMISE', e.toString());
|
renderError('SOMETHING_HAPPENED_IN_PROMISE', e);
|
||||||
};
|
};
|
||||||
|
|
||||||
const v = localStorage.getItem('v') || VERSION;
|
const v = localStorage.getItem('v') || VERSION;
|
||||||
|
@ -57,7 +57,7 @@
|
||||||
import(`/assets/${CLIENT_ENTRY}`)
|
import(`/assets/${CLIENT_ENTRY}`)
|
||||||
.catch(async e => {
|
.catch(async e => {
|
||||||
await checkUpdate();
|
await checkUpdate();
|
||||||
renderError('APP_FETCH_FAILED', JSON.stringify(e));
|
renderError('APP_FETCH_FAILED', e);
|
||||||
})
|
})
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
@ -104,20 +104,27 @@
|
||||||
|
|
||||||
// eslint-disable-next-line no-inner-declarations
|
// eslint-disable-next-line no-inner-declarations
|
||||||
function renderError(code, details) {
|
function renderError(code, details) {
|
||||||
document.documentElement.innerHTML = `
|
let errorsElement = document.getElementById('errors');
|
||||||
<h1>⚠エラーが発生しました</h1>
|
if (!errorsElement) {
|
||||||
<p>問題が解決しない場合は管理者までお問い合わせください。以下のオプションを試すこともできます:</p>
|
document.documentElement.innerHTML = `
|
||||||
|
<h1>⚠ An error has occurred. ⚠</h1>
|
||||||
|
<p>If the problem persists, please contact the administrator. You may also try the following options:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/cli">簡易クライアント</a>を起動</li>
|
<li>Start <a href="/cli">the simple client</a></li>
|
||||||
<li><a href="/bios">BIOS</a>で修復を試みる</li>
|
<li>Attempt to repair in <a href="/bios">BIOS</a></li>
|
||||||
<li><a href="/flush">キャッシュをクリア</a>する</li>
|
<li><a href="/flush">Flush preferences and cache</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<hr>
|
<hr>
|
||||||
<code>ERROR CODE: ${code}</code>
|
<div id="errors"></div>
|
||||||
<details>
|
`;
|
||||||
${details}
|
|
||||||
</details>
|
errorsElement = document.getElementById('errors');
|
||||||
`;
|
}
|
||||||
|
|
||||||
|
const detailsElement = document.createElement('details');
|
||||||
|
detailsElement.innerHTML = `<summary><code>ERROR CODE: ${code}</code></summary>${JSON.stringify(details)}`;
|
||||||
|
|
||||||
|
errorsElement.appendChild(detailsElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-inner-declarations
|
// eslint-disable-next-line no-inner-declarations
|
||||||
|
|
|
@ -11,6 +11,7 @@ import Router from '@koa/router';
|
||||||
import send from 'koa-send';
|
import send from 'koa-send';
|
||||||
import favicon from 'koa-favicon';
|
import favicon from 'koa-favicon';
|
||||||
import views from 'koa-views';
|
import views from 'koa-views';
|
||||||
|
import sharp from 'sharp';
|
||||||
import { createBullBoard } from '@bull-board/api';
|
import { createBullBoard } from '@bull-board/api';
|
||||||
import { BullAdapter } from '@bull-board/api/bullAdapter.js';
|
import { BullAdapter } from '@bull-board/api/bullAdapter.js';
|
||||||
import { KoaAdapter } from '@bull-board/koa';
|
import { KoaAdapter } from '@bull-board/koa';
|
||||||
|
@ -140,6 +141,49 @@ router.get('/twemoji/(.*)', async ctx => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.get('/twemoji-badge/(.*)', async ctx => {
|
||||||
|
const path = ctx.path.replace('/twemoji-badge/', '');
|
||||||
|
|
||||||
|
if (!path.match(/^[0-9a-f-]+\.png$/)) {
|
||||||
|
ctx.status = 404;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mask = await sharp(
|
||||||
|
`${_dirname}/../../../node_modules/@discordapp/twemoji/dist/svg/${path.replace('.png', '')}.svg`,
|
||||||
|
{ density: 1000 },
|
||||||
|
)
|
||||||
|
.resize(488, 488)
|
||||||
|
.greyscale()
|
||||||
|
.normalise()
|
||||||
|
.linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast
|
||||||
|
.flatten({ background: '#000' })
|
||||||
|
.extend({
|
||||||
|
top: 12,
|
||||||
|
bottom: 12,
|
||||||
|
left: 12,
|
||||||
|
right: 12,
|
||||||
|
background: '#000',
|
||||||
|
})
|
||||||
|
.toColorspace('b-w')
|
||||||
|
.png()
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
const buffer = await sharp({
|
||||||
|
create: { width: 512, height: 512, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } },
|
||||||
|
})
|
||||||
|
.pipelineColorspace('b-w')
|
||||||
|
.boolean(mask, 'eor')
|
||||||
|
.resize(96, 96)
|
||||||
|
.png()
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
ctx.set('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'');
|
||||||
|
ctx.set('Cache-Control', 'max-age=2592000');
|
||||||
|
ctx.set('Content-Type', 'image/png');
|
||||||
|
ctx.body = buffer;
|
||||||
|
});
|
||||||
|
|
||||||
// ServiceWorker
|
// ServiceWorker
|
||||||
router.get(`/sw.js`, async ctx => {
|
router.get(`/sw.js`, async ctx => {
|
||||||
await send(ctx as any, `/sw.js`, {
|
await send(ctx as any, `/sw.js`, {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Antenna } from '@/models/entities/antenna.js';
|
||||||
import { Note } from '@/models/entities/note.js';
|
import { Note } from '@/models/entities/note.js';
|
||||||
import { AntennaNotes, Mutings, Notes } from '@/models/index.js';
|
import { AntennaNotes, Mutings, Notes } from '@/models/index.js';
|
||||||
import { genId } from '@/misc/gen-id.js';
|
import { genId } from '@/misc/gen-id.js';
|
||||||
import { isMutedUserRelated } from '@/misc/is-muted-user-related.js';
|
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||||
import { publishAntennaStream, publishMainStream } from '@/services/stream.js';
|
import { publishAntennaStream, publishMainStream } from '@/services/stream.js';
|
||||||
import { User } from '@/models/entities/user.js';
|
import { User } from '@/models/entities/user.js';
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ export async function addNoteToAntenna(antenna: Antenna, note: Note, noteUser: {
|
||||||
_note.renote = await Notes.findOneByOrFail({ id: note.renoteId });
|
_note.renote = await Notes.findOneByOrFail({ id: note.renoteId });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMutedUserRelated(_note, new Set<string>(mutings.map(x => x.muteeId)))) {
|
if (isUserRelated(_note, new Set<string>(mutings.map(x => x.muteeId)))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
23
packages/backend/src/services/delete-account.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { Users } from '@/models/index.js';
|
||||||
|
import { createDeleteAccountJob } from '@/queue/index.js';
|
||||||
|
import { publishUserEvent } from './stream.js';
|
||||||
|
import { doPostSuspend } from './suspend-user.js';
|
||||||
|
|
||||||
|
export async function deleteAccount(user: {
|
||||||
|
id: string;
|
||||||
|
host: string | null;
|
||||||
|
}): Promise<void> {
|
||||||
|
// 物理削除する前にDelete activityを送信する
|
||||||
|
await doPostSuspend(user).catch(e => {});
|
||||||
|
|
||||||
|
createDeleteAccountJob(user, {
|
||||||
|
soft: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await Users.update(user.id, {
|
||||||
|
isDeleted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Terminate streaming
|
||||||
|
publishUserEvent(user.id, 'terminate', {});
|
||||||
|
}
|
|
@ -14,7 +14,6 @@ export async function deliverQuestionUpdate(noteId: Note['id']) {
|
||||||
if (user == null) throw new Error('note not found');
|
if (user == null) throw new Error('note not found');
|
||||||
|
|
||||||
if (Users.isLocalUser(user)) {
|
if (Users.isLocalUser(user)) {
|
||||||
|
|
||||||
const content = renderActivity(renderUpdate(await renderNote(note, false), user));
|
const content = renderActivity(renderUpdate(await renderNote(note, false), user));
|
||||||
deliverToFollowers(user, content);
|
deliverToFollowers(user, content);
|
||||||
deliverToRelays(user, content);
|
deliverToRelays(user, content);
|
||||||
|
|
|
@ -2,7 +2,7 @@ process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import * as childProcess from 'child_process';
|
import * as childProcess from 'child_process';
|
||||||
import { async, signup, request, post, react, connectStream, startServer, shutdownServer } from './utils.js';
|
import { async, signup, request, post, react, startServer, shutdownServer, waitFire } from './utils.js';
|
||||||
|
|
||||||
describe('Mute', () => {
|
describe('Mute', () => {
|
||||||
let p: childProcess.ChildProcess;
|
let p: childProcess.ChildProcess;
|
||||||
|
@ -55,48 +55,24 @@ describe('Mute', () => {
|
||||||
assert.strictEqual(res.body.hasUnreadMentions, false);
|
assert.strictEqual(res.body.hasUnreadMentions, false);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise(async done => {
|
it('ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない', async () => {
|
||||||
// 状態リセット
|
// 状態リセット
|
||||||
await request('/i/read-all-unread-notes', {}, alice);
|
await request('/i/read-all-unread-notes', {}, alice);
|
||||||
|
|
||||||
let fired = false;
|
const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadMention');
|
||||||
|
|
||||||
const ws = await connectStream(alice, 'main', ({ type }) => {
|
assert.strictEqual(fired, false);
|
||||||
if (type == 'unreadMention') {
|
});
|
||||||
fired = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
post(carol, { text: '@alice hi' });
|
it('ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない', async () => {
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
assert.strictEqual(fired, false);
|
|
||||||
ws.close();
|
|
||||||
done();
|
|
||||||
}, 5000);
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない', () => new Promise(async done => {
|
|
||||||
// 状態リセット
|
// 状態リセット
|
||||||
await request('/i/read-all-unread-notes', {}, alice);
|
await request('/i/read-all-unread-notes', {}, alice);
|
||||||
await request('/notifications/mark-all-as-read', {}, alice);
|
await request('/notifications/mark-all-as-read', {}, alice);
|
||||||
|
|
||||||
let fired = false;
|
const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadNotification');
|
||||||
|
|
||||||
const ws = await connectStream(alice, 'main', ({ type }) => {
|
assert.strictEqual(fired, false);
|
||||||
if (type == 'unreadNotification') {
|
});
|
||||||
fired = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
post(carol, { text: '@alice hi' });
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
assert.strictEqual(fired, false);
|
|
||||||
ws.close();
|
|
||||||
done();
|
|
||||||
}, 5000);
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('Timeline', () => {
|
describe('Timeline', () => {
|
||||||
it('タイムラインにミュートしているユーザーの投稿が含まれない', async(async () => {
|
it('タイムラインにミュートしているユーザーの投稿が含まれない', async(async () => {
|
||||||
|
|
|
@ -3,7 +3,7 @@ process.env.NODE_ENV = 'test';
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import * as childProcess from 'child_process';
|
import * as childProcess from 'child_process';
|
||||||
import { Note } from '../src/models/entities/note.js';
|
import { Note } from '../src/models/entities/note.js';
|
||||||
import { async, signup, request, post, uploadFile, startServer, shutdownServer, initTestDb } from './utils.js';
|
import { async, signup, request, post, uploadUrl, startServer, shutdownServer, initTestDb, api } from './utils.js';
|
||||||
|
|
||||||
describe('Note', () => {
|
describe('Note', () => {
|
||||||
let p: childProcess.ChildProcess;
|
let p: childProcess.ChildProcess;
|
||||||
|
@ -37,7 +37,7 @@ describe('Note', () => {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('ファイルを添付できる', async(async () => {
|
it('ファイルを添付できる', async(async () => {
|
||||||
const file = await uploadFile(alice);
|
const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
|
||||||
|
|
||||||
const res = await request('/notes/create', {
|
const res = await request('/notes/create', {
|
||||||
fileIds: [file.id],
|
fileIds: [file.id],
|
||||||
|
@ -49,7 +49,7 @@ describe('Note', () => {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('他人のファイルは無視', async(async () => {
|
it('他人のファイルは無視', async(async () => {
|
||||||
const file = await uploadFile(bob);
|
const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
|
||||||
|
|
||||||
const res = await request('/notes/create', {
|
const res = await request('/notes/create', {
|
||||||
text: 'test',
|
text: 'test',
|
||||||
|
@ -72,11 +72,13 @@ describe('Note', () => {
|
||||||
assert.deepStrictEqual(res.body.createdNote.fileIds, []);
|
assert.deepStrictEqual(res.body.createdNote.fileIds, []);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('不正なファイルIDで怒られる', async(async () => {
|
it('不正なファイルIDは無視', async(async () => {
|
||||||
const res = await request('/notes/create', {
|
const res = await request('/notes/create', {
|
||||||
fileIds: ['kyoppie'],
|
fileIds: ['kyoppie'],
|
||||||
}, alice);
|
}, alice);
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 200);
|
||||||
|
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||||
|
assert.deepStrictEqual(res.body.createdNote.fileIds, []);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('返信できる', async(async () => {
|
it('返信できる', async(async () => {
|
||||||
|
@ -136,7 +138,7 @@ describe('Note', () => {
|
||||||
|
|
||||||
it('文字数ぎりぎりで怒られない', async(async () => {
|
it('文字数ぎりぎりで怒られない', async(async () => {
|
||||||
const post = {
|
const post = {
|
||||||
text: '!'.repeat(500),
|
text: '!'.repeat(3000),
|
||||||
};
|
};
|
||||||
const res = await request('/notes/create', post, alice);
|
const res = await request('/notes/create', post, alice);
|
||||||
assert.strictEqual(res.status, 200);
|
assert.strictEqual(res.status, 200);
|
||||||
|
@ -144,7 +146,7 @@ describe('Note', () => {
|
||||||
|
|
||||||
it('文字数オーバーで怒られる', async(async () => {
|
it('文字数オーバーで怒られる', async(async () => {
|
||||||
const post = {
|
const post = {
|
||||||
text: '!'.repeat(501),
|
text: '!'.repeat(3001),
|
||||||
};
|
};
|
||||||
const res = await request('/notes/create', post, alice);
|
const res = await request('/notes/create', post, alice);
|
||||||
assert.strictEqual(res.status, 400);
|
assert.strictEqual(res.status, 400);
|
||||||
|
@ -207,7 +209,7 @@ describe('Note', () => {
|
||||||
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
|
||||||
assert.strictEqual(res.body.createdNote.text, post.text);
|
assert.strictEqual(res.body.createdNote.text, post.text);
|
||||||
|
|
||||||
const noteDoc = await Notes.findOne(res.body.createdNote.id);
|
const noteDoc = await Notes.findOneBy({ id: res.body.createdNote.id });
|
||||||
assert.deepStrictEqual(noteDoc.mentions, [bob.id]);
|
assert.deepStrictEqual(noteDoc.mentions, [bob.id]);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -336,32 +338,32 @@ describe('Note', () => {
|
||||||
|
|
||||||
describe('notes/delete', () => {
|
describe('notes/delete', () => {
|
||||||
it('delete a reply', async(async () => {
|
it('delete a reply', async(async () => {
|
||||||
const mainNoteRes = await request('/notes/create', {
|
const mainNoteRes = await api('notes/create', {
|
||||||
text: 'main post',
|
text: 'main post',
|
||||||
}, alice);
|
}, alice);
|
||||||
const replyOneRes = await request('/notes/create', {
|
const replyOneRes = await api('notes/create', {
|
||||||
text: 'reply one',
|
text: 'reply one',
|
||||||
replyId: mainNoteRes.body.createdNote.id,
|
replyId: mainNoteRes.body.createdNote.id,
|
||||||
}, alice);
|
}, alice);
|
||||||
const replyTwoRes = await request('/notes/create', {
|
const replyTwoRes = await api('notes/create', {
|
||||||
text: 'reply two',
|
text: 'reply two',
|
||||||
replyId: mainNoteRes.body.createdNote.id,
|
replyId: mainNoteRes.body.createdNote.id,
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
const deleteOneRes = await request('/notes/delete', {
|
const deleteOneRes = await api('notes/delete', {
|
||||||
noteId: replyOneRes.body.createdNote.id,
|
noteId: replyOneRes.body.createdNote.id,
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(deleteOneRes.status, 204);
|
assert.strictEqual(deleteOneRes.status, 204);
|
||||||
let mainNote = await Notes.findOne({ id: mainNoteRes.body.createdNote.id });
|
let mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id });
|
||||||
assert.strictEqual(mainNote.repliesCount, 1);
|
assert.strictEqual(mainNote.repliesCount, 1);
|
||||||
|
|
||||||
const deleteTwoRes = await request('/notes/delete', {
|
const deleteTwoRes = await api('notes/delete', {
|
||||||
noteId: replyTwoRes.body.createdNote.id,
|
noteId: replyTwoRes.body.createdNote.id,
|
||||||
}, alice);
|
}, alice);
|
||||||
|
|
||||||
assert.strictEqual(deleteTwoRes.status, 204);
|
assert.strictEqual(deleteTwoRes.status, 204);
|
||||||
mainNote = await Notes.findOne({ id: mainNoteRes.body.createdNote.id });
|
mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id });
|
||||||
assert.strictEqual(mainNote.repliesCount, 0);
|
assert.strictEqual(mainNote.repliesCount, 0);
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,12 +2,7 @@ process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import * as childProcess from 'child_process';
|
import * as childProcess from 'child_process';
|
||||||
import { dirname } from 'node:path';
|
import { async, signup, request, post, uploadUrl, startServer, shutdownServer } from './utils.js';
|
||||||
import { fileURLToPath } from 'node:url';
|
|
||||||
import { async, signup, request, post, uploadFile, startServer, shutdownServer } from './utils.js';
|
|
||||||
|
|
||||||
const _filename = fileURLToPath(import.meta.url);
|
|
||||||
const _dirname = dirname(_filename);
|
|
||||||
|
|
||||||
describe('users/notes', () => {
|
describe('users/notes', () => {
|
||||||
let p: childProcess.ChildProcess;
|
let p: childProcess.ChildProcess;
|
||||||
|
@ -20,8 +15,8 @@ describe('users/notes', () => {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
p = await startServer();
|
p = await startServer();
|
||||||
alice = await signup({ username: 'alice' });
|
alice = await signup({ username: 'alice' });
|
||||||
const jpg = await uploadFile(alice, _dirname + '/resources/Lenna.jpg');
|
const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
|
||||||
const png = await uploadFile(alice, _dirname + '/resources/Lenna.png');
|
const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.png');
|
||||||
jpgNote = await post(alice, {
|
jpgNote = await post(alice, {
|
||||||
fileIds: [jpg.id],
|
fileIds: [jpg.id],
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
|
import * as path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { dirname } from 'node:path';
|
import { dirname } from 'node:path';
|
||||||
import * as childProcess from 'child_process';
|
import * as childProcess from 'child_process';
|
||||||
import * as http from 'node:http';
|
import * as http from 'node:http';
|
||||||
import { SIGKILL } from 'constants';
|
import { SIGKILL } from 'constants';
|
||||||
import * as WebSocket from 'ws';
|
import WebSocket from 'ws';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'misskey-js';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
import FormData from 'form-data';
|
import FormData from 'form-data';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
import loadConfig from '../src/config/load.js';
|
import loadConfig from '../src/config/load.js';
|
||||||
import { entities } from '../src/db/postgre.js';
|
import { entities } from '../src/db/postgre.js';
|
||||||
|
import got from 'got';
|
||||||
|
|
||||||
const _filename = fileURLToPath(import.meta.url);
|
const _filename = fileURLToPath(import.meta.url);
|
||||||
const _dirname = dirname(_filename);
|
const _dirname = dirname(_filename);
|
||||||
|
@ -26,6 +28,42 @@ export const async = (fn: Function) => (done: Function) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const api = async (endpoint: string, params: any, me?: any) => {
|
||||||
|
endpoint = endpoint.replace(/^\//, '');
|
||||||
|
|
||||||
|
const auth = me ? {
|
||||||
|
i: me.token
|
||||||
|
} : {};
|
||||||
|
|
||||||
|
const res = await got<string>(`http://localhost:${port}/api/${endpoint}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(Object.assign(auth, params)),
|
||||||
|
retry: {
|
||||||
|
limit: 0,
|
||||||
|
},
|
||||||
|
hooks: {
|
||||||
|
beforeError: [
|
||||||
|
error => {
|
||||||
|
const { response } = error;
|
||||||
|
if (response && response.body) console.warn(response.body);
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = res.statusCode;
|
||||||
|
const body = res.statusCode !== 204 ? await JSON.parse(res.body) : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
body
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const request = async (endpoint: string, params: any, me?: any): Promise<{ body: any, status: number }> => {
|
export const request = async (endpoint: string, params: any, me?: any): Promise<{ body: any, status: number }> => {
|
||||||
const auth = me ? {
|
const auth = me ? {
|
||||||
i: me.token,
|
i: me.token,
|
||||||
|
@ -53,7 +91,7 @@ export const signup = async (params?: any): Promise<any> => {
|
||||||
password: 'test',
|
password: 'test',
|
||||||
}, params);
|
}, params);
|
||||||
|
|
||||||
const res = await request('/signup', q);
|
const res = await api('signup', q);
|
||||||
|
|
||||||
return res.body;
|
return res.body;
|
||||||
};
|
};
|
||||||
|
@ -63,34 +101,62 @@ export const post = async (user: any, params?: misskey.Endpoints['notes/create']
|
||||||
text: 'test',
|
text: 'test',
|
||||||
}, params);
|
}, params);
|
||||||
|
|
||||||
const res = await request('/notes/create', q, user);
|
const res = await api('notes/create', q, user);
|
||||||
|
|
||||||
return res.body ? res.body.createdNote : null;
|
return res.body ? res.body.createdNote : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const react = async (user: any, note: any, reaction: string): Promise<any> => {
|
export const react = async (user: any, note: any, reaction: string): Promise<any> => {
|
||||||
await request('/notes/reactions/create', {
|
await api('notes/reactions/create', {
|
||||||
noteId: note.id,
|
noteId: note.id,
|
||||||
reaction: reaction,
|
reaction: reaction,
|
||||||
}, user);
|
}, user);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const uploadFile = (user: any, path?: string): Promise<any> => {
|
/**
|
||||||
const formData = new FormData();
|
* Upload file
|
||||||
formData.append('i', user.token);
|
* @param user User
|
||||||
formData.append('file', fs.createReadStream(path || _dirname + '/resources/Lenna.png'));
|
* @param _path Optional, absolute path or relative from ./resources/
|
||||||
|
*/
|
||||||
|
export const uploadFile = async (user: any, _path?: string): Promise<any> => {
|
||||||
|
const absPath = _path == null ? `${_dirname}/resources/Lenna.jpg` : path.isAbsolute(_path) ? _path : `${_dirname}/resources/${_path}`;
|
||||||
|
|
||||||
return fetch(`http://localhost:${port}/api/drive/files/create`, {
|
const formData = new FormData() as any;
|
||||||
method: 'post',
|
formData.append('i', user.token);
|
||||||
|
formData.append('file', fs.createReadStream(absPath));
|
||||||
|
formData.append('force', 'true');
|
||||||
|
|
||||||
|
const res = await got<string>(`http://localhost:${port}/api/drive/files/create`, {
|
||||||
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
timeout: 30 * 1000,
|
retry: {
|
||||||
}).then(res => {
|
limit: 0,
|
||||||
if (!res.ok) {
|
},
|
||||||
throw `${res.status} ${res.statusText}`;
|
});
|
||||||
} else {
|
|
||||||
return res.json();
|
const body = res.statusCode !== 204 ? await JSON.parse(res.body) : null;
|
||||||
|
|
||||||
|
return body;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const uploadUrl = async (user: any, url: string) => {
|
||||||
|
let file: any;
|
||||||
|
|
||||||
|
const ws = await connectStream(user, 'main', (msg) => {
|
||||||
|
if (msg.type === 'driveFileCreated') {
|
||||||
|
file = msg.body;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await api('drive/files/upload-from-url', {
|
||||||
|
url,
|
||||||
|
force: true,
|
||||||
|
}, user);
|
||||||
|
|
||||||
|
await sleep(5000);
|
||||||
|
ws.close();
|
||||||
|
|
||||||
|
return file;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function connectStream(user: any, channel: string, listener: (message: Record<string, any>) => any, params?: any): Promise<WebSocket> {
|
export function connectStream(user: any, channel: string, listener: (message: Record<string, any>) => any, params?: any): Promise<WebSocket> {
|
||||||
|
@ -120,6 +186,40 @@ export function connectStream(user: any, channel: string, listener: (message: Re
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const waitFire = async (user: any, channel: string, trgr: () => any, cond: (msg: Record<string, any>) => boolean) => {
|
||||||
|
return new Promise<boolean>(async (res, rej) => {
|
||||||
|
let timer: NodeJS.Timeout;
|
||||||
|
|
||||||
|
let ws: WebSocket;
|
||||||
|
try {
|
||||||
|
ws = await connectStream(user, channel, msg => {
|
||||||
|
if (cond(msg)) {
|
||||||
|
ws.close();
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
res(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
rej(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ws!) return;
|
||||||
|
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
ws.close();
|
||||||
|
res(false);
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await trgr();
|
||||||
|
} catch (e) {
|
||||||
|
ws.close();
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
rej(e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
export const simpleGet = async (path: string, accept = '*/*'): Promise<{ status?: number, type?: string, location?: string }> => {
|
export const simpleGet = async (path: string, accept = '*/*'): Promise<{ status?: number, type?: string, location?: string }> => {
|
||||||
// node-fetchだと3xxを取れない
|
// node-fetchだと3xxを取れない
|
||||||
return await new Promise((resolve, reject) => {
|
return await new Promise((resolve, reject) => {
|
||||||
|
@ -176,7 +276,7 @@ export async function initTestDb(justBorrow = false, initEntities?: any[]) {
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function startServer(timeout = 30 * 1000): Promise<childProcess.ChildProcess> {
|
export function startServer(timeout = 60 * 1000): Promise<childProcess.ChildProcess> {
|
||||||
return new Promise((res, rej) => {
|
return new Promise((res, rej) => {
|
||||||
const t = setTimeout(() => {
|
const t = setTimeout(() => {
|
||||||
p.kill(SIGKILL);
|
p.kill(SIGKILL);
|
||||||
|
@ -214,3 +314,11 @@ export function shutdownServer(p: childProcess.ChildProcess, timeout = 20 * 1000
|
||||||
p.kill();
|
p.kill();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sleep(msec: number) {
|
||||||
|
return new Promise<void>(res => {
|
||||||
|
setTimeout(() => {
|
||||||
|
res();
|
||||||
|
}, msec);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -25,7 +25,6 @@ module.exports = {
|
||||||
// data の禁止理由: 抽象的すぎるため
|
// data の禁止理由: 抽象的すぎるため
|
||||||
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
|
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
|
||||||
'id-denylist': ['error', 'window', 'data', 'e'],
|
'id-denylist': ['error', 'window', 'data', 'e'],
|
||||||
'eqeqeq': ['error', 'always', { 'null': 'ignore' }],
|
|
||||||
'no-shadow': ['warn'],
|
'no-shadow': ['warn'],
|
||||||
'vue/attributes-order': ['error', {
|
'vue/attributes-order': ['error', {
|
||||||
'alphabetical': false,
|
'alphabetical': false,
|
||||||
|
|
|
@ -79,7 +79,6 @@
|
||||||
"vite": "2.9.10",
|
"vite": "2.9.10",
|
||||||
"vue": "3.2.37",
|
"vue": "3.2.37",
|
||||||
"vue-prism-editor": "2.0.0-alpha.2",
|
"vue-prism-editor": "2.0.0-alpha.2",
|
||||||
"vue-router": "4.0.16",
|
|
||||||
"vuedraggable": "4.0.1",
|
"vuedraggable": "4.0.1",
|
||||||
"websocket": "1.0.34",
|
"websocket": "1.0.34",
|
||||||
"ws": "8.8.0"
|
"ws": "8.8.0"
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { del, get, set } from '@/scripts/idb-proxy';
|
|
||||||
import { defineAsyncComponent, reactive } from 'vue';
|
import { defineAsyncComponent, reactive } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'misskey-js';
|
||||||
|
import { showSuspendedDialog } from './scripts/show-suspended-dialog';
|
||||||
|
import { i18n } from './i18n';
|
||||||
|
import { del, get, set } from '@/scripts/idb-proxy';
|
||||||
import { apiUrl } from '@/config';
|
import { apiUrl } from '@/config';
|
||||||
import { waiting, api, popup, popupMenu, success, alert } from '@/os';
|
import { waiting, api, popup, popupMenu, success, alert } from '@/os';
|
||||||
import { unisonReload, reloadChannel } from '@/scripts/unison-reload';
|
import { unisonReload, reloadChannel } from '@/scripts/unison-reload';
|
||||||
import { showSuspendedDialog } from './scripts/show-suspended-dialog';
|
|
||||||
import { i18n } from './i18n';
|
|
||||||
|
|
||||||
// TODO: 他のタブと永続化されたstateを同期
|
// TODO: 他のタブと永続化されたstateを同期
|
||||||
|
|
||||||
|
@ -22,13 +22,9 @@ export async function signout() {
|
||||||
waiting();
|
waiting();
|
||||||
localStorage.removeItem('account');
|
localStorage.removeItem('account');
|
||||||
|
|
||||||
//#region Remove account
|
await removeAccount($i.id);
|
||||||
const accounts = await getAccounts();
|
|
||||||
accounts.splice(accounts.findIndex(x => x.id === $i.id), 1);
|
|
||||||
|
|
||||||
if (accounts.length > 0) await set('accounts', accounts);
|
const accounts = await getAccounts();
|
||||||
else await del('accounts');
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
//#region Remove service worker registration
|
//#region Remove service worker registration
|
||||||
try {
|
try {
|
||||||
|
@ -55,7 +51,7 @@ export async function signout() {
|
||||||
} catch (err) {}
|
} catch (err) {}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
document.cookie = `igi=; path=/`;
|
document.cookie = 'igi=; path=/';
|
||||||
|
|
||||||
if (accounts.length > 0) login(accounts[0].token);
|
if (accounts.length > 0) login(accounts[0].token);
|
||||||
else unisonReload('/');
|
else unisonReload('/');
|
||||||
|
@ -72,14 +68,22 @@ export async function addAccount(id: Account['id'], token: Account['token']) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function removeAccount(id: Account['id']) {
|
||||||
|
const accounts = await getAccounts();
|
||||||
|
accounts.splice(accounts.findIndex(x => x.id === id), 1);
|
||||||
|
|
||||||
|
if (accounts.length > 0) await set('accounts', accounts);
|
||||||
|
else await del('accounts');
|
||||||
|
}
|
||||||
|
|
||||||
function fetchAccount(token: string): Promise<Account> {
|
function fetchAccount(token: string): Promise<Account> {
|
||||||
return new Promise((done, fail) => {
|
return new Promise((done, fail) => {
|
||||||
// Fetch user
|
// Fetch user
|
||||||
fetch(`${apiUrl}/i`, {
|
fetch(`${apiUrl}/i`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
i: token
|
i: token,
|
||||||
})
|
}),
|
||||||
})
|
})
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(res => {
|
.then(res => {
|
||||||
|
@ -216,13 +220,13 @@ export async function openAccountMenu(opts: {
|
||||||
type: 'link',
|
type: 'link',
|
||||||
icon: 'fas fa-users',
|
icon: 'fas fa-users',
|
||||||
text: i18n.ts.manageAccounts,
|
text: i18n.ts.manageAccounts,
|
||||||
to: `/settings/accounts`,
|
to: '/settings/accounts',
|
||||||
}]], ev.currentTarget ?? ev.target, {
|
}]], ev.currentTarget ?? ev.target, {
|
||||||
align: 'left'
|
align: 'left',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
popupMenu([...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget ?? ev.target, {
|
popupMenu([...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget ?? ev.target, {
|
||||||
align: 'left'
|
align: 'left',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="bcekxzvu _card _gap">
|
<div class="bcekxzvu _gap _panel">
|
||||||
<div class="_content target">
|
<div class="target">
|
||||||
<MkAvatar class="avatar" :user="report.targetUser" :show-indicator="true"/>
|
<MkA v-user-preview="report.targetUserId" class="info" :to="`/user-info/${report.targetUserId}`">
|
||||||
<MkA v-user-preview="report.targetUserId" class="info" :to="userPage(report.targetUser)">
|
<MkAvatar class="avatar" :user="report.targetUser" :show-indicator="true" :disable-link="true"/>
|
||||||
<MkUserName class="name" :user="report.targetUser"/>
|
<div class="names">
|
||||||
<MkAcct class="acct" :user="report.targetUser" style="display: block;"/>
|
<MkUserName class="name" :user="report.targetUser"/>
|
||||||
|
<MkAcct class="acct" :user="report.targetUser" style="display: block;"/>
|
||||||
|
</div>
|
||||||
</MkA>
|
</MkA>
|
||||||
|
<MkKeyValue class="_formBlock">
|
||||||
|
<template #key>{{ $ts.registeredDate }}</template>
|
||||||
|
<template #value>{{ new Date(report.targetUser.createdAt).toLocaleString() }} (<MkTime :time="report.targetUser.createdAt"/>)</template>
|
||||||
|
</MkKeyValue>
|
||||||
</div>
|
</div>
|
||||||
<div class="_content">
|
<div class="detail">
|
||||||
<div>
|
<div>
|
||||||
<Mfm :text="report.comment"/>
|
<Mfm :text="report.comment"/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -18,85 +24,85 @@
|
||||||
<MkAcct :user="report.assignee"/>
|
<MkAcct :user="report.assignee"/>
|
||||||
</div>
|
</div>
|
||||||
<div><MkTime :time="report.createdAt"/></div>
|
<div><MkTime :time="report.createdAt"/></div>
|
||||||
</div>
|
<div class="action">
|
||||||
<div class="_footer">
|
<MkSwitch v-model="forward" :disabled="report.targetUser.host == null || report.resolved">
|
||||||
<MkSwitch v-model="forward" :disabled="report.targetUser.host == null || report.resolved">
|
{{ $ts.forwardReport }}
|
||||||
{{ $ts.forwardReport }}
|
<template #caption>{{ $ts.forwardReportIsAnonymous }}</template>
|
||||||
<template #caption>{{ $ts.forwardReportIsAnonymous }}</template>
|
</MkSwitch>
|
||||||
</MkSwitch>
|
<MkButton v-if="!report.resolved" primary @click="resolve">{{ $ts.abuseMarkAsResolved }}</MkButton>
|
||||||
<MkButton v-if="!report.resolved" primary @click="resolve">{{ $ts.abuseMarkAsResolved }}</MkButton>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
|
|
||||||
import MkButton from '@/components/ui/button.vue';
|
import MkButton from '@/components/ui/button.vue';
|
||||||
import MkSwitch from '@/components/form/switch.vue';
|
import MkSwitch from '@/components/form/switch.vue';
|
||||||
|
import MkKeyValue from '@/components/key-value.vue';
|
||||||
import { acct, userPage } from '@/filters/user';
|
import { acct, userPage } from '@/filters/user';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
components: {
|
report: any;
|
||||||
MkButton,
|
}>();
|
||||||
MkSwitch,
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
const emit = defineEmits<{
|
||||||
report: {
|
(ev: 'resolved', reportId: string): void;
|
||||||
type: Object,
|
}>();
|
||||||
required: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
emits: ['resolved'],
|
let forward = $ref(props.report.forwarded);
|
||||||
|
|
||||||
data() {
|
function resolve() {
|
||||||
return {
|
os.apiWithDialog('admin/resolve-abuse-user-report', {
|
||||||
forward: this.report.forwarded,
|
forward: forward,
|
||||||
};
|
reportId: props.report.id,
|
||||||
},
|
}).then(() => {
|
||||||
|
emit('resolved', props.report.id);
|
||||||
methods: {
|
});
|
||||||
acct,
|
}
|
||||||
userPage,
|
|
||||||
|
|
||||||
resolve() {
|
|
||||||
os.apiWithDialog('admin/resolve-abuse-user-report', {
|
|
||||||
forward: this.forward,
|
|
||||||
reportId: this.report.id,
|
|
||||||
}).then(() => {
|
|
||||||
this.$emit('resolved', this.report.id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.bcekxzvu {
|
.bcekxzvu {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
> .target {
|
> .target {
|
||||||
display: flex;
|
width: 35%;
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
align-items: center;
|
padding: 24px;
|
||||||
|
border-right: solid 1px var(--divider);
|
||||||
> .avatar {
|
|
||||||
width: 42px;
|
|
||||||
height: 42px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .info {
|
> .info {
|
||||||
margin-left: 0.3em;
|
display: flex;
|
||||||
padding: 0 8px;
|
box-sizing: border-box;
|
||||||
flex: 1;
|
align-items: center;
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
--c: rgb(255 196 0 / 15%);
|
||||||
|
background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
|
||||||
|
background-size: 16px 16px;
|
||||||
|
|
||||||
> .name {
|
> .avatar {
|
||||||
font-weight: bold;
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .names {
|
||||||
|
margin-left: 0.3em;
|
||||||
|
padding: 0 8px;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
> .name {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .detail {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { markRaw, ref, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
import { markRaw, ref, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
||||||
import contains from '@/scripts/contains';
|
import contains from '@/scripts/contains';
|
||||||
|
import { char2filePath } from '@/scripts/twemoji-base';
|
||||||
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
|
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
|
||||||
import { acct } from '@/filters/user';
|
import { acct } from '@/filters/user';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
|
@ -42,7 +43,6 @@ import { MFM_TAGS } from '@/scripts/mfm-tags';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
import { emojilist } from '@/scripts/emojilist';
|
import { emojilist } from '@/scripts/emojilist';
|
||||||
import { instance } from '@/instance';
|
import { instance } from '@/instance';
|
||||||
import { twemojiSvgBase } from '@/scripts/twemoji-base';
|
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
type EmojiDef = {
|
type EmojiDef = {
|
||||||
|
@ -55,16 +55,10 @@ type EmojiDef = {
|
||||||
|
|
||||||
const lib = emojilist.filter(x => x.category !== 'flags');
|
const lib = emojilist.filter(x => x.category !== 'flags');
|
||||||
|
|
||||||
const char2file = (char: string) => {
|
|
||||||
let codes = Array.from(char).map(x => x.codePointAt(0)?.toString(16));
|
|
||||||
if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f');
|
|
||||||
return codes.filter(x => x && x.length).join('-');
|
|
||||||
};
|
|
||||||
|
|
||||||
const emjdb: EmojiDef[] = lib.map(x => ({
|
const emjdb: EmojiDef[] = lib.map(x => ({
|
||||||
emoji: x.char,
|
emoji: x.char,
|
||||||
name: x.name,
|
name: x.name,
|
||||||
url: `${twemojiSvgBase}/${char2file(x.char)}.svg`
|
url: char2filePath(x.char),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
for (const x of lib) {
|
for (const x of lib) {
|
||||||
|
@ -74,7 +68,7 @@ for (const x of lib) {
|
||||||
emoji: x.char,
|
emoji: x.char,
|
||||||
name: k,
|
name: k,
|
||||||
aliasOf: x.name,
|
aliasOf: x.name,
|
||||||
url: `${twemojiSvgBase}/${char2file(x.char)}.svg`
|
url: char2filePath(x.char),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,27 @@
|
||||||
<template>
|
<template>
|
||||||
<MkTooltip ref="tooltip" :showing="showing" :x="x" :y="y" :max-width="340" :direction="'left'" :inner-margin="16" @closed="emit('closed')">
|
<MkTooltip ref="tooltip" :showing="showing" :x="x" :y="y" :max-width="340" :direction="'top'" :inner-margin="16" @closed="emit('closed')">
|
||||||
<div v-if="title" class="qpcyisrl">
|
<div v-if="title || series" class="qpcyisrl">
|
||||||
<div class="title">{{ title }}</div>
|
<div v-if="title" class="title">{{ title }}</div>
|
||||||
<div v-for="x in series" class="series">
|
<template v-if="series">
|
||||||
<span class="color" :style="{ background: x.backgroundColor, borderColor: x.borderColor }"></span>
|
<div v-for="x in series" class="series">
|
||||||
<span>{{ x.text }}</span>
|
<span class="color" :style="{ background: x.backgroundColor, borderColor: x.borderColor }"></span>
|
||||||
</div>
|
<span>{{ x.text }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</MkTooltip>
|
</MkTooltip>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { } from 'vue';
|
||||||
import MkTooltip from './ui/tooltip.vue';
|
import MkTooltip from './ui/tooltip.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
showing: boolean;
|
showing: boolean;
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
title: string;
|
title?: string;
|
||||||
series: {
|
series?: {
|
||||||
backgroundColor: string;
|
backgroundColor: string;
|
||||||
borderColor: string;
|
borderColor: string;
|
||||||
text: string;
|
text: string;
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
id-denylist violation when setting it. This is causing about 60+ lint issues.
|
id-denylist violation when setting it. This is causing about 60+ lint issues.
|
||||||
As this is part of Chart.js's API it makes sense to disable the check here.
|
As this is part of Chart.js's API it makes sense to disable the check here.
|
||||||
*/
|
*/
|
||||||
import { defineProps, onMounted, ref, watch, PropType, onUnmounted } from 'vue';
|
import { onMounted, ref, watch, PropType, onUnmounted } from 'vue';
|
||||||
import {
|
import {
|
||||||
Chart,
|
Chart,
|
||||||
ArcElement,
|
ArcElement,
|
||||||
|
@ -39,7 +39,7 @@ import zoomPlugin from 'chartjs-plugin-zoom';
|
||||||
//import gradient from 'chartjs-plugin-gradient';
|
//import gradient from 'chartjs-plugin-gradient';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
import MkChartTooltip from '@/components/chart-tooltip.vue';
|
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
src: {
|
src: {
|
||||||
|
@ -53,7 +53,7 @@ const props = defineProps({
|
||||||
limit: {
|
limit: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: false,
|
required: false,
|
||||||
default: 90
|
default: 90,
|
||||||
},
|
},
|
||||||
span: {
|
span: {
|
||||||
type: String as PropType<'hour' | 'day'>,
|
type: String as PropType<'hour' | 'day'>,
|
||||||
|
@ -62,22 +62,22 @@ const props = defineProps({
|
||||||
detailed: {
|
detailed: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false,
|
||||||
},
|
},
|
||||||
stacked: {
|
stacked: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false,
|
||||||
},
|
},
|
||||||
bar: {
|
bar: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false,
|
||||||
},
|
},
|
||||||
aspectRatio: {
|
aspectRatio: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -156,46 +156,11 @@ const getDate = (ago: number) => {
|
||||||
const format = (arr) => {
|
const format = (arr) => {
|
||||||
return arr.map((v, i) => ({
|
return arr.map((v, i) => ({
|
||||||
x: getDate(i).getTime(),
|
x: getDate(i).getTime(),
|
||||||
y: v
|
y: v,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const tooltipShowing = ref(false);
|
const { handler: externalTooltipHandler } = useChartTooltip();
|
||||||
const tooltipX = ref(0);
|
|
||||||
const tooltipY = ref(0);
|
|
||||||
const tooltipTitle = ref(null);
|
|
||||||
const tooltipSeries = ref(null);
|
|
||||||
let disposeTooltipComponent;
|
|
||||||
|
|
||||||
os.popup(MkChartTooltip, {
|
|
||||||
showing: tooltipShowing,
|
|
||||||
x: tooltipX,
|
|
||||||
y: tooltipY,
|
|
||||||
title: tooltipTitle,
|
|
||||||
series: tooltipSeries,
|
|
||||||
}, {}).then(({ dispose }) => {
|
|
||||||
disposeTooltipComponent = dispose;
|
|
||||||
});
|
|
||||||
|
|
||||||
function externalTooltipHandler(context) {
|
|
||||||
if (context.tooltip.opacity === 0) {
|
|
||||||
tooltipShowing.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
tooltipTitle.value = context.tooltip.title[0];
|
|
||||||
tooltipSeries.value = context.tooltip.body.map((b, i) => ({
|
|
||||||
backgroundColor: context.tooltip.labelColors[i].backgroundColor,
|
|
||||||
borderColor: context.tooltip.labelColors[i].borderColor,
|
|
||||||
text: b.lines[0],
|
|
||||||
}));
|
|
||||||
|
|
||||||
const rect = context.chart.canvas.getBoundingClientRect();
|
|
||||||
|
|
||||||
tooltipShowing.value = true;
|
|
||||||
tooltipX.value = rect.left + window.pageXOffset + context.tooltip.caretX;
|
|
||||||
tooltipY.value = rect.top + window.pageYOffset + context.tooltip.caretY;
|
|
||||||
}
|
|
||||||
|
|
||||||
const render = () => {
|
const render = () => {
|
||||||
if (chartInstance) {
|
if (chartInstance) {
|
||||||
|
@ -343,7 +308,7 @@ const render = () => {
|
||||||
min: 'original',
|
min: 'original',
|
||||||
max: 'original',
|
max: 'original',
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
} : undefined,
|
} : undefined,
|
||||||
//gradient,
|
//gradient,
|
||||||
},
|
},
|
||||||
|
@ -367,8 +332,8 @@ const render = () => {
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}]
|
}],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -377,7 +342,7 @@ const exportData = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchFederationChart = async (): Promise<typeof chartData> => {
|
const fetchFederationChart = async (): Promise<typeof chartData> => {
|
||||||
const raw = await os.api('charts/federation', { limit: props.limit, span: props.span });
|
const raw = await os.apiGet('charts/federation', { limit: props.limit, span: props.span });
|
||||||
return {
|
return {
|
||||||
series: [{
|
series: [{
|
||||||
name: 'Received',
|
name: 'Received',
|
||||||
|
@ -427,36 +392,36 @@ const fetchFederationChart = async (): Promise<typeof chartData> => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchApRequestChart = async (): Promise<typeof chartData> => {
|
const fetchApRequestChart = async (): Promise<typeof chartData> => {
|
||||||
const raw = await os.api('charts/ap-request', { limit: props.limit, span: props.span });
|
const raw = await os.apiGet('charts/ap-request', { limit: props.limit, span: props.span });
|
||||||
return {
|
return {
|
||||||
series: [{
|
series: [{
|
||||||
name: 'In',
|
name: 'In',
|
||||||
type: 'area',
|
type: 'area',
|
||||||
color: '#008FFB',
|
color: '#008FFB',
|
||||||
data: format(raw.inboxReceived)
|
data: format(raw.inboxReceived),
|
||||||
}, {
|
}, {
|
||||||
name: 'Out (succ)',
|
name: 'Out (succ)',
|
||||||
type: 'area',
|
type: 'area',
|
||||||
color: '#00E396',
|
color: '#00E396',
|
||||||
data: format(raw.deliverSucceeded)
|
data: format(raw.deliverSucceeded),
|
||||||
}, {
|
}, {
|
||||||
name: 'Out (fail)',
|
name: 'Out (fail)',
|
||||||
type: 'area',
|
type: 'area',
|
||||||
color: '#FEB019',
|
color: '#FEB019',
|
||||||
data: format(raw.deliverFailed)
|
data: format(raw.deliverFailed),
|
||||||
}]
|
}],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchNotesChart = async (type: string): Promise<typeof chartData> => {
|
const fetchNotesChart = async (type: string): Promise<typeof chartData> => {
|
||||||
const raw = await os.api('charts/notes', { limit: props.limit, span: props.span });
|
const raw = await os.apiGet('charts/notes', { limit: props.limit, span: props.span });
|
||||||
return {
|
return {
|
||||||
series: [{
|
series: [{
|
||||||
name: 'All',
|
name: 'All',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: format(type === 'combined'
|
data: format(type === 'combined'
|
||||||
? sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec))
|
? sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec))
|
||||||
: sum(raw[type].inc, negate(raw[type].dec))
|
: sum(raw[type].inc, negate(raw[type].dec)),
|
||||||
),
|
),
|
||||||
color: '#888888',
|
color: '#888888',
|
||||||
}, {
|
}, {
|
||||||
|
@ -464,7 +429,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => {
|
||||||
type: 'area',
|
type: 'area',
|
||||||
data: format(type === 'combined'
|
data: format(type === 'combined'
|
||||||
? sum(raw.local.diffs.renote, raw.remote.diffs.renote)
|
? sum(raw.local.diffs.renote, raw.remote.diffs.renote)
|
||||||
: raw[type].diffs.renote
|
: raw[type].diffs.renote,
|
||||||
),
|
),
|
||||||
color: colors.green,
|
color: colors.green,
|
||||||
}, {
|
}, {
|
||||||
|
@ -472,7 +437,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => {
|
||||||
type: 'area',
|
type: 'area',
|
||||||
data: format(type === 'combined'
|
data: format(type === 'combined'
|
||||||
? sum(raw.local.diffs.reply, raw.remote.diffs.reply)
|
? sum(raw.local.diffs.reply, raw.remote.diffs.reply)
|
||||||
: raw[type].diffs.reply
|
: raw[type].diffs.reply,
|
||||||
),
|
),
|
||||||
color: colors.yellow,
|
color: colors.yellow,
|
||||||
}, {
|
}, {
|
||||||
|
@ -480,7 +445,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => {
|
||||||
type: 'area',
|
type: 'area',
|
||||||
data: format(type === 'combined'
|
data: format(type === 'combined'
|
||||||
? sum(raw.local.diffs.normal, raw.remote.diffs.normal)
|
? sum(raw.local.diffs.normal, raw.remote.diffs.normal)
|
||||||
: raw[type].diffs.normal
|
: raw[type].diffs.normal,
|
||||||
),
|
),
|
||||||
color: colors.blue,
|
color: colors.blue,
|
||||||
}, {
|
}, {
|
||||||
|
@ -488,7 +453,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => {
|
||||||
type: 'area',
|
type: 'area',
|
||||||
data: format(type === 'combined'
|
data: format(type === 'combined'
|
||||||
? sum(raw.local.diffs.withFile, raw.remote.diffs.withFile)
|
? sum(raw.local.diffs.withFile, raw.remote.diffs.withFile)
|
||||||
: raw[type].diffs.withFile
|
: raw[type].diffs.withFile,
|
||||||
),
|
),
|
||||||
color: colors.purple,
|
color: colors.purple,
|
||||||
}],
|
}],
|
||||||
|
@ -496,7 +461,7 @@ const fetchNotesChart = async (type: string): Promise<typeof chartData> => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchNotesTotalChart = async (): Promise<typeof chartData> => {
|
const fetchNotesTotalChart = async (): Promise<typeof chartData> => {
|
||||||
const raw = await os.api('charts/notes', { limit: props.limit, span: props.span });
|
const raw = await os.apiGet('charts/notes', { limit: props.limit, span: props.span });
|
||||||
return {
|
return {
|
||||||
series: [{
|
series: [{
|
||||||
name: 'Combined',
|
name: 'Combined',
|
||||||
|
@ -515,35 +480,35 @@ const fetchNotesTotalChart = async (): Promise<typeof chartData> => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchUsersChart = async (total: boolean): Promise<typeof chartData> => {
|
const fetchUsersChart = async (total: boolean): Promise<typeof chartData> => {
|
||||||
const raw = await os.api('charts/users', { limit: props.limit, span: props.span });
|
const raw = await os.apiGet('charts/users', { limit: props.limit, span: props.span });
|
||||||
return {
|
return {
|
||||||
series: [{
|
series: [{
|
||||||
name: 'Combined',
|
name: 'Combined',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: format(total
|
data: format(total
|
||||||
? sum(raw.local.total, raw.remote.total)
|
? sum(raw.local.total, raw.remote.total)
|
||||||
: sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec))
|
: sum(raw.local.inc, negate(raw.local.dec), raw.remote.inc, negate(raw.remote.dec)),
|
||||||
),
|
),
|
||||||
}, {
|
}, {
|
||||||
name: 'Local',
|
name: 'Local',
|
||||||
type: 'area',
|
type: 'area',
|
||||||
data: format(total
|
data: format(total
|
||||||
? raw.local.total
|
? raw.local.total
|
||||||
: sum(raw.local.inc, negate(raw.local.dec))
|
: sum(raw.local.inc, negate(raw.local.dec)),
|
||||||
),
|
),
|
||||||
}, {
|
}, {
|
||||||
name: 'Remote',
|
name: 'Remote',
|
||||||
type: 'area',
|
type: 'area',
|
||||||
data: format(total
|
data: format(total
|
||||||
? raw.remote.total
|
? raw.remote.total
|
||||||
: sum(raw.remote.inc, negate(raw.remote.dec))
|
: sum(raw.remote.inc, negate(raw.remote.dec)),
|
||||||
),
|
),
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchActiveUsersChart = async (): Promise<typeof chartData> => {
|
const fetchActiveUsersChart = async (): Promise<typeof chartData> => {
|
||||||
const raw = await os.api('charts/active-users', { limit: props.limit, span: props.span });
|
const raw = await os.apiGet('charts/active-users', { limit: props.limit, span: props.span });
|
||||||
return {
|
return {
|
||||||
series: [{
|
series: [{
|
||||||
name: 'Read & Write',
|
name: 'Read & Write',
|
||||||
|
@ -595,7 +560,7 @@ const fetchActiveUsersChart = async (): Promise<typeof chartData> => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchDriveChart = async (): Promise<typeof chartData> => {
|
const fetchDriveChart = async (): Promise<typeof chartData> => {
|
||||||
const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
|
const raw = await os.apiGet('charts/drive', { limit: props.limit, span: props.span });
|
||||||
return {
|
return {
|
||||||
bytes: true,
|
bytes: true,
|
||||||
series: [{
|
series: [{
|
||||||
|
@ -607,8 +572,8 @@ const fetchDriveChart = async (): Promise<typeof chartData> => {
|
||||||
raw.local.incSize,
|
raw.local.incSize,
|
||||||
negate(raw.local.decSize),
|
negate(raw.local.decSize),
|
||||||
raw.remote.incSize,
|
raw.remote.incSize,
|
||||||
negate(raw.remote.decSize)
|
negate(raw.remote.decSize),
|
||||||
)
|
),
|
||||||
),
|
),
|
||||||
}, {
|
}, {
|
||||||
name: 'Local +',
|
name: 'Local +',
|
||||||
|
@ -631,7 +596,7 @@ const fetchDriveChart = async (): Promise<typeof chartData> => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchDriveFilesChart = async (): Promise<typeof chartData> => {
|
const fetchDriveFilesChart = async (): Promise<typeof chartData> => {
|
||||||
const raw = await os.api('charts/drive', { limit: props.limit, span: props.span });
|
const raw = await os.apiGet('charts/drive', { limit: props.limit, span: props.span });
|
||||||
return {
|
return {
|
||||||
series: [{
|
series: [{
|
||||||
name: 'All',
|
name: 'All',
|
||||||
|
@ -642,8 +607,8 @@ const fetchDriveFilesChart = async (): Promise<typeof chartData> => {
|
||||||
raw.local.incCount,
|
raw.local.incCount,
|
||||||
negate(raw.local.decCount),
|
negate(raw.local.decCount),
|
||||||
raw.remote.incCount,
|
raw.remote.incCount,
|
||||||
negate(raw.remote.decCount)
|
negate(raw.remote.decCount),
|
||||||
)
|
),
|
||||||
),
|
),
|
||||||
}, {
|
}, {
|
||||||
name: 'Local +',
|
name: 'Local +',
|
||||||
|
@ -666,29 +631,29 @@ const fetchDriveFilesChart = async (): Promise<typeof chartData> => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchInstanceRequestsChart = async (): Promise<typeof chartData> => {
|
const fetchInstanceRequestsChart = async (): Promise<typeof chartData> => {
|
||||||
const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
|
const raw = await os.apiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
|
||||||
return {
|
return {
|
||||||
series: [{
|
series: [{
|
||||||
name: 'In',
|
name: 'In',
|
||||||
type: 'area',
|
type: 'area',
|
||||||
color: '#008FFB',
|
color: '#008FFB',
|
||||||
data: format(raw.requests.received)
|
data: format(raw.requests.received),
|
||||||
}, {
|
}, {
|
||||||
name: 'Out (succ)',
|
name: 'Out (succ)',
|
||||||
type: 'area',
|
type: 'area',
|
||||||
color: '#00E396',
|
color: '#00E396',
|
||||||
data: format(raw.requests.succeeded)
|
data: format(raw.requests.succeeded),
|
||||||
}, {
|
}, {
|
||||||
name: 'Out (fail)',
|
name: 'Out (fail)',
|
||||||
type: 'area',
|
type: 'area',
|
||||||
color: '#FEB019',
|
color: '#FEB019',
|
||||||
data: format(raw.requests.failed)
|
data: format(raw.requests.failed),
|
||||||
}]
|
}],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchInstanceUsersChart = async (total: boolean): Promise<typeof chartData> => {
|
const fetchInstanceUsersChart = async (total: boolean): Promise<typeof chartData> => {
|
||||||
const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
|
const raw = await os.apiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
|
||||||
return {
|
return {
|
||||||
series: [{
|
series: [{
|
||||||
name: 'Users',
|
name: 'Users',
|
||||||
|
@ -696,14 +661,14 @@ const fetchInstanceUsersChart = async (total: boolean): Promise<typeof chartData
|
||||||
color: '#008FFB',
|
color: '#008FFB',
|
||||||
data: format(total
|
data: format(total
|
||||||
? raw.users.total
|
? raw.users.total
|
||||||
: sum(raw.users.inc, negate(raw.users.dec))
|
: sum(raw.users.inc, negate(raw.users.dec)),
|
||||||
)
|
),
|
||||||
}]
|
}],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchInstanceNotesChart = async (total: boolean): Promise<typeof chartData> => {
|
const fetchInstanceNotesChart = async (total: boolean): Promise<typeof chartData> => {
|
||||||
const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
|
const raw = await os.apiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
|
||||||
return {
|
return {
|
||||||
series: [{
|
series: [{
|
||||||
name: 'Notes',
|
name: 'Notes',
|
||||||
|
@ -711,14 +676,14 @@ const fetchInstanceNotesChart = async (total: boolean): Promise<typeof chartData
|
||||||
color: '#008FFB',
|
color: '#008FFB',
|
||||||
data: format(total
|
data: format(total
|
||||||
? raw.notes.total
|
? raw.notes.total
|
||||||
: sum(raw.notes.inc, negate(raw.notes.dec))
|
: sum(raw.notes.inc, negate(raw.notes.dec)),
|
||||||
)
|
),
|
||||||
}]
|
}],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchInstanceFfChart = async (total: boolean): Promise<typeof chartData> => {
|
const fetchInstanceFfChart = async (total: boolean): Promise<typeof chartData> => {
|
||||||
const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
|
const raw = await os.apiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
|
||||||
return {
|
return {
|
||||||
series: [{
|
series: [{
|
||||||
name: 'Following',
|
name: 'Following',
|
||||||
|
@ -726,22 +691,22 @@ const fetchInstanceFfChart = async (total: boolean): Promise<typeof chartData> =
|
||||||
color: '#008FFB',
|
color: '#008FFB',
|
||||||
data: format(total
|
data: format(total
|
||||||
? raw.following.total
|
? raw.following.total
|
||||||
: sum(raw.following.inc, negate(raw.following.dec))
|
: sum(raw.following.inc, negate(raw.following.dec)),
|
||||||
)
|
),
|
||||||
}, {
|
}, {
|
||||||
name: 'Followers',
|
name: 'Followers',
|
||||||
type: 'area',
|
type: 'area',
|
||||||
color: '#00E396',
|
color: '#00E396',
|
||||||
data: format(total
|
data: format(total
|
||||||
? raw.followers.total
|
? raw.followers.total
|
||||||
: sum(raw.followers.inc, negate(raw.followers.dec))
|
: sum(raw.followers.inc, negate(raw.followers.dec)),
|
||||||
)
|
),
|
||||||
}]
|
}],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof chartData> => {
|
const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof chartData> => {
|
||||||
const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
|
const raw = await os.apiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
|
||||||
return {
|
return {
|
||||||
bytes: true,
|
bytes: true,
|
||||||
series: [{
|
series: [{
|
||||||
|
@ -750,14 +715,14 @@ const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof char
|
||||||
color: '#008FFB',
|
color: '#008FFB',
|
||||||
data: format(total
|
data: format(total
|
||||||
? raw.drive.totalUsage
|
? raw.drive.totalUsage
|
||||||
: sum(raw.drive.incUsage, negate(raw.drive.decUsage))
|
: sum(raw.drive.incUsage, negate(raw.drive.decUsage)),
|
||||||
)
|
),
|
||||||
}]
|
}],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof chartData> => {
|
const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof chartData> => {
|
||||||
const raw = await os.api('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
|
const raw = await os.apiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
|
||||||
return {
|
return {
|
||||||
series: [{
|
series: [{
|
||||||
name: 'Drive files',
|
name: 'Drive files',
|
||||||
|
@ -765,14 +730,14 @@ const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof char
|
||||||
color: '#008FFB',
|
color: '#008FFB',
|
||||||
data: format(total
|
data: format(total
|
||||||
? raw.drive.totalFiles
|
? raw.drive.totalFiles
|
||||||
: sum(raw.drive.incFiles, negate(raw.drive.decFiles))
|
: sum(raw.drive.incFiles, negate(raw.drive.decFiles)),
|
||||||
)
|
),
|
||||||
}]
|
}],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchPerUserNotesChart = async (): Promise<typeof chartData> => {
|
const fetchPerUserNotesChart = async (): Promise<typeof chartData> => {
|
||||||
const raw = await os.api('charts/user/notes', { userId: props.args.user.id, limit: props.limit, span: props.span });
|
const raw = await os.apiGet('charts/user/notes', { userId: props.args.user.id, limit: props.limit, span: props.span });
|
||||||
return {
|
return {
|
||||||
series: [...(props.args.withoutAll ? [] : [{
|
series: [...(props.args.withoutAll ? [] : [{
|
||||||
name: 'All',
|
name: 'All',
|
||||||
|
@ -804,7 +769,7 @@ const fetchPerUserNotesChart = async (): Promise<typeof chartData> => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchPerUserFollowingChart = async (): Promise<typeof chartData> => {
|
const fetchPerUserFollowingChart = async (): Promise<typeof chartData> => {
|
||||||
const raw = await os.api('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span });
|
const raw = await os.apiGet('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span });
|
||||||
return {
|
return {
|
||||||
series: [{
|
series: [{
|
||||||
name: 'Local',
|
name: 'Local',
|
||||||
|
@ -819,7 +784,7 @@ const fetchPerUserFollowingChart = async (): Promise<typeof chartData> => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchPerUserFollowersChart = async (): Promise<typeof chartData> => {
|
const fetchPerUserFollowersChart = async (): Promise<typeof chartData> => {
|
||||||
const raw = await os.api('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span });
|
const raw = await os.apiGet('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span });
|
||||||
return {
|
return {
|
||||||
series: [{
|
series: [{
|
||||||
name: 'Local',
|
name: 'Local',
|
||||||
|
@ -834,7 +799,7 @@ const fetchPerUserFollowersChart = async (): Promise<typeof chartData> => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchPerUserDriveChart = async (): Promise<typeof chartData> => {
|
const fetchPerUserDriveChart = async (): Promise<typeof chartData> => {
|
||||||
const raw = await os.api('charts/user/drive', { userId: props.args.user.id, limit: props.limit, span: props.span });
|
const raw = await os.apiGet('charts/user/drive', { userId: props.args.user.id, limit: props.limit, span: props.span });
|
||||||
return {
|
return {
|
||||||
series: [{
|
series: [{
|
||||||
name: 'Inc',
|
name: 'Inc',
|
||||||
|
@ -891,10 +856,6 @@ watch(() => [props.src, props.span], fetchAndRender);
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchAndRender();
|
fetchAndRender();
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (disposeTooltipComponent) disposeTooltipComponent();
|
|
||||||
});
|
|
||||||
/* eslint-enable id-denylist */
|
/* eslint-enable id-denylist */
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -57,7 +57,7 @@ const isThumbnailAvailable = computed(() => {
|
||||||
.zdjebgpv {
|
.zdjebgpv {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
background: #e1e1e1;
|
background: var(--panel);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
|
|
||||||
|
|
118
packages/client/src/components/file-list-for-admin.vue
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<MkPagination v-slot="{items}" :pagination="pagination" class="urempief" :class="{ grid: viewMode === 'grid' }">
|
||||||
|
<MkA
|
||||||
|
v-for="file in items"
|
||||||
|
:key="file.id"
|
||||||
|
v-tooltip.mfm="`${file.type}\n${bytes(file.size)}\n${new Date(file.createdAt).toLocaleString()}\nby ${file.user ? '@' + Acct.toString(file.user) : 'system'}`"
|
||||||
|
:to="`/admin/file/${file.id}`"
|
||||||
|
class="file _button"
|
||||||
|
>
|
||||||
|
<div v-if="file.isSensitive" class="sensitive-label">{{ i18n.ts.sensitive }}</div>
|
||||||
|
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
|
||||||
|
<div v-if="viewMode === 'list'" class="body">
|
||||||
|
<div>
|
||||||
|
<small style="opacity: 0.7;">{{ file.name }}</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<MkAcct v-if="file.user" :user="file.user"/>
|
||||||
|
<div v-else>{{ i18n.ts.system }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span style="margin-right: 1em;">{{ file.type }}</span>
|
||||||
|
<span>{{ bytes(file.size) }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>{{ i18n.ts.registeredDate }}: <MkTime :time="file.createdAt" mode="detail"/></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MkA>
|
||||||
|
</MkPagination>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import * as Acct from 'misskey-js/built/acct';
|
||||||
|
import MkSwitch from '@/components/ui/switch.vue';
|
||||||
|
import MkPagination from '@/components/ui/pagination.vue';
|
||||||
|
import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue';
|
||||||
|
import bytes from '@/filters/bytes';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
pagination: any;
|
||||||
|
viewMode: 'grid' | 'list';
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@keyframes sensitive-blink {
|
||||||
|
0% { opacity: 1; }
|
||||||
|
50% { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.urempief {
|
||||||
|
margin-top: var(--margin);
|
||||||
|
|
||||||
|
&.list {
|
||||||
|
> .file {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
text-align: left;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
> .thumbnail {
|
||||||
|
width: 128px;
|
||||||
|
height: 128px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .body {
|
||||||
|
margin-left: 0.3em;
|
||||||
|
padding: 8px;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
||||||
|
grid-gap: 12px;
|
||||||
|
margin: var(--margin) 0;
|
||||||
|
|
||||||
|
> .file {
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
|
||||||
|
> .thumbnail {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .sensitive-label {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
top: 8px;
|
||||||
|
left: 8px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
background: #ff0000bf;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 85%;
|
||||||
|
animation: sensitive-blink 1s infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
143
packages/client/src/components/form/checkbox.vue
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="ziffeoms"
|
||||||
|
:class="{ disabled, checked }"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref="input"
|
||||||
|
type="checkbox"
|
||||||
|
:disabled="disabled"
|
||||||
|
@keydown.enter="toggle"
|
||||||
|
>
|
||||||
|
<span ref="button" v-adaptive-border v-tooltip="checked ? $ts.itsOn : $ts.itsOff" class="button" @click.prevent="toggle">
|
||||||
|
<i class="check fas fa-check"></i>
|
||||||
|
</span>
|
||||||
|
<span class="label">
|
||||||
|
<!-- TODO: 無名slotの方は廃止 -->
|
||||||
|
<span @click="toggle"><slot name="label"></slot><slot></slot></span>
|
||||||
|
<p class="caption"><slot name="caption"></slot></p>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { toRefs, Ref } from 'vue';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import Ripple from '@/components/ripple.vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean | Ref<boolean>;
|
||||||
|
disabled?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'update:modelValue', v: boolean): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
let button = $ref<HTMLElement>();
|
||||||
|
const checked = toRefs(props).modelValue;
|
||||||
|
const toggle = () => {
|
||||||
|
if (props.disabled) return;
|
||||||
|
emit('update:modelValue', !checked.value);
|
||||||
|
|
||||||
|
if (!checked.value) {
|
||||||
|
const rect = button.getBoundingClientRect();
|
||||||
|
const x = rect.left + (button.offsetWidth / 2);
|
||||||
|
const y = rect.top + (button.offsetHeight / 2);
|
||||||
|
os.popup(Ripple, { x, y, particle: false }, {}, 'end');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.ziffeoms {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
> input {
|
||||||
|
position: absolute;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .button {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 23px;
|
||||||
|
height: 23px;
|
||||||
|
outline: none;
|
||||||
|
background: var(--panel);
|
||||||
|
border: solid 1px var(--panel);
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: inherit;
|
||||||
|
|
||||||
|
> .check {
|
||||||
|
margin: auto;
|
||||||
|
opacity: 0;
|
||||||
|
color: var(--fgOnAccent);
|
||||||
|
font-size: 13px;
|
||||||
|
transform: scale(0.5);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
> .button {
|
||||||
|
border-color: var(--inputBorderHover) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .label {
|
||||||
|
margin-left: 12px;
|
||||||
|
margin-top: 2px;
|
||||||
|
display: block;
|
||||||
|
transition: inherit;
|
||||||
|
color: var(--fg);
|
||||||
|
|
||||||
|
> span {
|
||||||
|
display: block;
|
||||||
|
line-height: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .caption {
|
||||||
|
margin: 8px 0 0 0;
|
||||||
|
color: var(--fgTransparentWeak);
|
||||||
|
font-size: 0.85em;
|
||||||
|
|
||||||
|
&:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.checked {
|
||||||
|
> .button {
|
||||||
|
background-color: var(--accent) !important;
|
||||||
|
border-color: var(--accent) !important;
|
||||||
|
|
||||||
|
> .check {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -9,13 +9,13 @@
|
||||||
<i v-else class="fas fa-angle-down icon"></i>
|
<i v-else class="fas fa-angle-down icon"></i>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<keep-alive>
|
<KeepAlive>
|
||||||
<div v-if="openedAtLeastOnce" v-show="opened" class="body">
|
<div v-if="openedAtLeastOnce" v-show="opened" class="body">
|
||||||
<MkSpacer :margin-min="14" :margin-max="22">
|
<MkSpacer :margin-min="14" :margin-max="22">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
</div>
|
</div>
|
||||||
</keep-alive>
|
</KeepAlive>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
<div class="label" @click="focus"><slot name="label"></slot></div>
|
<div class="label" @click="focus"><slot name="label"></slot></div>
|
||||||
<div class="input" :class="{ inline, disabled, focused }">
|
<div class="input" :class="{ inline, disabled, focused }">
|
||||||
<div ref="prefixEl" class="prefix"><slot name="prefix"></slot></div>
|
<div ref="prefixEl" class="prefix"><slot name="prefix"></slot></div>
|
||||||
<input ref="inputEl"
|
<input
|
||||||
|
ref="inputEl"
|
||||||
v-model="v"
|
v-model="v"
|
||||||
v-adaptive-border
|
v-adaptive-border
|
||||||
:type="type"
|
:type="type"
|
||||||
|
@ -32,176 +33,118 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
|
import { onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
|
||||||
import MkButton from '@/components/ui/button.vue';
|
|
||||||
import { debounce } from 'throttle-debounce';
|
import { debounce } from 'throttle-debounce';
|
||||||
|
import MkButton from '@/components/ui/button.vue';
|
||||||
|
import { useInterval } from '@/scripts/use-interval';
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
components: {
|
modelValue: string | number;
|
||||||
MkButton,
|
type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time';
|
||||||
},
|
required?: boolean;
|
||||||
|
readonly?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
pattern?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
autofocus?: boolean;
|
||||||
|
autocomplete?: boolean;
|
||||||
|
spellcheck?: boolean;
|
||||||
|
step?: any;
|
||||||
|
datalist?: string[];
|
||||||
|
inline?: boolean;
|
||||||
|
debounce?: boolean;
|
||||||
|
manualSave?: boolean;
|
||||||
|
small?: boolean;
|
||||||
|
large?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
props: {
|
const emit = defineEmits<{
|
||||||
modelValue: {
|
(ev: 'change', _ev: KeyboardEvent): void;
|
||||||
required: true
|
(ev: 'keydown', _ev: KeyboardEvent): void;
|
||||||
},
|
(ev: 'enter'): void;
|
||||||
type: {
|
(ev: 'update:modelValue', value: string | number): void;
|
||||||
type: String,
|
}>();
|
||||||
required: false
|
|
||||||
},
|
|
||||||
required: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
readonly: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
disabled: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
pattern: {
|
|
||||||
type: String,
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
placeholder: {
|
|
||||||
type: String,
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
autofocus: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
autocomplete: {
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
spellcheck: {
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
step: {
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
datalist: {
|
|
||||||
type: Array,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
inline: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
debounce: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
manualSave: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
emits: ['change', 'keydown', 'enter', 'update:modelValue'],
|
const { modelValue, type, autofocus } = toRefs(props);
|
||||||
|
const v = ref(modelValue.value);
|
||||||
|
const id = Math.random().toString(); // TODO: uuid?
|
||||||
|
const focused = ref(false);
|
||||||
|
const changed = ref(false);
|
||||||
|
const invalid = ref(false);
|
||||||
|
const filled = computed(() => v.value !== '' && v.value != null);
|
||||||
|
const inputEl = ref<HTMLElement>();
|
||||||
|
const prefixEl = ref<HTMLElement>();
|
||||||
|
const suffixEl = ref<HTMLElement>();
|
||||||
|
const height =
|
||||||
|
props.small ? 38 :
|
||||||
|
props.large ? 42 :
|
||||||
|
40;
|
||||||
|
|
||||||
setup(props, context) {
|
const focus = () => inputEl.value.focus();
|
||||||
const { modelValue, type, autofocus } = toRefs(props);
|
const onInput = (ev: KeyboardEvent) => {
|
||||||
const v = ref(modelValue.value);
|
changed.value = true;
|
||||||
const id = Math.random().toString(); // TODO: uuid?
|
emit('change', ev);
|
||||||
const focused = ref(false);
|
};
|
||||||
const changed = ref(false);
|
const onKeydown = (ev: KeyboardEvent) => {
|
||||||
const invalid = ref(false);
|
emit('keydown', ev);
|
||||||
const filled = computed(() => v.value !== '' && v.value != null);
|
|
||||||
const inputEl = ref<HTMLElement>();
|
|
||||||
const prefixEl = ref<HTMLElement>();
|
|
||||||
const suffixEl = ref<HTMLElement>();
|
|
||||||
|
|
||||||
const focus = () => inputEl.value.focus();
|
if (ev.code === 'Enter') {
|
||||||
const onInput = (ev) => {
|
emit('enter');
|
||||||
changed.value = true;
|
}
|
||||||
context.emit('change', ev);
|
};
|
||||||
};
|
|
||||||
const onKeydown = (ev: KeyboardEvent) => {
|
|
||||||
context.emit('keydown', ev);
|
|
||||||
|
|
||||||
if (ev.code === 'Enter') {
|
const updated = () => {
|
||||||
context.emit('enter');
|
changed.value = false;
|
||||||
}
|
if (type.value === 'number') {
|
||||||
};
|
emit('update:modelValue', parseFloat(v.value));
|
||||||
|
} else {
|
||||||
|
emit('update:modelValue', v.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const updated = () => {
|
const debouncedUpdated = debounce(1000, updated);
|
||||||
changed.value = false;
|
|
||||||
if (type?.value === 'number') {
|
|
||||||
context.emit('update:modelValue', parseFloat(v.value));
|
|
||||||
} else {
|
|
||||||
context.emit('update:modelValue', v.value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const debouncedUpdated = debounce(1000, updated);
|
watch(modelValue, newValue => {
|
||||||
|
v.value = newValue;
|
||||||
|
});
|
||||||
|
|
||||||
watch(modelValue, newValue => {
|
watch(v, newValue => {
|
||||||
v.value = newValue;
|
if (!props.manualSave) {
|
||||||
});
|
if (props.debounce) {
|
||||||
|
debouncedUpdated();
|
||||||
|
} else {
|
||||||
|
updated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watch(v, newValue => {
|
invalid.value = inputEl.value.validity.badInput;
|
||||||
if (!props.manualSave) {
|
});
|
||||||
if (props.debounce) {
|
|
||||||
debouncedUpdated();
|
|
||||||
} else {
|
|
||||||
updated();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
invalid.value = inputEl.value.validity.badInput;
|
// このコンポーネントが作成された時、非表示状態である場合がある
|
||||||
});
|
// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
|
||||||
|
useInterval(() => {
|
||||||
|
if (prefixEl.value) {
|
||||||
|
if (prefixEl.value.offsetWidth) {
|
||||||
|
inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (suffixEl.value) {
|
||||||
|
if (suffixEl.value.offsetWidth) {
|
||||||
|
inputEl.value.style.paddingRight = suffixEl.value.offsetWidth + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 100, {
|
||||||
|
immediate: true,
|
||||||
|
afterMounted: true,
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (autofocus.value) {
|
if (autofocus.value) {
|
||||||
focus();
|
focus();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
// このコンポーネントが作成された時、非表示状態である場合がある
|
|
||||||
// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
|
|
||||||
const clock = window.setInterval(() => {
|
|
||||||
if (prefixEl.value) {
|
|
||||||
if (prefixEl.value.offsetWidth) {
|
|
||||||
inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (suffixEl.value) {
|
|
||||||
if (suffixEl.value.offsetWidth) {
|
|
||||||
inputEl.value.style.paddingRight = suffixEl.value.offsetWidth + 'px';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.clearInterval(clock);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
v,
|
|
||||||
focused,
|
|
||||||
invalid,
|
|
||||||
changed,
|
|
||||||
filled,
|
|
||||||
inputEl,
|
|
||||||
prefixEl,
|
|
||||||
suffixEl,
|
|
||||||
focus,
|
|
||||||
onInput,
|
|
||||||
onKeydown,
|
|
||||||
updated,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -228,14 +171,13 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
> .input {
|
> .input {
|
||||||
$height: 42px;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
> input {
|
> input {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
display: block;
|
display: block;
|
||||||
height: $height;
|
height: v-bind("height + 'px'");
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
|
@ -265,7 +207,7 @@ export default defineComponent({
|
||||||
top: 0;
|
top: 0;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
height: $height;
|
height: v-bind("height + 'px'");
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
&:empty {
|
&:empty {
|
||||||
|
|
|
@ -7,7 +7,8 @@
|
||||||
:aria-disabled="disabled"
|
:aria-disabled="disabled"
|
||||||
@click="toggle"
|
@click="toggle"
|
||||||
>
|
>
|
||||||
<input type="radio"
|
<input
|
||||||
|
type="radio"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
>
|
>
|
||||||
<span class="button">
|
<span class="button">
|
||||||
|
@ -23,27 +24,27 @@ import { defineComponent } from 'vue';
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
required: false
|
required: false,
|
||||||
},
|
},
|
||||||
value: {
|
value: {
|
||||||
required: false
|
required: false,
|
||||||
},
|
},
|
||||||
disabled: {
|
disabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
checked(): boolean {
|
checked(): boolean {
|
||||||
return this.modelValue === this.value;
|
return this.modelValue === this.value;
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggle() {
|
toggle() {
|
||||||
if (this.disabled) return;
|
if (this.disabled) return;
|
||||||
this.$emit('update:modelValue', this.value);
|
this.$emit('update:modelValue', this.value);
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -53,7 +54,8 @@ export default defineComponent({
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 10px 12px;
|
padding: 9px 12px;
|
||||||
|
min-width: 60px;
|
||||||
background-color: var(--panel);
|
background-color: var(--panel);
|
||||||
background-clip: padding-box !important;
|
background-clip: padding-box !important;
|
||||||
border: solid 1px var(--panel);
|
border: solid 1px var(--panel);
|
||||||
|
|
|
@ -4,11 +4,11 @@ import MkRadio from './radio.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
MkRadio
|
MkRadio,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
required: false
|
required: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
@ -19,7 +19,7 @@ export default defineComponent({
|
||||||
watch: {
|
watch: {
|
||||||
value() {
|
value() {
|
||||||
this.$emit('update:modelValue', this.value);
|
this.$emit('update:modelValue', this.value);
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
render() {
|
render() {
|
||||||
let options = this.$slots.default();
|
let options = this.$slots.default();
|
||||||
|
@ -30,25 +30,25 @@ export default defineComponent({
|
||||||
if (options.length === 1 && options[0].props == null) options = options[0].children;
|
if (options.length === 1 && options[0].props == null) options = options[0].children;
|
||||||
|
|
||||||
return h('div', {
|
return h('div', {
|
||||||
class: 'novjtcto'
|
class: 'novjtcto',
|
||||||
}, [
|
}, [
|
||||||
...(label ? [h('div', {
|
...(label ? [h('div', {
|
||||||
class: 'label'
|
class: 'label',
|
||||||
}, [label])] : []),
|
}, [label])] : []),
|
||||||
h('div', {
|
h('div', {
|
||||||
class: 'body'
|
class: 'body',
|
||||||
}, options.map(option => h(MkRadio, {
|
}, options.map(option => h(MkRadio, {
|
||||||
key: option.key,
|
key: option.key,
|
||||||
value: option.props.value,
|
value: option.props.value,
|
||||||
modelValue: this.value,
|
modelValue: this.value,
|
||||||
'onUpdate:modelValue': value => this.value = value,
|
'onUpdate:modelValue': value => this.value = value,
|
||||||
}, option.children)),
|
}, option.children)),
|
||||||
),
|
),
|
||||||
...(caption ? [h('div', {
|
...(caption ? [h('div', {
|
||||||
class: 'caption'
|
class: 'caption',
|
||||||
}, [caption])] : []),
|
}, [caption])] : []),
|
||||||
]);
|
]);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -65,9 +65,9 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
> .body {
|
> .body {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
gap: 12px;
|
||||||
grid-gap: 12px;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .caption {
|
> .caption {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="timctyfi" :class="{ disabled }">
|
<div class="timctyfi" :class="{ disabled }">
|
||||||
<div class="label"><slot name="label"></slot></div>
|
<div class="label"><slot name="label"></slot></div>
|
||||||
<div v-panel class="body">
|
<div v-adaptive-border class="body">
|
||||||
<div ref="containerEl" class="container">
|
<div ref="containerEl" class="container">
|
||||||
<div class="track">
|
<div class="track">
|
||||||
<div class="highlight" :style="{ width: (steppedValue * 100) + '%' }"></div>
|
<div class="highlight" :style="{ width: (steppedValue * 100) + '%' }"></div>
|
||||||
|
@ -24,31 +24,31 @@ export default defineComponent({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: false,
|
required: false,
|
||||||
default: 0
|
default: 0,
|
||||||
},
|
},
|
||||||
disabled: {
|
disabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false,
|
||||||
},
|
},
|
||||||
min: {
|
min: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: false,
|
required: false,
|
||||||
default: 0
|
default: 0,
|
||||||
},
|
},
|
||||||
max: {
|
max: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: false,
|
required: false,
|
||||||
default: 100
|
default: 100,
|
||||||
},
|
},
|
||||||
step: {
|
step: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: false,
|
required: false,
|
||||||
default: 1
|
default: 1,
|
||||||
},
|
},
|
||||||
autofocus: {
|
autofocus: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false
|
required: false,
|
||||||
},
|
},
|
||||||
textConverter: {
|
textConverter: {
|
||||||
type: Function,
|
type: Function,
|
||||||
|
@ -90,14 +90,18 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
watch([steppedValue, containerEl], calcThumbPosition);
|
watch([steppedValue, containerEl], calcThumbPosition);
|
||||||
|
|
||||||
|
let ro: ResizeObserver | undefined;
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const ro = new ResizeObserver((entries, observer) => {
|
ro = new ResizeObserver((entries, observer) => {
|
||||||
calcThumbPosition();
|
calcThumbPosition();
|
||||||
});
|
});
|
||||||
ro.observe(containerEl.value);
|
ro.observe(containerEl.value);
|
||||||
onUnmounted(() => {
|
});
|
||||||
ro.disconnect();
|
|
||||||
});
|
onUnmounted(() => {
|
||||||
|
if (ro) ro.disconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
const steps = computed(() => {
|
const steps = computed(() => {
|
||||||
|
@ -191,7 +195,9 @@ export default defineComponent({
|
||||||
$thumbWidth: 20px;
|
$thumbWidth: 20px;
|
||||||
|
|
||||||
> .body {
|
> .body {
|
||||||
padding: 12px;
|
padding: 10px 12px;
|
||||||
|
background: var(--panel);
|
||||||
|
border: solid 1px var(--panel);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|
||||||
> .container {
|
> .container {
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
<div class="label" @click="focus"><slot name="label"></slot></div>
|
<div class="label" @click="focus"><slot name="label"></slot></div>
|
||||||
<div ref="container" class="input" :class="{ inline, disabled, focused }" @click.prevent="onClick">
|
<div ref="container" class="input" :class="{ inline, disabled, focused }" @click.prevent="onClick">
|
||||||
<div ref="prefixEl" class="prefix"><slot name="prefix"></slot></div>
|
<div ref="prefixEl" class="prefix"><slot name="prefix"></slot></div>
|
||||||
<select ref="inputEl"
|
<select
|
||||||
|
ref="inputEl"
|
||||||
v-model="v"
|
v-model="v"
|
||||||
v-adaptive-border
|
v-adaptive-border
|
||||||
class="select"
|
class="select"
|
||||||
|
@ -25,178 +26,139 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs, VNode } from 'vue';
|
import { onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs, VNode, useSlots } from 'vue';
|
||||||
import MkButton from '@/components/ui/button.vue';
|
import MkButton from '@/components/ui/button.vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
|
import { useInterval } from '@/scripts/use-interval';
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
components: {
|
modelValue: string;
|
||||||
MkButton,
|
required?: boolean;
|
||||||
},
|
readonly?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
autofocus?: boolean;
|
||||||
|
inline?: boolean;
|
||||||
|
manualSave?: boolean;
|
||||||
|
small?: boolean;
|
||||||
|
large?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
props: {
|
const emit = defineEmits<{
|
||||||
modelValue: {
|
(ev: 'change', _ev: KeyboardEvent): void;
|
||||||
required: true
|
(ev: 'update:modelValue', value: string): void;
|
||||||
},
|
}>();
|
||||||
required: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
readonly: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
disabled: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
placeholder: {
|
|
||||||
type: String,
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
autofocus: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
inline: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
manualSave: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
emits: ['change', 'update:modelValue'],
|
const slots = useSlots();
|
||||||
|
|
||||||
setup(props, context) {
|
const { modelValue, autofocus } = toRefs(props);
|
||||||
const { modelValue, autofocus } = toRefs(props);
|
const v = ref(modelValue.value);
|
||||||
const v = ref(modelValue.value);
|
const focused = ref(false);
|
||||||
const focused = ref(false);
|
const changed = ref(false);
|
||||||
const changed = ref(false);
|
const invalid = ref(false);
|
||||||
const invalid = ref(false);
|
const filled = computed(() => v.value !== '' && v.value != null);
|
||||||
const filled = computed(() => v.value !== '' && v.value != null);
|
const inputEl = ref(null);
|
||||||
const inputEl = ref(null);
|
const prefixEl = ref(null);
|
||||||
const prefixEl = ref(null);
|
const suffixEl = ref(null);
|
||||||
const suffixEl = ref(null);
|
const container = ref(null);
|
||||||
const container = ref(null);
|
const height =
|
||||||
|
props.small ? 38 :
|
||||||
|
props.large ? 42 :
|
||||||
|
40;
|
||||||
|
|
||||||
const focus = () => inputEl.value.focus();
|
const focus = () => inputEl.value.focus();
|
||||||
const onInput = (ev) => {
|
const onInput = (ev) => {
|
||||||
changed.value = true;
|
changed.value = true;
|
||||||
context.emit('change', ev);
|
emit('change', ev);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updated = () => {
|
const updated = () => {
|
||||||
changed.value = false;
|
changed.value = false;
|
||||||
context.emit('update:modelValue', v.value);
|
emit('update:modelValue', v.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(modelValue, newValue => {
|
watch(modelValue, newValue => {
|
||||||
v.value = newValue;
|
v.value = newValue;
|
||||||
});
|
|
||||||
|
|
||||||
watch(v, newValue => {
|
|
||||||
if (!props.manualSave) {
|
|
||||||
updated();
|
|
||||||
}
|
|
||||||
|
|
||||||
invalid.value = inputEl.value.validity.badInput;
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
nextTick(() => {
|
|
||||||
if (autofocus.value) {
|
|
||||||
focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
// このコンポーネントが作成された時、非表示状態である場合がある
|
|
||||||
// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
|
|
||||||
const clock = window.setInterval(() => {
|
|
||||||
if (prefixEl.value) {
|
|
||||||
if (prefixEl.value.offsetWidth) {
|
|
||||||
inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (suffixEl.value) {
|
|
||||||
if (suffixEl.value.offsetWidth) {
|
|
||||||
inputEl.value.style.paddingRight = suffixEl.value.offsetWidth + 'px';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.clearInterval(clock);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const onClick = (ev: MouseEvent) => {
|
|
||||||
focused.value = true;
|
|
||||||
|
|
||||||
const menu = [];
|
|
||||||
let options = context.slots.default();
|
|
||||||
|
|
||||||
const pushOption = (option: VNode) => {
|
|
||||||
menu.push({
|
|
||||||
text: option.children,
|
|
||||||
active: v.value === option.props.value,
|
|
||||||
action: () => {
|
|
||||||
v.value = option.props.value;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const scanOptions = (options: VNode[]) => {
|
|
||||||
for (const vnode of options) {
|
|
||||||
if (vnode.type === 'optgroup') {
|
|
||||||
const optgroup = vnode;
|
|
||||||
menu.push({
|
|
||||||
type: 'label',
|
|
||||||
text: optgroup.props.label,
|
|
||||||
});
|
|
||||||
scanOptions(optgroup.children);
|
|
||||||
} else if (Array.isArray(vnode.children)) { // 何故かフラグメントになってくることがある
|
|
||||||
const fragment = vnode;
|
|
||||||
scanOptions(fragment.children);
|
|
||||||
} else {
|
|
||||||
const option = vnode;
|
|
||||||
pushOption(option);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
scanOptions(options);
|
|
||||||
|
|
||||||
os.popupMenu(menu, container.value, {
|
|
||||||
width: container.value.offsetWidth,
|
|
||||||
}).then(() => {
|
|
||||||
focused.value = false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
v,
|
|
||||||
focused,
|
|
||||||
invalid,
|
|
||||||
changed,
|
|
||||||
filled,
|
|
||||||
inputEl,
|
|
||||||
prefixEl,
|
|
||||||
suffixEl,
|
|
||||||
container,
|
|
||||||
focus,
|
|
||||||
onInput,
|
|
||||||
onClick,
|
|
||||||
updated,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(v, newValue => {
|
||||||
|
if (!props.manualSave) {
|
||||||
|
updated();
|
||||||
|
}
|
||||||
|
|
||||||
|
invalid.value = inputEl.value.validity.badInput;
|
||||||
|
});
|
||||||
|
|
||||||
|
// このコンポーネントが作成された時、非表示状態である場合がある
|
||||||
|
// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
|
||||||
|
useInterval(() => {
|
||||||
|
if (prefixEl.value) {
|
||||||
|
if (prefixEl.value.offsetWidth) {
|
||||||
|
inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (suffixEl.value) {
|
||||||
|
if (suffixEl.value.offsetWidth) {
|
||||||
|
inputEl.value.style.paddingRight = suffixEl.value.offsetWidth + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 100, {
|
||||||
|
immediate: true,
|
||||||
|
afterMounted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
if (autofocus.value) {
|
||||||
|
focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const onClick = (ev: MouseEvent) => {
|
||||||
|
focused.value = true;
|
||||||
|
|
||||||
|
const menu = [];
|
||||||
|
let options = slots.default!();
|
||||||
|
|
||||||
|
const pushOption = (option: VNode) => {
|
||||||
|
menu.push({
|
||||||
|
text: option.children,
|
||||||
|
active: v.value === option.props.value,
|
||||||
|
action: () => {
|
||||||
|
v.value = option.props.value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const scanOptions = (options: VNode[]) => {
|
||||||
|
for (const vnode of options) {
|
||||||
|
if (vnode.type === 'optgroup') {
|
||||||
|
const optgroup = vnode;
|
||||||
|
menu.push({
|
||||||
|
type: 'label',
|
||||||
|
text: optgroup.props.label,
|
||||||
|
});
|
||||||
|
scanOptions(optgroup.children);
|
||||||
|
} else if (Array.isArray(vnode.children)) { // 何故かフラグメントになってくることがある
|
||||||
|
const fragment = vnode;
|
||||||
|
scanOptions(fragment.children);
|
||||||
|
} else {
|
||||||
|
const option = vnode;
|
||||||
|
pushOption(option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
scanOptions(options);
|
||||||
|
|
||||||
|
os.popupMenu(menu, container.value, {
|
||||||
|
width: container.value.offsetWidth,
|
||||||
|
}).then(() => {
|
||||||
|
focused.value = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -222,7 +184,6 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
> .input {
|
> .input {
|
||||||
$height: 42px;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
@ -236,7 +197,7 @@ export default defineComponent({
|
||||||
appearance: none;
|
appearance: none;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
display: block;
|
display: block;
|
||||||
height: $height;
|
height: v-bind("height + 'px'");
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
|
@ -264,7 +225,7 @@ export default defineComponent({
|
||||||
top: 0;
|
top: 0;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
height: $height;
|
height: v-bind("height + 'px'");
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
&:empty {
|
&:empty {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="ziffeoms"
|
class="ziffeomt"
|
||||||
:class="{ disabled, checked }"
|
:class="{ disabled, checked }"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
@ -9,8 +9,8 @@
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@keydown.enter="toggle"
|
@keydown.enter="toggle"
|
||||||
>
|
>
|
||||||
<span ref="button" v-adaptive-border v-tooltip="checked ? $ts.itsOn : $ts.itsOff" class="button" @click.prevent="toggle">
|
<span ref="button" v-tooltip="checked ? $ts.itsOn : $ts.itsOff" class="button" @click.prevent="toggle">
|
||||||
<i class="check fas fa-check"></i>
|
<div class="knob"></div>
|
||||||
</span>
|
</span>
|
||||||
<span class="label">
|
<span class="label">
|
||||||
<!-- TODO: 無名slotの方は廃止 -->
|
<!-- TODO: 無名slotの方は廃止 -->
|
||||||
|
@ -23,7 +23,6 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { toRefs, Ref } from 'vue';
|
import { toRefs, Ref } from 'vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import Ripple from '@/components/ripple.vue';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: boolean | Ref<boolean>;
|
modelValue: boolean | Ref<boolean>;
|
||||||
|
@ -41,16 +40,13 @@ const toggle = () => {
|
||||||
emit('update:modelValue', !checked.value);
|
emit('update:modelValue', !checked.value);
|
||||||
|
|
||||||
if (!checked.value) {
|
if (!checked.value) {
|
||||||
const rect = button.getBoundingClientRect();
|
|
||||||
const x = rect.left + (button.offsetWidth / 2);
|
|
||||||
const y = rect.top + (button.offsetHeight / 2);
|
|
||||||
os.popup(Ripple, { x, y, particle: false }, {}, 'end');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.ziffeoms {
|
.ziffeomt {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
@ -73,21 +69,25 @@ const toggle = () => {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 23px;
|
width: 32px;
|
||||||
height: 23px;
|
height: 23px;
|
||||||
outline: none;
|
outline: none;
|
||||||
background: var(--panel);
|
background: var(--swutchOffBg);
|
||||||
border: solid 1px var(--panel);
|
background-clip: content-box;
|
||||||
border-radius: 4px;
|
border: solid 1px var(--swutchOffBg);
|
||||||
|
border-radius: 999px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: inherit;
|
transition: inherit;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
> .check {
|
> .knob {
|
||||||
margin: auto;
|
position: absolute;
|
||||||
opacity: 0;
|
top: 3px;
|
||||||
color: var(--fgOnAccent);
|
left: 3px;
|
||||||
font-size: 13px;
|
width: 15px;
|
||||||
transform: scale(0.5);
|
height: 15px;
|
||||||
|
background: var(--swutchOffFg);
|
||||||
|
border-radius: 999px;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -130,12 +130,12 @@ const toggle = () => {
|
||||||
|
|
||||||
&.checked {
|
&.checked {
|
||||||
> .button {
|
> .button {
|
||||||
background-color: var(--accent) !important;
|
background-color: var(--swutchOnBg) !important;
|
||||||
border-color: var(--accent) !important;
|
border-color: var(--swutchOnBg) !important;
|
||||||
|
|
||||||
> .check {
|
> .knob {
|
||||||
opacity: 1;
|
left: 12px;
|
||||||
transform: scale(1);
|
background: var(--swutchOnFg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|