Compare commits
16 Commits
992e75f1ce
...
b83d07da28
| Author | SHA1 | Date |
|---|---|---|
|
|
b83d07da28 | |
|
|
90b058e226 | |
|
|
c2383fac16 | |
|
|
b449f3e7a8 | |
|
|
32f5949935 | |
|
|
4c3935bf80 | |
|
|
dc1a91a5b2 | |
|
|
fd70a700f8 | |
|
|
d8dc10829c | |
|
|
b1efc298f7 | |
|
|
98462ccbaf | |
|
|
ca00a08e6e | |
|
|
54c6fb762a | |
|
|
22d966e92d | |
|
|
bf303238f0 | |
|
|
b0eae49eaa |
|
|
@ -114,6 +114,7 @@ redis:
|
|||
|
||||
# Available methods:
|
||||
# aid ... Short, Millisecond accuracy
|
||||
# aidx ... Millisecond accuracy
|
||||
# meid ... Similar to ObjectID, Millisecond accuracy
|
||||
# ulid ... Millisecond accuracy
|
||||
# objectid ... This is left for backward compatibility
|
||||
|
|
@ -121,7 +122,7 @@ redis:
|
|||
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
||||
# ID SETTINGS AFTER THAT!
|
||||
|
||||
id: 'aid'
|
||||
id: 'aidx'
|
||||
|
||||
# ┌─────────────────────┐
|
||||
#───┘ Other configuration └─────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@ redis:
|
|||
|
||||
# Available methods:
|
||||
# aid ... Short, Millisecond accuracy
|
||||
# aidx ... Millisecond accuracy
|
||||
# meid ... Similar to ObjectID, Millisecond accuracy
|
||||
# ulid ... Millisecond accuracy
|
||||
# objectid ... This is left for backward compatibility
|
||||
|
|
@ -132,7 +133,7 @@ redis:
|
|||
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
||||
# ID SETTINGS AFTER THAT!
|
||||
|
||||
id: 'aid'
|
||||
id: 'aidx'
|
||||
|
||||
# ┌─────────────────────┐
|
||||
#───┘ Other configuration └─────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ redis:
|
|||
|
||||
# Available methods:
|
||||
# aid ... Short, Millisecond accuracy
|
||||
# aidx ... Millisecond accuracy
|
||||
# meid ... Similar to ObjectID, Millisecond accuracy
|
||||
# ulid ... Millisecond accuracy
|
||||
# objectid ... This is left for backward compatibility
|
||||
|
|
@ -121,7 +122,7 @@ redis:
|
|||
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
||||
# ID SETTINGS AFTER THAT!
|
||||
|
||||
id: 'aid'
|
||||
id: 'aidx'
|
||||
|
||||
# ┌─────────────────────┐
|
||||
#───┘ Other configuration └─────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -12,4 +12,4 @@ db:
|
|||
redis:
|
||||
host: 127.0.0.1
|
||||
port: 56312
|
||||
id: aid
|
||||
id: aidx
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ jobs:
|
|||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.x]
|
||||
node-version: [20.5.1]
|
||||
|
||||
services:
|
||||
postgres:
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ jobs:
|
|||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.x]
|
||||
node-version: [20.5.1]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.0.0
|
||||
|
|
@ -51,7 +51,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: [20.x]
|
||||
node-version: [20.5.1]
|
||||
browser: [chrome]
|
||||
|
||||
services:
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.x]
|
||||
node-version: [20.5.1]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.x]
|
||||
node-version: [20.5.1]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.0.0
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
- お知らせのバナー表示やダイアログ表示が可能に
|
||||
- お知らせのアイコンを設定可能に
|
||||
- チャンネルをセンシティブ指定できるようになりました
|
||||
- センシティブチャンネルのNoteのReNoteはデフォルトでHome TLに流れるようになりました
|
||||
- 二要素認証のバックアップコードが生成されるようになりました ref. https://github.com/MisskeyIO/misskey/pull/121
|
||||
|
||||
### Client
|
||||
|
|
@ -35,6 +36,8 @@
|
|||
- Enhance: ノート検索にローカルのみ検索可能なオプションの追加
|
||||
- Enhance: AiScriptで`LOCALE`として現在の設定言語を取得できるように
|
||||
- Enhance: Renote自体を通報できるように
|
||||
- Enhance: データセーバーモードの強化
|
||||
- Enhance: Renoteを管理者権限で削除可能に
|
||||
- `$[rainbow ]`記法が、動きのあるMFMが無効になっていても使用できるようになりました
|
||||
- Playの操作を行うAPI TokenをAPIコンソールから発行できるように
|
||||
- Fix: サーバー情報画面(`/instance-info/{domain}`)でブロックができないのを修正
|
||||
|
|
@ -50,6 +53,8 @@
|
|||
- cacheRemoteFilesの初期値はfalseになりました
|
||||
- ファイルアップロード時等にファイル名の拡張子を修正する関数(correctFilename)の挙動を改善
|
||||
- Webhookのペイロードにサーバーのurlが含まれるようになりました
|
||||
- Webhook設定でsecretを空に出来るように
|
||||
- 使われていないアンテナの自動停止を設定可能に
|
||||
- Fix: 一部のfeatured noteを照会できない問題を修正
|
||||
- Fix: muteがapiからのuser list timeline取得で機能しない問題を修正
|
||||
- Fix: ジョブキュー管理画面の認証を回避できる問題を修正
|
||||
|
|
|
|||
|
|
@ -135,6 +135,7 @@ redis:
|
|||
|
||||
# Available methods:
|
||||
# aid ... Short, Millisecond accuracy
|
||||
# aidx ... Millisecond accuracy
|
||||
# meid ... Similar to ObjectID, Millisecond accuracy
|
||||
# ulid ... Millisecond accuracy
|
||||
# objectid ... This is left for backward compatibility
|
||||
|
|
@ -142,7 +143,7 @@ redis:
|
|||
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
||||
# ID SETTINGS AFTER THAT!
|
||||
|
||||
id: "aid"
|
||||
id: "aidx"
|
||||
# ┌─────────────────────┐
|
||||
#───┘ Other configuration └─────────────────────────────────────
|
||||
|
||||
|
|
|
|||
65
gulpfile.mjs
65
gulpfile.mjs
|
|
@ -1,65 +0,0 @@
|
|||
/**
|
||||
* Gulp tasks
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import gulp from 'gulp';
|
||||
import replace from 'gulp-replace';
|
||||
import terser from 'gulp-terser';
|
||||
import cssnano from 'gulp-cssnano';
|
||||
|
||||
import locales from './locales/index.js';
|
||||
import meta from './package.json' assert { type: "json" };
|
||||
|
||||
gulp.task('copy:backend:views', () =>
|
||||
gulp.src('./packages/backend/src/server/web/views/**/*').pipe(gulp.dest('./packages/backend/built/server/web/views'))
|
||||
);
|
||||
|
||||
gulp.task('copy:frontend:fonts', () =>
|
||||
gulp.src('./packages/frontend/node_modules/three/examples/fonts/**/*').pipe(gulp.dest('./built/_frontend_dist_/fonts/'))
|
||||
);
|
||||
|
||||
gulp.task('copy:frontend:tabler-icons', () =>
|
||||
gulp.src('./packages/frontend/node_modules/@tabler/icons-webfont/**/*').pipe(gulp.dest('./built/_frontend_dist_/tabler-icons/'))
|
||||
);
|
||||
|
||||
gulp.task('copy:frontend:locales', cb => {
|
||||
fs.mkdirSync('./built/_frontend_dist_/locales', { recursive: true });
|
||||
|
||||
const v = { '_version_': meta.version };
|
||||
|
||||
for (const [lang, locale] of Object.entries(locales)) {
|
||||
fs.writeFileSync(`./built/_frontend_dist_/locales/${lang}.${meta.version}.json`, JSON.stringify({ ...locale, ...v }), 'utf-8');
|
||||
}
|
||||
|
||||
cb();
|
||||
});
|
||||
|
||||
gulp.task('build:backend:script', () => {
|
||||
return gulp.src(['./packages/backend/src/server/web/boot.js', './packages/backend/src/server/web/bios.js', './packages/backend/src/server/web/cli.js'])
|
||||
.pipe(replace('LANGS', JSON.stringify(Object.keys(locales))))
|
||||
.pipe(terser({
|
||||
toplevel: true
|
||||
}))
|
||||
.pipe(gulp.dest('./packages/backend/built/server/web/'));
|
||||
});
|
||||
|
||||
gulp.task('build:backend:style', () => {
|
||||
return gulp.src(['./packages/backend/src/server/web/style.css', './packages/backend/src/server/web/bios.css', './packages/backend/src/server/web/cli.css', './packages/backend/src/server/web/error.css'])
|
||||
.pipe(cssnano({
|
||||
zindex: false
|
||||
}))
|
||||
.pipe(gulp.dest('./packages/backend/built/server/web/'));
|
||||
});
|
||||
|
||||
gulp.task('build', gulp.parallel(
|
||||
'copy:frontend:locales', 'copy:backend:views', 'build:backend:script', 'build:backend:style', 'copy:frontend:fonts', 'copy:frontend:tabler-icons'
|
||||
));
|
||||
|
||||
gulp.task('default', gulp.task('build'));
|
||||
|
||||
gulp.task('watch', () => {
|
||||
gulp.watch([
|
||||
'./packages/*/src/**/*',
|
||||
], { ignoreInitial: false }, gulp.task('build'));
|
||||
});
|
||||
|
|
@ -45,6 +45,7 @@ pin: "置頂"
|
|||
unpin: "取消置頂"
|
||||
copyContent: "複製內容"
|
||||
copyLink: "複製連結"
|
||||
copyLinkRenote: "複製轉貼連結"
|
||||
delete: "刪除"
|
||||
deleteAndEdit: "刪除並編輯"
|
||||
deleteAndEditConfirm: "要刪除並再次編輯嗎?此貼文的所有反應、轉發和回覆也將會消失。"
|
||||
|
|
@ -655,6 +656,7 @@ behavior: "行為"
|
|||
sample: "範例"
|
||||
abuseReports: "檢舉"
|
||||
reportAbuse: "檢舉"
|
||||
reportAbuseRenote: "檢舉轉貼"
|
||||
reportAbuseOf: "檢舉{name}"
|
||||
fillAbuseReportDescription: "請填寫檢舉的詳細理由。如有需要,請附上相關 URL。"
|
||||
abuseReported: "檢舉完成。感謝您的報告。"
|
||||
|
|
|
|||
19
package.json
19
package.json
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"version": "2023.9.0-beta.3",
|
||||
"version": "2023.9.0-beta.4",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/misskey-dev/misskey.git"
|
||||
},
|
||||
"packageManager": "pnpm@8.7.1",
|
||||
"packageManager": "pnpm@8.7.4",
|
||||
"workspaces": [
|
||||
"packages/frontend",
|
||||
"packages/backend",
|
||||
|
|
@ -15,7 +15,8 @@
|
|||
"private": true,
|
||||
"scripts": {
|
||||
"build-pre": "node ./scripts/build-pre.js",
|
||||
"build": "pnpm build-pre && pnpm -r build && pnpm gulp",
|
||||
"build-assets": "node ./scripts/build-assets.mjs",
|
||||
"build": "pnpm build-pre && pnpm -r build && pnpm build-assets",
|
||||
"build-storybook": "pnpm --filter frontend build-storybook",
|
||||
"start": "pnpm check:connect && cd packages/backend && node ./built/boot/index.js",
|
||||
"start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/index.js",
|
||||
|
|
@ -23,7 +24,6 @@
|
|||
"migrate": "cd packages/backend && pnpm migrate",
|
||||
"check:connect": "cd packages/backend && pnpm check:connect",
|
||||
"migrateandstart": "pnpm migrate && pnpm start",
|
||||
"gulp": "pnpm exec gulp build",
|
||||
"watch": "pnpm dev",
|
||||
"dev": "node ./scripts/dev.mjs",
|
||||
"lint": "pnpm -r lint",
|
||||
|
|
@ -34,7 +34,6 @@
|
|||
"jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage",
|
||||
"test": "pnpm -r test",
|
||||
"test-and-coverage": "pnpm -r test-and-coverage",
|
||||
"format": "pnpm exec gulp format",
|
||||
"clean": "node ./scripts/clean.js",
|
||||
"clean-all": "node ./scripts/clean-all.js",
|
||||
"cleanall": "pnpm clean-all"
|
||||
|
|
@ -45,17 +44,13 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"execa": "8.0.1",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-cssnano": "2.1.3",
|
||||
"gulp-rename": "2.0.0",
|
||||
"gulp-replace": "1.1.4",
|
||||
"gulp-terser": "2.1.0",
|
||||
"cssnano": "6.0.1",
|
||||
"js-yaml": "4.1.0",
|
||||
"postcss": "8.4.27",
|
||||
"terser": "5.19.2",
|
||||
"typescript": "5.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/gulp": "4.0.13",
|
||||
"@types/gulp-rename": "2.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "6.6.0",
|
||||
"@typescript-eslint/parser": "6.6.0",
|
||||
"cross-env": "7.0.3",
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@
|
|||
"mime-types": "2.1.35",
|
||||
"misskey-js": "workspace:*",
|
||||
"ms": "3.0.0-canary.1",
|
||||
"nanoid": "4.0.2",
|
||||
"nested-property": "4.0.0",
|
||||
"node-fetch": "3.3.2",
|
||||
"nodemailer": "6.9.4",
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ type Source = {
|
|||
|
||||
perChannelMaxNoteCacheCount?: number;
|
||||
perUserNotificationsMaxCount?: number;
|
||||
deactivateAntennaThreshold?: number;
|
||||
};
|
||||
|
||||
export type Config = {
|
||||
|
|
@ -161,6 +162,7 @@ export type Config = {
|
|||
redisForJobQueue: RedisOptions & RedisOptionsSource;
|
||||
perChannelMaxNoteCacheCount: number;
|
||||
perUserNotificationsMaxCount: number;
|
||||
deactivateAntennaThreshold: number;
|
||||
};
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
|
|
@ -252,6 +254,7 @@ export function loadConfig(): Config {
|
|||
clientManifestExists: clientManifestExists,
|
||||
perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000,
|
||||
perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 300,
|
||||
deactivateAntennaThreshold: config.deactivateAntennaThreshold ?? (1000 * 60 * 60 * 24 * 7),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { ulid } from 'ulid';
|
|||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { genAid, parseAid } from '@/misc/id/aid.js';
|
||||
import { genAidx, parseAidx } from '@/misc/id/aidx.js';
|
||||
import { genMeid, parseMeid } from '@/misc/id/meid.js';
|
||||
import { genMeidg, parseMeidg } from '@/misc/id/meidg.js';
|
||||
import { genObjectId, parseObjectId } from '@/misc/id/object-id.js';
|
||||
|
|
@ -31,6 +32,7 @@ export class IdService {
|
|||
|
||||
switch (this.method) {
|
||||
case 'aid': return genAid(date);
|
||||
case 'aidx': return genAidx(date);
|
||||
case 'meid': return genMeid(date);
|
||||
case 'meidg': return genMeidg(date);
|
||||
case 'ulid': return ulid(date.getTime());
|
||||
|
|
@ -43,6 +45,7 @@ export class IdService {
|
|||
public parse(id: string): { date: Date; } {
|
||||
switch (this.method) {
|
||||
case 'aid': return parseAid(id);
|
||||
case 'aidx': return parseAidx(id);
|
||||
case 'objectid': return parseObjectId(id);
|
||||
case 'meid': return parseMeid(id);
|
||||
case 'meidg': return parseMeidg(id);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
// AIDX
|
||||
// 長さ8の[2000年1月1日からの経過ミリ秒をbase36でエンコードしたもの] + 長さ4の[個体ID] + 長さ4の[カウンタ]
|
||||
// (c) mei23
|
||||
// https://misskey.m544.net/notes/71899acdcc9859ec5708ac24
|
||||
|
||||
import { customAlphabet } from 'nanoid';
|
||||
|
||||
export const aidxRegExp = /^[0-9a-z]{16}$/;
|
||||
|
||||
const TIME2000 = 946684800000;
|
||||
const TIME_LENGTH = 8;
|
||||
const NODE_LENGTH = 4;
|
||||
const NOISE_LENGTH = 4;
|
||||
|
||||
const nodeId = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', NODE_LENGTH)();
|
||||
let counter = 0;
|
||||
|
||||
function getTime(time: number): string {
|
||||
time = time - TIME2000;
|
||||
if (time < 0) time = 0;
|
||||
|
||||
return time.toString(36).padStart(TIME_LENGTH, '0').slice(-TIME_LENGTH);
|
||||
}
|
||||
|
||||
function getNoise(): string {
|
||||
return counter.toString(36).padStart(NOISE_LENGTH, '0').slice(-NOISE_LENGTH);
|
||||
}
|
||||
|
||||
export function genAidx(date: Date): string {
|
||||
const t = date.getTime();
|
||||
if (isNaN(t)) throw new Error('Failed to create AIDX: Invalid Date');
|
||||
counter++;
|
||||
return getTime(t) + nodeId + getNoise();
|
||||
}
|
||||
|
||||
export function parseAidx(id: string): { date: Date; } {
|
||||
const time = parseInt(id.slice(0, TIME_LENGTH), 36) + TIME2000;
|
||||
return { date: new Date(time) };
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import type { AntennasRepository, MutedNotesRepository, RoleAssignmentsRepositor
|
|||
import type Logger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
|
||||
|
|
@ -18,6 +19,9 @@ export class CleanProcessorService {
|
|||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.userIpsRepository)
|
||||
private userIpsRepository: UserIpsRepository,
|
||||
|
||||
|
|
@ -54,12 +58,14 @@ export class CleanProcessorService {
|
|||
reason: 'word',
|
||||
});
|
||||
|
||||
// 7日以上使われてないアンテナを停止
|
||||
this.antennasRepository.update({
|
||||
lastUsedAt: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 7))),
|
||||
}, {
|
||||
isActive: false,
|
||||
});
|
||||
// 使われてないアンテナを停止
|
||||
if (this.config.deactivateAntennaThreshold > 0) {
|
||||
this.antennasRepository.update({
|
||||
lastUsedAt: LessThan(new Date(Date.now() - this.config.deactivateAntennaThreshold)),
|
||||
}, {
|
||||
isActive: false,
|
||||
});
|
||||
}
|
||||
|
||||
const expiredRoleAssignments = await this.roleAssignmentsRepository.createQueryBuilder('assign')
|
||||
.where('assign.expiresAt IS NOT NULL')
|
||||
|
|
|
|||
|
|
@ -34,12 +34,12 @@ export const paramDef = {
|
|||
properties: {
|
||||
name: { type: 'string', minLength: 1, maxLength: 100 },
|
||||
url: { type: 'string', minLength: 1, maxLength: 1024 },
|
||||
secret: { type: 'string', minLength: 1, maxLength: 1024 },
|
||||
secret: { type: 'string', maxLength: 1024, default: '' },
|
||||
on: { type: 'array', items: {
|
||||
type: 'string', enum: webhookEventTypes,
|
||||
} },
|
||||
},
|
||||
required: ['name', 'url', 'secret', 'on'],
|
||||
required: ['name', 'url', 'on'],
|
||||
} as const;
|
||||
|
||||
// TODO: ロジックをサービスに切り出す
|
||||
|
|
|
|||
|
|
@ -34,13 +34,13 @@ export const paramDef = {
|
|||
webhookId: { type: 'string', format: 'misskey:id' },
|
||||
name: { type: 'string', minLength: 1, maxLength: 100 },
|
||||
url: { type: 'string', minLength: 1, maxLength: 1024 },
|
||||
secret: { type: 'string', minLength: 1, maxLength: 1024 },
|
||||
secret: { type: 'string', maxLength: 1024, default: '' },
|
||||
on: { type: 'array', items: {
|
||||
type: 'string', enum: webhookEventTypes,
|
||||
} },
|
||||
active: { type: 'boolean' },
|
||||
},
|
||||
required: ['webhookId', 'name', 'url', 'secret', 'on', 'active'],
|
||||
required: ['webhookId', 'name', 'url', 'on', 'active'],
|
||||
} as const;
|
||||
|
||||
// TODO: ロジックをサービスに切り出す
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { GlobalModule } from '@/GlobalModule.js';
|
|||
import { AnnouncementService } from '@/core/AnnouncementService.js';
|
||||
import type { MiAnnouncement, AnnouncementsRepository, AnnouncementReadsRepository, UsersRepository, MiUser } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { genAid } from '@/misc/id/aid.js';
|
||||
import { genAidx } from '@/misc/id/aidx.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
|
|
@ -33,7 +33,7 @@ describe('AnnouncementService', () => {
|
|||
function createUser(data: Partial<MiUser> = {}) {
|
||||
const un = secureRndstr(16);
|
||||
return usersRepository.insert({
|
||||
id: genAid(new Date()),
|
||||
id: genAidx(new Date()),
|
||||
createdAt: new Date(),
|
||||
username: un,
|
||||
usernameLower: un,
|
||||
|
|
@ -44,7 +44,7 @@ describe('AnnouncementService', () => {
|
|||
|
||||
function createAnnouncement(data: Partial<MiAnnouncement> = {}) {
|
||||
return announcementsRepository.insert({
|
||||
id: genAid(new Date()),
|
||||
id: genAidx(new Date()),
|
||||
createdAt: new Date(),
|
||||
updatedAt: null,
|
||||
title: 'Title',
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { RoleService } from '@/core/RoleService.js';
|
|||
import type { MiRole, RolesRepository, RoleAssignmentsRepository, UsersRepository, MiUser } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { genAid } from '@/misc/id/aid.js';
|
||||
import { genAidx } from '@/misc/id/aidx.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
|
|
@ -37,7 +37,7 @@ describe('RoleService', () => {
|
|||
function createUser(data: Partial<MiUser> = {}) {
|
||||
const un = secureRndstr(16);
|
||||
return usersRepository.insert({
|
||||
id: genAid(new Date()),
|
||||
id: genAidx(new Date()),
|
||||
createdAt: new Date(),
|
||||
username: un,
|
||||
usernameLower: un,
|
||||
|
|
@ -48,7 +48,7 @@ describe('RoleService', () => {
|
|||
|
||||
function createRole(data: Partial<MiRole> = {}) {
|
||||
return rolesRepository.insert({
|
||||
id: genAid(new Date()),
|
||||
id: genAidx(new Date()),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
lastUsedAt: new Date(),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import { ulid } from 'ulid';
|
||||
import { describe, test, expect } from '@jest/globals';
|
||||
import { aidRegExp, genAid, parseAid } from '@/misc/id/aid.js';
|
||||
import { aidxRegExp, genAidx, parseAidx } from '@/misc/id/aidx.js';
|
||||
import { genMeid, meidRegExp, parseMeid } from '@/misc/id/meid.js';
|
||||
import { genMeidg, meidgRegExp, parseMeidg } from '@/misc/id/meidg.js';
|
||||
import { genObjectId, objectIdRegExp, parseObjectId } from '@/misc/id/object-id.js';
|
||||
|
|
@ -19,6 +20,13 @@ describe('misc:id', () => {
|
|||
expect(parseAid(gotAid).date.getTime()).toBe(date.getTime());
|
||||
});
|
||||
|
||||
test('aidx', () => {
|
||||
const date = new Date();
|
||||
const gotAidx = genAidx(date);
|
||||
expect(gotAidx).toMatch(aidxRegExp);
|
||||
expect(parseAidx(gotAidx).date.getTime()).toBe(date.getTime());
|
||||
});
|
||||
|
||||
test('meid', () => {
|
||||
const date = new Date();
|
||||
const gotMeid = genMeid(date);
|
||||
|
|
|
|||
|
|
@ -98,8 +98,6 @@
|
|||
"@testing-library/vue": "7.0.0",
|
||||
"@types/escape-regexp": "0.0.1",
|
||||
"@types/estree": "1.0.1",
|
||||
"@types/gulp": "4.0.13",
|
||||
"@types/gulp-rename": "2.0.2",
|
||||
"@types/matter-js": "0.19.0",
|
||||
"@types/micromatch": "4.0.2",
|
||||
"@types/node": "20.5.9",
|
||||
|
|
|
|||
|
|
@ -4,34 +4,41 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div :class="hide ? $style.hidden : $style.visible" :style="darkMode ? '--c: rgb(255 255 255 / 2%);' : '--c: rgb(0 0 0 / 2%);'" @click="onclick">
|
||||
<a
|
||||
:class="$style.imageContainer"
|
||||
:href="image.url"
|
||||
:title="image.name"
|
||||
<div :class="hide ? $style.hidden : $style.visible" :style="darkMode ? '--c: rgb(255 255 255 / 2%);' : '--c: rgb(0 0 0 / 2%);'" @click.stop="onclick">
|
||||
<component
|
||||
:is="disableImageLink ? 'div' : 'a'"
|
||||
v-bind="disableImageLink ? {
|
||||
title: image.name,
|
||||
class: $style.imageContainer,
|
||||
} : {
|
||||
title: image.name,
|
||||
class: $style.imageContainer,
|
||||
href: image.url,
|
||||
style: 'cursor: zoom-in;'
|
||||
}"
|
||||
>
|
||||
<ImgWithBlurhash
|
||||
:hash="image.blurhash"
|
||||
:src="(defaultStore.state.enableDataSaverMode && hide) ? null : url"
|
||||
:forceBlurhash="hide"
|
||||
:cover="hide"
|
||||
:cover="hide || cover"
|
||||
:alt="image.comment || image.name"
|
||||
:title="image.comment || image.name"
|
||||
:width="image.properties.width"
|
||||
:height="image.properties.height"
|
||||
:style="hide ? 'filter: brightness(0.5);' : null"
|
||||
/>
|
||||
</a>
|
||||
</component>
|
||||
<template v-if="hide">
|
||||
<div :class="$style.hiddenText">
|
||||
<div :class="$style.hiddenTextWrapper">
|
||||
<b v-if="image.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b>
|
||||
<b v-else style="display: block;"><i class="ti ti-photo"></i> {{ defaultStore.state.enableDataSaverMode && image.size ? bytes(image.size) : i18n.ts.image }}</b>
|
||||
<span style="display: block;">{{ i18n.ts.clickToShow }}</span>
|
||||
<span v-if="controls" style="display: block;">{{ i18n.ts.clickToShow }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-else-if="controls">
|
||||
<div :class="$style.indicators">
|
||||
<div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div>
|
||||
<div v-if="image.comment" :class="$style.indicator">ALT</div>
|
||||
|
|
@ -54,10 +61,17 @@ import { i18n } from '@/i18n';
|
|||
import * as os from '@/os';
|
||||
import { iAmModerator } from '@/account';
|
||||
|
||||
const props = defineProps<{
|
||||
const props = withDefaults(defineProps<{
|
||||
image: Misskey.entities.DriveFile;
|
||||
raw?: boolean;
|
||||
}>();
|
||||
cover?: boolean;
|
||||
disableImageLink?: boolean;
|
||||
controls?: boolean;
|
||||
}>(), {
|
||||
cover: false,
|
||||
disableImageLink: false,
|
||||
controls: true,
|
||||
});
|
||||
|
||||
let hide = $ref(true);
|
||||
let darkMode: boolean = $ref(defaultStore.state.darkMode);
|
||||
|
|
@ -70,6 +84,9 @@ const url = $computed(() => (props.raw || defaultStore.state.loadRawImages)
|
|||
);
|
||||
|
||||
function onclick() {
|
||||
if (!props.controls) {
|
||||
return;
|
||||
}
|
||||
if (hide) {
|
||||
hide = false;
|
||||
}
|
||||
|
|
@ -167,7 +184,6 @@ function showMenu(ev: MouseEvent) {
|
|||
|
||||
.imageContainer {
|
||||
display: block;
|
||||
cursor: zoom-in;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
|
|||
|
|
@ -319,9 +319,15 @@ function renote(viaKeyboard = false) {
|
|||
const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility;
|
||||
const localOnly = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly;
|
||||
|
||||
let visibility = appearNote.visibility;
|
||||
visibility = smallerVisibility(visibility, configuredVisibility);
|
||||
if (appearNote.channel?.isSensitive) {
|
||||
visibility = smallerVisibility(visibility, 'home');
|
||||
}
|
||||
|
||||
os.api('notes/create', {
|
||||
localOnly,
|
||||
visibility: smallerVisibility(appearNote.visibility, configuredVisibility),
|
||||
visibility,
|
||||
renoteId: appearNote.id,
|
||||
}).then(() => {
|
||||
os.toast(i18n.ts.renoted);
|
||||
|
|
@ -425,22 +431,26 @@ async function clip() {
|
|||
}
|
||||
|
||||
function showRenoteMenu(viaKeyboard = false): void {
|
||||
function getUnrenote(): MenuItem {
|
||||
return {
|
||||
text: i18n.ts.unrenote,
|
||||
icon: 'ti ti-trash',
|
||||
danger: true,
|
||||
action: () => {
|
||||
os.api('notes/delete', {
|
||||
noteId: note.id,
|
||||
});
|
||||
isDeleted.value = true;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (isMyRenote) {
|
||||
pleaseLogin();
|
||||
os.popupMenu([
|
||||
getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote),
|
||||
null,
|
||||
{
|
||||
text: i18n.ts.unrenote,
|
||||
icon: 'ti ti-trash',
|
||||
danger: true,
|
||||
action: () => {
|
||||
os.api('notes/delete', {
|
||||
noteId: note.id,
|
||||
});
|
||||
isDeleted.value = true;
|
||||
},
|
||||
},
|
||||
getUnrenote(),
|
||||
], renoteTime.value, {
|
||||
viaKeyboard: viaKeyboard,
|
||||
});
|
||||
|
|
@ -449,6 +459,7 @@ function showRenoteMenu(viaKeyboard = false): void {
|
|||
getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote),
|
||||
null,
|
||||
getAbuseNoteMenu(note, i18n.ts.reportAbuseRenote),
|
||||
$i.isModerator || $i.isAdmin ? getUnrenote() : undefined,
|
||||
], renoteTime.value, {
|
||||
viaKeyboard: viaKeyboard,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,7 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<MkA :to="`/@${page.user.username}/pages/${page.name}`" class="vhpxefrj" tabindex="-1">
|
||||
<div v-if="page.eyeCatchingImage" class="thumbnail" :style="`background-image: url('${page.eyeCatchingImage.thumbnailUrl}')`"></div>
|
||||
<div v-if="page.eyeCatchingImage" class="thumbnail">
|
||||
<MediaImage
|
||||
:image="page.eyeCatchingImage"
|
||||
:disableImageLink="true"
|
||||
:controls="false"
|
||||
:cover="true"
|
||||
:class="$style.eyeCatchingImageRoot"
|
||||
/>
|
||||
</div>
|
||||
<article>
|
||||
<header>
|
||||
<h1 :title="page.title">{{ page.title }}</h1>
|
||||
|
|
@ -23,12 +31,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { userName } from '@/filters/user';
|
||||
import MediaImage from '@/components/MkMediaImage.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
page: Misskey.entities.Page;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style module>
|
||||
.eyeCatchingImageRoot {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
border-radius: var(--radius) var(--radius) 0 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vhpxefrj {
|
||||
display: block;
|
||||
|
|
@ -39,32 +57,15 @@ const props = defineProps<{
|
|||
}
|
||||
|
||||
> .thumbnail {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
> button {
|
||||
font-size: 3.5em;
|
||||
opacity: 0.7;
|
||||
|
||||
&:hover {
|
||||
font-size: 4em;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
& + article {
|
||||
left: 100px;
|
||||
width: calc(100% - 100px);
|
||||
border-radius: 0 0 var(--radius) var(--radius);
|
||||
}
|
||||
}
|
||||
|
||||
> article {
|
||||
background-color: var(--panel);
|
||||
padding: 16px;
|
||||
border-radius: var(--radius);
|
||||
|
||||
> header {
|
||||
margin-bottom: 8px;
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
<div v-else>
|
||||
<component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substring(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
|
||||
<div v-if="thumbnail" :class="$style.thumbnail" :style="`background-image: url('${thumbnail}')`">
|
||||
<div v-if="thumbnail" :class="$style.thumbnail" :style="defaultStore.state.enableDataSaverMode ? '' : `background-image: url('${thumbnail}')`">
|
||||
</div>
|
||||
<article :class="$style.body">
|
||||
<header :class="$style.header">
|
||||
|
|
@ -260,6 +260,7 @@ onUnmounted(() => {
|
|||
height: 100%;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-color: var(--bg);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -5,20 +5,24 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<ImgWithBlurhash v-if="image" style="max-width: 100%;" :hash="image.blurhash" :src="image.url" :alt="image.comment" :title="image.comment" :width="image.properties.width" :height="image.properties.height" :cover="false"/>
|
||||
<MediaImage
|
||||
v-if="image"
|
||||
:image="image"
|
||||
:disableImageLink="true"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { ImageBlock } from './block.type';
|
||||
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
|
||||
import MediaImage from '@/components/MkMediaImage.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
block: ImageBlock,
|
||||
page: Misskey.entities.Page,
|
||||
}>();
|
||||
|
||||
const image = props.page.attachedFiles.find(x => x.id === props.block.fileId);
|
||||
const image = ref<Misskey.entities.DriveFile>(props.page.attachedFiles.find(x => x.id === props.block.fileId));
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div :class="{ [$style.center]: page.alignCenter, [$style.serif]: page.font === 'serif' }">
|
||||
<div :class="{ [$style.center]: page.alignCenter, [$style.serif]: page.font === 'serif' }" class="_gaps_s">
|
||||
<XBlock v-for="child in page.content" :key="child.id" :page="page" :block="child" :h="2"/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
-->
|
||||
<div class="banner">
|
||||
<img v-if="page.eyeCatchingImageId" :src="page.eyeCatchingImage.url"/>
|
||||
<MkMediaImage
|
||||
v-if="page.eyeCatchingImageId"
|
||||
:image="page.eyeCatchingImage"
|
||||
:cover="true"
|
||||
:disableImageLink="true"
|
||||
class="thumbnail"
|
||||
/>
|
||||
</div>
|
||||
<div class="content">
|
||||
<XPage :page="page"/>
|
||||
|
|
@ -74,6 +80,7 @@ import XPage from '@/components/page/page.vue';
|
|||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os';
|
||||
import { url } from '@/config';
|
||||
import MkMediaImage from '@/components/MkMediaImage.vue';
|
||||
import MkFollowButton from '@/components/MkFollowButton.vue';
|
||||
import MkContainer from '@/components/MkContainer.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
|
|
@ -204,11 +211,14 @@ definePageMetadata(computed(() => page ? {
|
|||
}
|
||||
|
||||
> .banner {
|
||||
> img {
|
||||
> .thumbnail {
|
||||
// TODO: 良い感じのアスペクト比で表示
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
height: auto;
|
||||
aspect-ratio: 3/1;
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2669
pnpm-lock.yaml
2669
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
import cssnano from 'cssnano';
|
||||
import postcss from 'postcss';
|
||||
import * as terser from 'terser';
|
||||
|
||||
import locales from '../locales/index.js';
|
||||
import meta from '../package.json' assert { type: "json" };
|
||||
|
||||
async function copyFrontendFonts() {
|
||||
await fs.cp('./packages/frontend/node_modules/three/examples/fonts', './built/_frontend_dist_/fonts', { dereference: true, recursive: true });
|
||||
}
|
||||
|
||||
async function copyFrontendTablerIcons() {
|
||||
await fs.cp('./packages/frontend/node_modules/@tabler/icons-webfont', './built/_frontend_dist_/tabler-icons', { dereference: true, recursive: true });
|
||||
}
|
||||
|
||||
async function copyFrontendLocales() {
|
||||
await fs.mkdir('./built/_frontend_dist_/locales', { recursive: true });
|
||||
|
||||
const v = { '_version_': meta.version };
|
||||
|
||||
for (const [lang, locale] of Object.entries(locales)) {
|
||||
await fs.writeFile(`./built/_frontend_dist_/locales/${lang}.${meta.version}.json`, JSON.stringify({ ...locale, ...v }), 'utf-8');
|
||||
}
|
||||
}
|
||||
|
||||
async function copyBackendViews() {
|
||||
await fs.cp('./packages/backend/src/server/web/views', './packages/backend/built/server/web/views', { recursive: true });
|
||||
}
|
||||
|
||||
async function buildBackendScript() {
|
||||
await fs.mkdir('./packages/backend/built/server/web', { recursive: true });
|
||||
|
||||
for (const file of [
|
||||
'./packages/backend/src/server/web/boot.js',
|
||||
'./packages/backend/src/server/web/bios.js',
|
||||
'./packages/backend/src/server/web/cli.js'
|
||||
]) {
|
||||
let source = await fs.readFile(file, { encoding: 'utf-8' });
|
||||
source = source.replaceAll('LANGS', JSON.stringify(Object.keys(locales)));
|
||||
const { code } = await terser.minify(source, { toplevel: true });
|
||||
await fs.writeFile(`./packages/backend/built/server/web/${path.basename(file)}`, code);
|
||||
}
|
||||
}
|
||||
|
||||
async function buildBackendStyle() {
|
||||
await fs.mkdir('./packages/backend/built/server/web', { recursive: true });
|
||||
|
||||
for (const file of [
|
||||
'./packages/backend/src/server/web/style.css',
|
||||
'./packages/backend/src/server/web/bios.css',
|
||||
'./packages/backend/src/server/web/cli.css',
|
||||
'./packages/backend/src/server/web/error.css'
|
||||
]) {
|
||||
const source = await fs.readFile(file, { encoding: 'utf-8' });
|
||||
const { css } = await postcss([cssnano({ zindex: false })]).process(source, { from: undefined });
|
||||
await fs.writeFile(`./packages/backend/built/server/web/${path.basename(file)}`, css);
|
||||
}
|
||||
}
|
||||
|
||||
async function build() {
|
||||
await Promise.all([
|
||||
copyFrontendFonts(),
|
||||
copyFrontendTablerIcons(),
|
||||
copyFrontendLocales(),
|
||||
copyBackendViews(),
|
||||
buildBackendScript(),
|
||||
buildBackendStyle(),
|
||||
]);
|
||||
}
|
||||
|
||||
await build();
|
||||
|
||||
if (process.argv.includes("--watch")) {
|
||||
const watcher = fs.watch('./packages', { recursive: true });
|
||||
for await (const event of watcher) {
|
||||
if (/^[a-z]+\/src/.test(event.filename)) {
|
||||
await build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,7 +23,13 @@ await execa('pnpm', ['build-pre'], {
|
|||
stderr: process.stderr,
|
||||
});
|
||||
|
||||
execa('pnpm', ['exec', 'gulp', 'watch'], {
|
||||
await execa('pnpm', ['build-assets'], {
|
||||
cwd: _dirname + '/../',
|
||||
stdout: process.stdout,
|
||||
stderr: process.stderr,
|
||||
});
|
||||
|
||||
execa('pnpm', ['build-assets', '--watch'], {
|
||||
cwd: _dirname + '/../',
|
||||
stdout: process.stdout,
|
||||
stderr: process.stderr,
|
||||
|
|
|
|||
Loading…
Reference in New Issue