diff --git a/.config/ci.yml b/.config/ci.yml index c381d21d921d9d345fe74dfee7ae8a2390fa102f..44092d36623b4ff32fec95a83473623eccac2e2f 100644 --- a/.config/ci.yml +++ b/.config/ci.yml @@ -106,7 +106,7 @@ redis: # ┌───────────────────────────┠#───┘ MeiliSearch configuration └───────────────────────────── -# You can set scope to local (default value) or global +# You can set scope to local (default value) or global # (include notes from remote). #meilisearch: @@ -198,13 +198,18 @@ proxyRemoteFiles: true # https://example.com/thumbnail.webp?thumbnail=1&url=https%3A%2F%2Fstorage.example.com%2Fpath%2Fto%2Fvideo.mp4 #videoThumbnailGenerator: https://example.com -# Sign to ActivityPub GET request (default: true) +# Sign outgoing ActivityPub GET request (default: true) signToActivityPubGet: true +# Sign outgoing ActivityPub Activities (default: true) +# Linked Data signatures are cryptographic signatures attached to each activity to provide proof of authenticity. +# When using authorized fetch, this is often undesired as any signed activity can be forwarded to a blocked instance by relays and other instances. +# This setting allows admins to disable LD signatures for increased privacy, at the expense of fewer relayed activities and additional inbound fetch (GET) requests. +attachLdSignatureForRelays: true # check that inbound ActivityPub GET requests are signed ("authorized fetch") checkActivityPubGetSignature: false # For security reasons, uploading attachments from the intranet is prohibited, -# but exceptions can be made from the following settings. Default value is "undefined". +# but exceptions can be made from the following settings. Default value is "undefined". # Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)). #allowedPrivateNetworks: [ # '127.0.0.1/32' diff --git a/.config/docker_example.yml b/.config/docker_example.yml index c22bd83c2e0bdd3a51678365ff5377aaf9868244..f4645d672d6b3918aaeb5a138315a3b656893aef 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -270,8 +270,13 @@ proxyRemoteFiles: true # https://example.com/thumbnail.webp?thumbnail=1&url=https%3A%2F%2Fstorage.example.com%2Fpath%2Fto%2Fvideo.mp4 #videoThumbnailGenerator: https://example.com -# Sign to ActivityPub GET request (default: true) +# Sign outgoing ActivityPub GET request (default: true) signToActivityPubGet: true +# Sign outgoing ActivityPub Activities (default: true) +# Linked Data signatures are cryptographic signatures attached to each activity to provide proof of authenticity. +# When using authorized fetch, this is often undesired as any signed activity can be forwarded to a blocked instance by relays and other instances. +# This setting allows admins to disable LD signatures for increased privacy, at the expense of fewer relayed activities and additional inbound fetch (GET) requests. +attachLdSignatureForRelays: true # check that inbound ActivityPub GET requests are signed ("authorized fetch") checkActivityPubGetSignature: false diff --git a/.config/example.yml b/.config/example.yml index ae55b983bb548ed297c6ea495bca83bd2afe3063..21e85b7b8904566136155c4fbed59f6d83d6076a 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -285,8 +285,13 @@ proxyRemoteFiles: true # https://example.com/thumbnail.webp?thumbnail=1&url=https%3A%2F%2Fstorage.example.com%2Fpath%2Fto%2Fvideo.mp4 #videoThumbnailGenerator: https://example.com -# Sign to ActivityPub GET request (default: true) +# Sign outgoing ActivityPub GET request (default: true) signToActivityPubGet: true +# Sign outgoing ActivityPub Activities (default: true) +# Linked Data signatures are cryptographic signatures attached to each activity to provide proof of authenticity. +# When using authorized fetch, this is often undesired as any signed activity can be forwarded to a blocked instance by relays and other instances. +# This setting allows admins to disable LD signatures for increased privacy, at the expense of fewer relayed activities and additional inbound fetch (GET) requests. +attachLdSignatureForRelays: true # check that inbound ActivityPub GET requests are signed ("authorized fetch") checkActivityPubGetSignature: false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2acacd6dfa6c1f3768e278936e9d42dace6a5c83..053fffcaeb360131250f39c6fceab63b67f4b330 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -96,6 +96,15 @@ If your language is not listed in Crowdin, please open an issue.  +## Icon Font (Shark Font) +Sharkey has its own Icon Font called Shark Font which can be found at https://activitypub.software/TransFem-org/shark-font +Build Instructions can all be found over there in the `README`. + +If you have an Icon Suggestion or want to add an Icon please open an issue/merge request over at that repo. + +When Updating the Font make sure to copy **all generated files** from the `dest` folder into `packages/backend/assets/fonts/sharkey-icons` +For the CSS simply copy the file content and replace the old content in `style.css` and for the WOFF, TTF and SVG simply replace them. + ## Development During development, it is useful to use the diff --git a/chart/files/default.yml b/chart/files/default.yml index 2e1381ec574ac1c684823a6fb4d91608d3b3f79f..aab7ed6ce12edd997908bdaafb14d28b8fa30d08 100644 --- a/chart/files/default.yml +++ b/chart/files/default.yml @@ -208,8 +208,13 @@ id: "aidx" # Media Proxy #mediaProxy: https://example.com/proxy -# Sign to ActivityPub GET request (default: true) +# Sign outgoing ActivityPub GET request (default: true) signToActivityPubGet: true +# Sign outgoing ActivityPub Activities (default: true) +# Linked Data signatures are cryptographic signatures attached to each activity to provide proof of authenticity. +# When using authorized fetch, this is often undesired as any signed activity can be forwarded to a blocked instance by relays and other instances. +# This setting allows admins to disable LD signatures for increased privacy, at the expense of fewer relayed activities and additional inbound fetch (GET) requests. +attachLdSignatureForRelays: true # check that inbound ActivityPub GET requests are signed ("authorized fetch") checkActivityPubGetSignature: false diff --git a/locales/en-US.yml b/locales/en-US.yml index 03a5a91e84e4e4e4a0954f7b3a5df8697a11763b..689dc560c08839c108d14b162c73abaeaa7205a6 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1305,8 +1305,8 @@ alwaysConfirmFollow: "Always confirm when following" inquiry: "Contact" _delivery: status: "Delivery status" - stop: "Suspended" - resume: "Delivery resume" + stop: "Suspend delivery" + resume: "Resume delivery" _type: none: "Publishing" manuallySuspended: "Manually suspended" diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 2b04c5abfb0c13459d01f93eda570fd94514658d..be86420e67da967c45ec5401a47fc7c7377ed38d 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -1238,7 +1238,8 @@ keepOriginalFilename: "Mantieni il nome file originale" keepOriginalFilenameDescription: "Disattivandola, i file verranno caricati usando nomi casuali." noDescription: "Manca la descrizione" _delivery: - stop: "Sospensione" + stop: "Sospendi la distribuzione di attività " + resume: "Riprendi la distribuzione di attività " _type: none: "Pubblicazione" _bubbleGame: diff --git a/package.json b/package.json index 519a8c453d2ea307332fe1979e326af80d313c87..f60fd789dbc10774836b0d235019908c5536cc68 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "build": "pnpm build-pre && pnpm -r build && pnpm build-assets", "build-storybook": "pnpm --filter frontend build-storybook", "build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api", - "start": "pnpm check:connect && cd packages/backend && node ./built/boot/entry.js", + "start": "pnpm check:connect && cd packages/backend && MK_WARNED_ABOUT_CONFIG=true node ./built/boot/entry.js", "start:test": "cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js", "init": "pnpm migrate", "migrate": "cd packages/backend && pnpm migrate", diff --git a/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.svg b/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.svg deleted file mode 100644 index 9d21137072a19c2da1fb41a98c9199c57c5d333c..0000000000000000000000000000000000000000 Binary files a/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.svg and /dev/null differ diff --git a/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.ttf b/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.ttf deleted file mode 100644 index a2601e0f1b00c4f2e72249911b911e48c55d6de1..0000000000000000000000000000000000000000 Binary files a/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.ttf and /dev/null differ diff --git a/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.woff b/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.woff deleted file mode 100644 index d9f471fa35a861eb745fb136094ffd9c90b7922f..0000000000000000000000000000000000000000 Binary files a/packages/backend/assets/fonts/sharkey-icons/custom-sharkey-icons.woff and /dev/null differ diff --git a/packages/backend/assets/fonts/sharkey-icons/shark-font.svg b/packages/backend/assets/fonts/sharkey-icons/shark-font.svg new file mode 100644 index 0000000000000000000000000000000000000000..b67bd7f7d8119ae3bd9f3a0ed6557f1ca7a1f64a Binary files /dev/null and b/packages/backend/assets/fonts/sharkey-icons/shark-font.svg differ diff --git a/packages/backend/assets/fonts/sharkey-icons/shark-font.ttf b/packages/backend/assets/fonts/sharkey-icons/shark-font.ttf new file mode 100644 index 0000000000000000000000000000000000000000..0fdf04df082cc5724a7c94b19b299cfd5a317040 Binary files /dev/null and b/packages/backend/assets/fonts/sharkey-icons/shark-font.ttf differ diff --git a/packages/backend/assets/fonts/sharkey-icons/shark-font.woff b/packages/backend/assets/fonts/sharkey-icons/shark-font.woff new file mode 100644 index 0000000000000000000000000000000000000000..993666bc3aacaf202da6002fbc9baa8e7a881581 Binary files /dev/null and b/packages/backend/assets/fonts/sharkey-icons/shark-font.woff differ diff --git a/packages/backend/assets/fonts/sharkey-icons/style.css b/packages/backend/assets/fonts/sharkey-icons/style.css index 7fb0f9450470c649b09732630360c7a0bd521c3c..7168702e5a38a4c7edeee32b4df4728b6c0568de 100644 --- a/packages/backend/assets/fonts/sharkey-icons/style.css +++ b/packages/backend/assets/fonts/sharkey-icons/style.css @@ -1,31 +1,114 @@ -@charset "UTF-8"; - @font-face { - font-family: "custom-sharkey-icons"; - src: url("./custom-sharkey-icons.woff") format("woff"), - url("./custom-sharkey-icons.ttf") format("truetype"), - url("./custom-sharkey-icons.svg#custom-sharkey-icons") format("svg"); - font-weight: normal; + font-display: auto; + font-family: "shark-font"; font-style: normal; - font-display: block; + font-weight: normal; + + src: url("./shark-font.woff?1722899913909") format("woff"), url("./shark-font.ttf?1722899913909") format("truetype"), url("./shark-font.svg?1722899913909#shark-font") format("svg"); } .sk-icons { - font-family: "custom-sharkey-icons" !important; - font-style: normal; + display: inline-block; + font-family: "shark-font"; font-weight: normal; + font-style: normal; font-variant: normal; - text-transform: none; + text-rendering: auto; line-height: 1; - speak: none; - -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; +} + +.sk-icons-lg { + font-size: 1.33333em; + line-height: 0.75em; + vertical-align: -0.0667em; +} + +.sk-icons-xs { + font-size: 0.75em; +} + +.sk-icons-sm { + font-size: 0.875em; +} + +.sk-icons-1x { + font-size: 1em; +} + +.sk-icons-2x { + font-size: 2em; +} + +.sk-icons-3x { + font-size: 3em; +} + +.sk-icons-4x { + font-size: 4em; } -.sk-icons.sk-shark:before { - content: "\61"; +.sk-icons-5x { + font-size: 5em; } -.sk-icons.sk-misskey:before { - content: "\62"; +.sk-icons-6x { + font-size: 6em; } + +.sk-icons-7x { + font-size: 7em; +} + +.sk-icons-8x { + font-size: 8em; +} + +.sk-icons-9x { + font-size: 9em; +} + +.sk-icons-10x { + font-size: 10em; +} + +.sk-icons-fw { + text-align: center; + width: 1.25em; +} + +.sk-icons-border { + border: solid 0.08em #eee; + border-radius: 0.1em; + padding: 0.2em 0.25em 0.15em; +} + +.sk-icons-pull-left { + float: left; +} + +.sk-icons-pull-right { + float: right; +} + +.sk-icons.sk-icons-pull-left { + margin-right: 0.3em; +} + +.sk-icons.sk-icons-pull-right { + margin-left: 0.3em; +} + + +.sk-icons.sk-foldermove::before { + content: "\ea01"; +} + +.sk-icons.sk-misskey::before { + content: "\ea02"; +} + +.sk-icons.sk-shark::before { + content: "\ea03"; +} \ No newline at end of file diff --git a/packages/backend/scripts/check_connect.js b/packages/backend/scripts/check_connect.js index ba25fd416c782cd376cb0e3ba37df6b3656e422f..d4bf4baf43cbf2048eda4db33ece2f82921718ee 100644 --- a/packages/backend/scripts/check_connect.js +++ b/packages/backend/scripts/check_connect.js @@ -5,11 +5,33 @@ import Redis from 'ioredis'; import { loadConfig } from '../built/config.js'; +import { createPostgresDataSource } from '../built/postgres.js'; const config = loadConfig(); -const redis = new Redis(config.redis); -redis.on('connect', () => redis.disconnect()); -redis.on('error', (e) => { - throw e; -}); +// createPostgresDataSource handels primaries and replicas automatically. +// usually, it only opens connections first use, so we force it using +// .initialize() +createPostgresDataSource(config) + .initialize() + .then(c => { c.destroy() }) + .catch(e => { throw e }); + + +// Connect to all redis servers +function connectToRedis(redisOptions) { + const redis = new Redis(redisOptions); + redis.on('connect', () => redis.disconnect()); + redis.on('error', (e) => { + throw e; + }); +} + +// If not all of these are defined, the default one gets reused. +// so we use a Set to only try connecting once to each **uniq** redis. +(new Set([ + config.redis, + config.redisForPubsub, + config.redisForJobQueue, + config.redisForTimelines, +])).forEach(connectToRedis); diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts index 303ba9420764b4cc79b8c5b8df42a3cc477115fb..f757ed64b93b42445ee0fee6bbd72605bcbcff27 100644 --- a/packages/backend/src/boot/master.ts +++ b/packages/backend/src/boot/master.ts @@ -112,6 +112,11 @@ export async function masterMain() { await server(); } + if (config.clusterLimit === 0) { + bootLogger.error("Configuration error: we can't create workers, `config.clusterLimit` is 0 (if you don't want to use clustering, set the environment variable `MK_DISABLE_CLUSTERING` to a non-empty value instead)", null, true); + process.exit(1); + } + await spawnWorkers(config.clusterLimit); } @@ -180,7 +185,10 @@ async function connectDb(): Promise<void> { */ async function spawnWorkers(limit = 1) { - const workers = Math.min(limit, os.cpus().length); + const cpuCount = os.cpus().length; + // in some weird environments, node can't count the CPUs; we trust the config in those cases + const workers = cpuCount === 0 ? limit : Math.min(limit, cpuCount); + bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`); await Promise.all([...Array(workers)].map(spawnWorker)); bootLogger.succ('All workers started'); diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 58c4d028aa3305283c842f70e5d3cbc124e2d52e..9411a7b599347ce91e3be742512139d18600d98b 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -12,9 +12,10 @@ import * as Sentry from '@sentry/node'; import type { RedisOptions } from 'ioredis'; type RedisOptionsSource = Partial<RedisOptions> & { - host: string; - port: number; + host?: string; + port?: number; family?: number; + path?: string, pass: string; db?: number; prefix?: string; @@ -95,6 +96,7 @@ type Source = { customMOTD?: string[]; signToActivityPubGet?: boolean; + attachLdSignatureForRelays?: boolean; checkActivityPubGetSignature?: boolean; perChannelMaxNoteCacheCount?: number; @@ -161,6 +163,7 @@ export type Config = { proxyRemoteFiles: boolean | undefined; customMOTD: string[] | undefined; signToActivityPubGet: boolean; + attachLdSignatureForRelays: boolean; checkActivityPubGetSignature: boolean | undefined; version: string; @@ -221,8 +224,15 @@ export function loadConfig(): Config { JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_vite_/manifest.json`, 'utf-8')) : { 'src/_boot_.ts': { file: 'src/_boot_.ts' } }; - const config = globSync(path).sort() - .map(path => fs.readFileSync(path, 'utf-8')) + const configFiles = globSync(path).sort(); + + if (configFiles.length === 0 + && !process.env['MK_WARNED_ABOUT_CONFIG']) { + console.log('No config files loaded, check if this is intentional'); + process.env['MK_WARNED_ABOUT_CONFIG'] = '1'; + } + + const config = configFiles.map(path => fs.readFileSync(path, 'utf-8')) .map(contents => yaml.load(contents) as Source) .reduce( (acc: Source, cur: Source) => Object.assign(acc, cur), @@ -248,7 +258,7 @@ export function loadConfig(): Config { version, publishTarballInsteadOfProvideRepositoryUrl: !!config.publishTarballInsteadOfProvideRepositoryUrl, url: url.origin, - port: config.port ?? parseInt(process.env.PORT ?? '', 10), + port: config.port ?? parseInt(process.env.PORT ?? '3000', 10), socket: config.socket, chmodSocket: config.chmodSocket, disableHsts: config.disableHsts, @@ -291,6 +301,7 @@ export function loadConfig(): Config { proxyRemoteFiles: config.proxyRemoteFiles, customMOTD: config.customMOTD, signToActivityPubGet: config.signToActivityPubGet ?? true, + attachLdSignatureForRelays: config.attachLdSignatureForRelays ?? true, checkActivityPubGetSignature: config.checkActivityPubGetSignature, mediaProxy: externalMediaProxy ?? internalMediaProxy, externalMediaProxyEnabled: externalMediaProxy !== null && externalMediaProxy !== internalMediaProxy, @@ -436,8 +447,8 @@ function applyEnvOverrides(config: Source) { // these are all the settings that can be overridden - _apply_top([['url', 'port', 'socket', 'chmodSocket', 'disableHsts']]); - _apply_top(['db', ['host', 'port', 'db', 'user', 'pass']]); + _apply_top([['url', 'port', 'socket', 'chmodSocket', 'disableHsts', 'id', 'dbReplications']]); + _apply_top(['db', ['host', 'port', 'db', 'user', 'pass', 'disableCache']]); _apply_top(['dbSlaves', Array.from((config.dbSlaves ?? []).keys()), ['host', 'port', 'db', 'user', 'pass']]); _apply_top([ ['redis', 'redisForPubsub', 'redisForJobQueue', 'redisForTimelines'], @@ -447,7 +458,8 @@ function applyEnvOverrides(config: Source) { _apply_top([['sentryForFrontend', 'sentryForBackend'], 'options', ['dsn', 'profileSampleRate', 'serverName', 'includeLocalVariables', 'proxy', 'keepAlive', 'caCerts']]); _apply_top(['sentryForBackend', 'enableNodeProfiling']); _apply_top([['clusterLimit', 'deliverJobConcurrency', 'inboxJobConcurrency', 'relashionshipJobConcurrency', 'deliverJobPerSec', 'inboxJobPerSec', 'relashionshipJobPerSec', 'deliverJobMaxAttempts', 'inboxJobMaxAttempts']]); - _apply_top([['outgoingAddress', 'outgoingAddressFamily', 'proxy', 'proxySmtp', 'mediaProxy', 'videoThumbnailGenerator']]); + _apply_top([['outgoingAddress', 'outgoingAddressFamily', 'proxy', 'proxySmtp', 'mediaProxy', 'proxyRemoteFiles', 'videoThumbnailGenerator']]); _apply_top([['maxFileSize', 'maxNoteLength', 'pidFile']]); _apply_top(['import', ['downloadTimeout', 'maxFileSize']]); + _apply_top([['signToActivityPubGet', 'checkActivityPubGetSignature']]); } diff --git a/packages/backend/src/core/AvatarDecorationService.ts b/packages/backend/src/core/AvatarDecorationService.ts index 21e31d79a4292dc3a99fa90e71d00ba4c020ba10..fa3f63677eb02fc99dcd109b2c98c84505c77e9a 100644 --- a/packages/backend/src/core/AvatarDecorationService.ts +++ b/packages/backend/src/core/AvatarDecorationService.ts @@ -29,7 +29,7 @@ export class AvatarDecorationService implements OnApplicationShutdown { private moderationLogService: ModerationLogService, private globalEventService: GlobalEventService, ) { - this.cache = new MemorySingleCache<MiAvatarDecoration[]>(1000 * 60 * 30); + this.cache = new MemorySingleCache<MiAvatarDecoration[]>(1000 * 60 * 30); // 30s this.redisForSub.on('message', this.onMessage); } diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts index d008e7ec52a3c6cd4b3fcdee28ef95d793dbb024..6725ebe75b14503583df80ad055a1455c7c91eb0 100644 --- a/packages/backend/src/core/CacheService.ts +++ b/packages/backend/src/core/CacheService.ts @@ -56,10 +56,10 @@ export class CacheService implements OnApplicationShutdown { ) { //this.onMessage = this.onMessage.bind(this); - this.userByIdCache = new MemoryKVCache<MiUser>(Infinity); - this.localUserByNativeTokenCache = new MemoryKVCache<MiLocalUser | null>(Infinity); - this.localUserByIdCache = new MemoryKVCache<MiLocalUser>(Infinity); - this.uriPersonCache = new MemoryKVCache<MiUser | null>(Infinity); + this.userByIdCache = new MemoryKVCache<MiUser>(1000 * 60 * 5); // 5m + this.localUserByNativeTokenCache = new MemoryKVCache<MiLocalUser | null>(1000 * 60 * 5); // 5m + this.localUserByIdCache = new MemoryKVCache<MiLocalUser>(1000 * 60 * 5); // 5m + this.uriPersonCache = new MemoryKVCache<MiUser | null>(1000 * 60 * 5); // 5m this.userProfileCache = new RedisKVCache<MiUserProfile>(this.redisClient, 'userProfile', { lifetime: 1000 * 60 * 30, // 30m @@ -135,14 +135,14 @@ export class CacheService implements OnApplicationShutdown { if (user == null) { this.userByIdCache.delete(body.id); this.localUserByIdCache.delete(body.id); - for (const [k, v] of this.uriPersonCache.cache.entries()) { + for (const [k, v] of this.uriPersonCache.entries) { if (v.value?.id === body.id) { this.uriPersonCache.delete(k); } } } else { this.userByIdCache.set(user.id, user); - for (const [k, v] of this.uriPersonCache.cache.entries()) { + for (const [k, v] of this.uriPersonCache.entries) { if (v.value?.id === user.id) { this.uriPersonCache.set(k, user); } diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index bfbc2b172da65207d7e297c8e12f622c84ba1f41..aa99261549abdd0d8794277eda9c9e7fd0f4d352 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -26,7 +26,7 @@ const parseEmojiStrRegexp = /^([-\w]+)(?:@([\w.-]+))?$/; @Injectable() export class CustomEmojiService implements OnApplicationShutdown { - private cache: MemoryKVCache<MiEmoji | null>; + private emojisCache: MemoryKVCache<MiEmoji | null>; public localEmojisCache: RedisSingleCache<Map<string, MiEmoji>>; constructor( @@ -49,7 +49,7 @@ export class CustomEmojiService implements OnApplicationShutdown { private globalEventService: GlobalEventService, private driveService: DriveService, ) { - this.cache = new MemoryKVCache<MiEmoji | null>(1000 * 60 * 60 * 12); + this.emojisCache = new MemoryKVCache<MiEmoji | null>(1000 * 60 * 60 * 12); // 12h this.localEmojisCache = new RedisSingleCache<Map<string, MiEmoji>>(this.redisClient, 'localEmojis', { lifetime: 1000 * 60 * 30, // 30m @@ -142,6 +142,13 @@ export class CustomEmojiService implements OnApplicationShutdown { this.localEmojisCache.refresh(); + if (data.driveFile != null) { + const file = await this.driveFilesRepository.findOneBy({ url: emoji.originalUrl, userHost: emoji.host ? emoji.host : IsNull() }); + if (file && file.id != data.driveFile.id) { + await this.driveService.deleteFile(file, false, moderator ? moderator : undefined); + } + } + const packed = await this.emojiEntityService.packDetailed(emoji.id); if (emoji.name === data.name) { @@ -350,14 +357,14 @@ export class CustomEmojiService implements OnApplicationShutdown { if (name == null) return null; if (host == null) return null; - const newHost = host === this.config.host ? null : host; + const newHost = host === this.config.host ? null : host; const queryOrNull = async () => (await this.emojisRepository.findOneBy({ name, host: newHost ?? IsNull(), })) ?? null; - const emoji = await this.cache.fetch(`${name} ${host}`, queryOrNull); + const emoji = await this.emojisCache.fetch(`${name} ${host}`, queryOrNull); if (emoji == null) return null; return emoji.publicUrl || emoji.originalUrl; // || emoji.originalUrl ã—ã¦ã‚‹ã®ã¯å¾Œæ–¹äº’æ›æ€§ã®ãŸã‚(publicUrlã¯stringãªã®ã§??ã¯ã ã‚) @@ -384,7 +391,7 @@ export class CustomEmojiService implements OnApplicationShutdown { */ @bindThis public async prefetchEmojis(emojis: { name: string; host: string | null; }[]): Promise<void> { - const notCachedEmojis = emojis.filter(emoji => this.cache.get(`${emoji.name} ${emoji.host}`) == null); + const notCachedEmojis = emojis.filter(emoji => this.emojisCache.get(`${emoji.name} ${emoji.host}`) == null); const emojisQuery: any[] = []; const hosts = new Set(notCachedEmojis.map(e => e.host)); for (const host of hosts) { @@ -399,7 +406,7 @@ export class CustomEmojiService implements OnApplicationShutdown { select: ['name', 'host', 'originalUrl', 'publicUrl'], }) : []; for (const emoji of _emojis) { - this.cache.set(`${emoji.name} ${emoji.host}`, emoji); + this.emojisCache.set(`${emoji.name} ${emoji.host}`, emoji); } } @@ -424,7 +431,7 @@ export class CustomEmojiService implements OnApplicationShutdown { @bindThis public dispose(): void { - this.cache.dispose(); + this.emojisCache.dispose(); } @bindThis diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index 22871adb16bc4bc9529f5543b093a65ab54e43c1..4c8a3dc0500fa142044ae177da64df1f68522f29 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -135,6 +135,7 @@ export interface NoteEventTypes { }; replied: { id: MiNote['id']; + userId: MiUser['id']; }; } type NoteStreamEventTypes = { diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index 625df1feaa82fcbb9ddfd3e3541dae3cd32a82e7..76d0eb23398418b0cfce2f244ba2e7e78bba283f 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -6,7 +6,7 @@ import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; import * as parse5 from 'parse5'; -import { Window, XMLSerializer } from 'happy-dom'; +import { Window, DocumentFragment, XMLSerializer } from 'happy-dom'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { intersperse } from '@/misc/prelude/array.js'; @@ -483,6 +483,8 @@ export class MfmService { const doc = window.document; + const body = doc.createElement('p'); + async function appendChildren(children: mfm.MfmNode[], targetElement: any): Promise<void> { if (children) { for (const child of await Promise.all(children.map(async (x) => await (handlers as any)[x.type](x)))) targetElement.appendChild(child); @@ -661,7 +663,7 @@ export class MfmService { }, }; - await appendChildren(nodes, doc.body); + await appendChildren(nodes, body); if (quoteUri !== null) { const a = doc.createElement('a'); @@ -675,9 +677,15 @@ export class MfmService { quote.innerHTML += 'RE: '; quote.appendChild(a); - doc.body.appendChild(quote); + body.appendChild(quote); + } + + let result = new XMLSerializer().serializeToString(body); + + if (inline) { + result = result.replace(/^<p>/,'').replace(/<\/p>$/,''); } - return inline ? doc.body.innerHTML : `<p>${doc.body.innerHTML}</p>`; + return result; } } diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 41efa76f3f6ede0c0c11667f6a6dc0bd8726843b..44b066444dfbd00e05ba7b5e2e60a5ad2360058b 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -831,6 +831,7 @@ export class NoteCreateService implements OnApplicationShutdown { if (data.reply) { this.globalEventService.publishNoteStream(data.reply.id, 'replied', { id: note.id, + userId: user.id, }); // 通知 if (data.reply.userHost === null) { diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts index e9dc9b57afef6415d7eda5ccdac9a974c7d40fb0..91857dc683d268d1938cf8c665831ec29bf5c2f1 100644 --- a/packages/backend/src/core/RelayService.ts +++ b/packages/backend/src/core/RelayService.ts @@ -35,7 +35,7 @@ export class RelayService { private createSystemUserService: CreateSystemUserService, private apRendererService: ApRendererService, ) { - this.relaysCache = new MemorySingleCache<MiRelay[]>(1000 * 60 * 10); + this.relaysCache = new MemorySingleCache<MiRelay[]>(1000 * 60 * 10); // 10m } @bindThis diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index f5a753afc717b6868277fdc43860d89ed2ced4d2..2b6089fd3a0072db87b005453fa7c31fc407479e 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -129,10 +129,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { private moderationLogService: ModerationLogService, private fanoutTimelineService: FanoutTimelineService, ) { - //this.onMessage = this.onMessage.bind(this); - - this.rolesCache = new MemorySingleCache<MiRole[]>(1000 * 60 * 60 * 1); - this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 60 * 1); + this.rolesCache = new MemorySingleCache<MiRole[]>(1000 * 60 * 60); // 1h + this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 5); // 5m this.redisForSub.on('message', this.onMessage); } diff --git a/packages/backend/src/core/UserKeypairService.ts b/packages/backend/src/core/UserKeypairService.ts index 51ac99179a6a8828309d5b76c2b03dfccfc0c464..92d61cd103f7347156191dd92284666cb26b3f9a 100644 --- a/packages/backend/src/core/UserKeypairService.ts +++ b/packages/backend/src/core/UserKeypairService.ts @@ -25,7 +25,7 @@ export class UserKeypairService implements OnApplicationShutdown { ) { this.cache = new RedisKVCache<MiUserKeypair>(this.redisClient, 'userKeypair', { lifetime: 1000 * 60 * 60 * 24, // 24h - memoryCacheLifetime: Infinity, + memoryCacheLifetime: 1000 * 60 * 60, // 1h fetcher: (key) => this.userKeypairsRepository.findOneByOrFail({ userId: key }), toRedisConverter: (value) => JSON.stringify(value), fromRedisConverter: (value) => JSON.parse(value), diff --git a/packages/backend/src/core/WebfingerService.ts b/packages/backend/src/core/WebfingerService.ts index cbbac52cdb3a4172a13dfb2692eb4c58da4d17a9..1517dd00744509e35306c6546f29342674448259 100644 --- a/packages/backend/src/core/WebfingerService.ts +++ b/packages/backend/src/core/WebfingerService.ts @@ -22,7 +22,9 @@ export type IWebFinger = { const urlRegex = /^https?:\/\//; const mRegex = /^([^@]+)@(.*)/; -const defaultProtocol = process.env.MISSKEY_WEBFINGER_USE_HTTP?.toLowerCase() === 'true' ? 'http' : 'https'; +// we have the colons here, because URL.protocol does as well, so it's +// more uniform in the places we use both +const defaultProtocol = process.env.MISSKEY_WEBFINGER_USE_HTTP?.toLowerCase() === 'true' ? 'http:' : 'https:'; @Injectable() export class WebfingerService { @@ -82,7 +84,7 @@ export class WebfingerService { const m = query.match(mRegex); if (m) { const hostname = m[2]; - return `${defaultProtocol}://${hostname}/.well-known/host-meta`; + return `${defaultProtocol}//${hostname}/.well-known/host-meta`; } throw new Error(`Invalid query (${query})`); @@ -101,7 +103,7 @@ export class WebfingerService { const template = (hostMeta['XRD']['Link'] as Array<any>).filter(p => p['@_rel'] === 'lrdd')[0]['@_template']; return template.indexOf('{uri}') < 0 ? null : template; } catch (err) { - console.error(`error while request host-meta for ${url}`); + console.error(`error while request host-meta for ${url}: ${err}`); return null; } } diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 44680a2ed5d5d4e009d2032a2ed3ce1d204d0e22..062af397321e55e5eafd389250165627f087991d 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -54,8 +54,8 @@ export class ApDbResolverService implements OnApplicationShutdown { private cacheService: CacheService, private apPersonService: ApPersonService, ) { - this.publicKeyCache = new MemoryKVCache<MiUserPublickey | null>(Infinity); - this.publicKeyByUserIdCache = new MemoryKVCache<MiUserPublickey | null>(Infinity); + this.publicKeyCache = new MemoryKVCache<MiUserPublickey | null>(1000 * 60 * 60 * 12); // 12h + this.publicKeyByUserIdCache = new MemoryKVCache<MiUserPublickey | null>(1000 * 60 * 60 * 12); // 12h } @bindThis diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 90784fdc1d662478d06222a1d752903a10f81190..98fc647a838fab28cff4bb08a61e5d778717c4df 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -793,6 +793,13 @@ export class ApRendererService { @bindThis public async attachLdSignature(activity: any, user: { id: MiUser['id']; host: null; }): Promise<IActivity> { + // Linked Data signatures are cryptographic signatures attached to each activity to provide proof of authenticity. + // When using authorized fetch, this is often undesired as any signed activity can be forwarded to a blocked instance by relays and other instances. + // This setting allows admins to disable LD signatures for increased privacy, at the expense of fewer relayed activities and additional inbound fetch (GET) requests. + if (!this.config.attachLdSignatureForRelays) { + return activity; + } + const keypair = await this.userKeypairService.getUserKeypair(user.id); const jsonLd = this.jsonLdService.use(); diff --git a/packages/backend/src/core/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts index 3d98c5b76443e71c1c844a508222ce684ae60f8f..b281ac9728fe6e96daff02cb026cc9bc5f021e00 100644 --- a/packages/backend/src/core/activitypub/models/ApImageService.ts +++ b/packages/backend/src/core/activitypub/models/ApImageService.ts @@ -82,7 +82,7 @@ export class ApImageService { url: image.url, user: actor, uri: image.url, - sensitive: image.sensitive, + sensitive: !!(image.sensitive), isLink: !shouldBeCached, comment: truncate(image.name ?? undefined, DB_MAX_IMAGE_COMMENT_LENGTH), }); diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts index 002a93397de7bf26258e059285a90628a37e0839..2dfb8d5356877b23f5e6304d481d07bb63972ef0 100644 --- a/packages/backend/src/core/entities/InstanceEntityService.ts +++ b/packages/backend/src/core/entities/InstanceEntityService.ts @@ -63,8 +63,9 @@ export class InstanceEntityService { @bindThis public packMany( instances: MiInstance[], + me?: { id: MiUser['id']; } | null | undefined, ) { - return Promise.all(instances.map(x => this.pack(x))); + return Promise.all(instances.map(x => this.pack(x, me))); } } diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index bba64a06eff2dbb2dd28faa5bad1bb7041e08022..d968069ca35563d770179f0a12a577a213c27471 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -7,23 +7,23 @@ import * as Redis from 'ioredis'; import { bindThis } from '@/decorators.js'; export class RedisKVCache<T> { - private redisClient: Redis.Redis; - private name: string; - private lifetime: number; - private memoryCache: MemoryKVCache<T>; - private fetcher: (key: string) => Promise<T>; - private toRedisConverter: (value: T) => string; - private fromRedisConverter: (value: string) => T | undefined; - - constructor(redisClient: RedisKVCache<T>['redisClient'], name: RedisKVCache<T>['name'], opts: { - lifetime: RedisKVCache<T>['lifetime']; - memoryCacheLifetime: number; - fetcher: RedisKVCache<T>['fetcher']; - toRedisConverter: RedisKVCache<T>['toRedisConverter']; - fromRedisConverter: RedisKVCache<T>['fromRedisConverter']; - }) { - this.redisClient = redisClient; - this.name = name; + private readonly lifetime: number; + private readonly memoryCache: MemoryKVCache<T>; + private readonly fetcher: (key: string) => Promise<T>; + private readonly toRedisConverter: (value: T) => string; + private readonly fromRedisConverter: (value: string) => T | undefined; + + constructor( + private redisClient: Redis.Redis, + private name: string, + opts: { + lifetime: RedisKVCache<T>['lifetime']; + memoryCacheLifetime: number; + fetcher: RedisKVCache<T>['fetcher']; + toRedisConverter: RedisKVCache<T>['toRedisConverter']; + fromRedisConverter: RedisKVCache<T>['fromRedisConverter']; + }, + ) { this.lifetime = opts.lifetime; this.memoryCache = new MemoryKVCache(opts.memoryCacheLifetime); this.fetcher = opts.fetcher; @@ -55,7 +55,13 @@ export class RedisKVCache<T> { const cached = await this.redisClient.get(`kvcache:${this.name}:${key}`); if (cached == null) return undefined; - return this.fromRedisConverter(cached); + + const value = this.fromRedisConverter(cached); + if (value !== undefined) { + this.memoryCache.set(key, value); + } + + return value; } @bindThis @@ -77,14 +83,14 @@ export class RedisKVCache<T> { // Cache MISS const value = await this.fetcher(key); - this.set(key, value); + await this.set(key, value); return value; } @bindThis public async refresh(key: string) { const value = await this.fetcher(key); - this.set(key, value); + await this.set(key, value); // TODO: イベント発行ã—ã¦ä»–プãƒã‚»ã‚¹ã®ãƒ¡ãƒ¢ãƒªã‚ャッシュも更新ã§ãるよã†ã«ã™ã‚‹ } @@ -101,23 +107,23 @@ export class RedisKVCache<T> { } export class RedisSingleCache<T> { - private redisClient: Redis.Redis; - private name: string; - private lifetime: number; - private memoryCache: MemorySingleCache<T>; - private fetcher: () => Promise<T>; - private toRedisConverter: (value: T) => string; - private fromRedisConverter: (value: string) => T | undefined; - - constructor(redisClient: RedisSingleCache<T>['redisClient'], name: RedisSingleCache<T>['name'], opts: { - lifetime: RedisSingleCache<T>['lifetime']; - memoryCacheLifetime: number; - fetcher: RedisSingleCache<T>['fetcher']; - toRedisConverter: RedisSingleCache<T>['toRedisConverter']; - fromRedisConverter: RedisSingleCache<T>['fromRedisConverter']; - }) { - this.redisClient = redisClient; - this.name = name; + private readonly lifetime: number; + private readonly memoryCache: MemorySingleCache<T>; + private readonly fetcher: () => Promise<T>; + private readonly toRedisConverter: (value: T) => string; + private readonly fromRedisConverter: (value: string) => T | undefined; + + constructor( + private redisClient: Redis.Redis, + private name: string, + opts: { + lifetime: number; + memoryCacheLifetime: number; + fetcher: RedisSingleCache<T>['fetcher']; + toRedisConverter: RedisSingleCache<T>['toRedisConverter']; + fromRedisConverter: RedisSingleCache<T>['fromRedisConverter']; + }, + ) { this.lifetime = opts.lifetime; this.memoryCache = new MemorySingleCache(opts.memoryCacheLifetime); this.fetcher = opts.fetcher; @@ -149,7 +155,13 @@ export class RedisSingleCache<T> { const cached = await this.redisClient.get(`singlecache:${this.name}`); if (cached == null) return undefined; - return this.fromRedisConverter(cached); + + const value = this.fromRedisConverter(cached); + if (value !== undefined) { + this.memoryCache.set(value); + } + + return value; } @bindThis @@ -171,14 +183,14 @@ export class RedisSingleCache<T> { // Cache MISS const value = await this.fetcher(); - this.set(value); + await this.set(value); return value; } @bindThis public async refresh() { const value = await this.fetcher(); - this.set(value); + await this.set(value); // TODO: イベント発行ã—ã¦ä»–プãƒã‚»ã‚¹ã®ãƒ¡ãƒ¢ãƒªã‚ャッシュも更新ã§ãるよã†ã«ã™ã‚‹ } @@ -187,22 +199,12 @@ export class RedisSingleCache<T> { // TODO: メモリ節約ã®ãŸã‚ã‚ã¾ã‚Šå‚ç…§ã•ã‚Œãªã„ã‚ーを定期的ã«å‰Šé™¤ã§ãるよã†ã«ã™ã‚‹ï¼Ÿ export class MemoryKVCache<T> { - /** - * データをæŒã¤ãƒžãƒƒãƒ— - * @deprecated ã“れを直接æ“作ã™ã‚‹ã¹ãã§ã¯ãªã„ - */ - public cache: Map<string, { date: number; value: T; }>; - private lifetime: number; - private gcIntervalHandle: NodeJS.Timeout; + private readonly cache = new Map<string, { date: number; value: T; }>(); + private readonly gcIntervalHandle = setInterval(() => this.gc(), 1000 * 60 * 3); // 3m - constructor(lifetime: MemoryKVCache<never>['lifetime']) { - this.cache = new Map(); - this.lifetime = lifetime; - - this.gcIntervalHandle = setInterval(() => { - this.gc(); - }, 1000 * 60 * 3); - } + constructor( + private readonly lifetime: number, + ) {} @bindThis /** @@ -287,10 +289,14 @@ export class MemoryKVCache<T> { @bindThis public gc(): void { const now = Date.now(); + for (const [key, { date }] of this.cache.entries()) { - if ((now - date) > this.lifetime) { - this.cache.delete(key); - } + // The map is ordered from oldest to youngest. + // We can stop once we find an entry that's still active, because all following entries must *also* be active. + const age = now - date; + if (age < this.lifetime) break; + + this.cache.delete(key); } } @@ -298,16 +304,19 @@ export class MemoryKVCache<T> { public dispose(): void { clearInterval(this.gcIntervalHandle); } + + public get entries() { + return this.cache.entries(); + } } export class MemorySingleCache<T> { private cachedAt: number | null = null; private value: T | undefined; - private lifetime: number; - constructor(lifetime: MemorySingleCache<never>['lifetime']) { - this.lifetime = lifetime; - } + constructor( + private lifetime: number, + ) {} @bindThis public set(value: T): void { diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index 4a1b42383f19ef5ce29b2c4a16d507126483c167..b7c418fa5936217faa5dd483f466afb0bc4dc54d 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -236,12 +236,8 @@ export function createPostgresDataSource(config: Config) { cache: !config.db.disableCache && process.env.NODE_ENV !== 'test' ? { // dbã‚’closeã—ã¦ã‚‚何故ã‹redisã®ã‚³ãƒã‚¯ã‚·ãƒ§ãƒ³ãŒå†…部的ã«æ®‹ã‚Šç¶šã‘るよã†ã§ã€ãƒ†ã‚¹ãƒˆã®éš›ã«æ”¯éšœãŒå‡ºã‚‹ãŸã‚無効ã«ã™ã‚‹(ã‚ャッシュもå«ã‚ã¦ãƒ†ã‚¹ãƒˆã—ãŸã„ãŸã‚本当ã¯æœ‰åŠ¹ã«ã—ãŸã„ãŒ...) type: 'ioredis', options: { - host: config.redis.host, - port: config.redis.port, - family: config.redis.family ?? 0, - password: config.redis.pass, + ...config.redis, keyPrefix: `${config.redis.prefix}:query:`, - db: config.redis.db ?? 0, }, } : false, logging: log, diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index d665945861e2172f4c0174ffe440b146fcde87cb..4076e9da9038f8ca7f5d5c66b882745bc6127dfd 100644 --- a/packages/backend/src/queue/processors/DeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -45,7 +45,7 @@ export class DeliverProcessorService { private queueLoggerService: QueueLoggerService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('deliver'); - this.suspendedHostsCache = new MemorySingleCache<MiInstance[]>(1000 * 60 * 60); + this.suspendedHostsCache = new MemorySingleCache<MiInstance[]>(1000 * 60 * 60); // 1h } @bindThis diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index 716bb0944b635b13017c80876733a13e1c219282..bc8d3c0411d00418958ebb96c776daa286db6706 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -135,7 +135,7 @@ export class NodeinfoServerService { return document; }; - const cache = new MemorySingleCache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10); + const cache = new MemorySingleCache<Awaited<ReturnType<typeof nodeinfo2>>>(1000 * 60 * 10); // 10m fastify.get(nodeinfo2_1path, async (request, reply) => { const base = await cache.fetch(() => nodeinfo2(21)); diff --git a/packages/backend/src/server/api/AuthenticateService.ts b/packages/backend/src/server/api/AuthenticateService.ts index ddef8db9879ac82b5ebba604ac4b00cbe8428c5f..690ff2e022822659f0bb691ee3a75655e55143a0 100644 --- a/packages/backend/src/server/api/AuthenticateService.ts +++ b/packages/backend/src/server/api/AuthenticateService.ts @@ -37,7 +37,7 @@ export class AuthenticateService implements OnApplicationShutdown { private cacheService: CacheService, ) { - this.appCache = new MemoryKVCache<MiApp>(Infinity); + this.appCache = new MemoryKVCache<MiApp>(1000 * 60 * 60 * 24 * 7); // 1w } @bindThis diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts index c3f2247b69591c71aa264d40ac26d27a01c52ea3..c1ce3f2238c5d9c8fe32798c79381a35ab88f6ae 100644 --- a/packages/backend/src/server/api/endpoints/federation/instances.ts +++ b/packages/backend/src/server/api/endpoints/federation/instances.ts @@ -203,7 +203,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const instances = await query.limit(ps.limit).offset(ps.offset).getMany(); - return await this.instanceEntityService.packMany(instances); + return await this.instanceEntityService.packMany(instances, me); }); } } diff --git a/packages/backend/src/server/api/endpoints/federation/stats.ts b/packages/backend/src/server/api/endpoints/federation/stats.ts index bac54970ab853c69993670c9e5932870c7304b94..69900bff9acae94cb7cca44800b923dfa4b1eba5 100644 --- a/packages/backend/src/server/api/endpoints/federation/stats.ts +++ b/packages/backend/src/server/api/endpoints/federation/stats.ts @@ -107,9 +107,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const gotPubCount = topPubInstances.map(x => x.followingCount).reduce((a, b) => a + b, 0); return await awaitAll({ - topSubInstances: this.instanceEntityService.packMany(topSubInstances), + topSubInstances: this.instanceEntityService.packMany(topSubInstances, me), otherFollowersCount: Math.max(0, allSubCount - gotSubCount), - topPubInstances: this.instanceEntityService.packMany(topPubInstances), + topPubInstances: this.instanceEntityService.packMany(topPubInstances, me), otherFollowingCount: Math.max(0, allPubCount - gotPubCount), }); }); diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts index a4e61a0e8f2fab268414ad221c475b5789ceb922..084d4af658178e57e0ad24c85e25d834f3e3fa3a 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts @@ -14,12 +14,19 @@ import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/model import { WebAuthnService } from '@/core/WebAuthnService.js'; import { ApiError } from '@/server/api/error.js'; import { UserAuthService } from '@/core/UserAuthService.js'; +import ms from 'ms'; export const meta = { requireCredential: true, secure: true, + limit: { + duration: ms('1hour'), + max: 10, + minInterval: ms('1sec'), + }, + errors: { incorrectPassword: { message: 'Incorrect password.', diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts index cc6e9ee42d242ffb144c0e4478731c38ecc10d43..6ab50a57c93fbcd0f8633fd0759f89fb3c0d9039 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register-key.ts @@ -12,12 +12,19 @@ import { DI } from '@/di-symbols.js'; import { WebAuthnService } from '@/core/WebAuthnService.js'; import { ApiError } from '@/server/api/error.js'; import { UserAuthService } from '@/core/UserAuthService.js'; +import ms from 'ms'; export const meta = { requireCredential: true, secure: true, + limit: { + duration: ms('1hour'), + max: 10, + minInterval: ms('1sec'), + }, + errors: { userNotFound: { message: 'User not found.', diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts index 7283159f874e55f4c4870a25c4f7736831cba42c..888d0fc6efe2ca341710718f03a362dfae11d705 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts @@ -14,12 +14,19 @@ import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { ApiError } from '@/server/api/error.js'; import { UserAuthService } from '@/core/UserAuthService.js'; +import ms from 'ms'; export const meta = { requireCredential: true, secure: true, + limit: { + duration: ms('1hour'), + max: 10, + minInterval: ms('1sec'), + }, + errors: { incorrectPassword: { message: 'Incorrect password.', diff --git a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts index 098fd5930328218462c7c8649afd2b3eb72ebb55..614fd0c498b86073360b1f53ed613c646e314289 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts @@ -13,10 +13,17 @@ import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '@/server/api/error.js'; import { UserAuthService } from '@/core/UserAuthService.js'; +import ms from 'ms'; export const meta = { requireCredential: true, + limit: { + duration: ms('1hour'), + max: 10, + minInterval: ms('1sec'), + }, + secure: true, errors: { diff --git a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts index 8da331505b82428ea8daa9568826e48bc0080d9a..27738253734fb4b67678a290401a3c2219dd3af8 100644 --- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts +++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts @@ -13,12 +13,19 @@ import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; import { ApiError } from '@/server/api/error.js'; import { UserAuthService } from '@/core/UserAuthService.js'; +import ms from 'ms'; export const meta = { requireCredential: true, secure: true, + limit: { + duration: ms('1hour'), + max: 10, + minInterval: ms('1sec'), + }, + errors: { incorrectPassword: { message: 'Incorrect password.', diff --git a/packages/backend/src/server/api/endpoints/i/change-password.ts b/packages/backend/src/server/api/endpoints/i/change-password.ts index 6aedde717c93f65000c2b4ee53690290bd63e479..f131c7e9d12f7c0b4c46b501bea72376a6c06259 100644 --- a/packages/backend/src/server/api/endpoints/i/change-password.ts +++ b/packages/backend/src/server/api/endpoints/i/change-password.ts @@ -10,10 +10,17 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UserProfilesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { UserAuthService } from '@/core/UserAuthService.js'; +import ms from 'ms'; export const meta = { requireCredential: true, + limit: { + duration: ms('1hour'), + max: 10, + minInterval: ms('1sec'), + }, + secure: true, } as const; diff --git a/packages/backend/src/server/api/endpoints/i/delete-account.ts b/packages/backend/src/server/api/endpoints/i/delete-account.ts index af4d601ad6a86d9e298234a41f47a2c4d8d2d762..565eaaafc01e3d870d498f6e601e09bb5f6fa0b4 100644 --- a/packages/backend/src/server/api/endpoints/i/delete-account.ts +++ b/packages/backend/src/server/api/endpoints/i/delete-account.ts @@ -11,10 +11,17 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { DeleteAccountService } from '@/core/DeleteAccountService.js'; import { DI } from '@/di-symbols.js'; import { UserAuthService } from '@/core/UserAuthService.js'; +import ms from 'ms'; export const meta = { requireCredential: true, + limit: { + duration: ms('1hour'), + max: 10, + minInterval: ms('1sec'), + }, + secure: true, } as const; diff --git a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts index e1cdfdc18548b8abdfe701dfa3a0f76182bc8406..814ffb5488bd98041a0b552e3c8207668bef4227 100644 --- a/packages/backend/src/server/api/endpoints/i/regenerate-token.ts +++ b/packages/backend/src/server/api/endpoints/i/regenerate-token.ts @@ -11,10 +11,17 @@ import type { UsersRepository, UserProfilesRepository } from '@/models/_.js'; import generateUserToken from '@/misc/generate-native-user-token.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; +import ms from 'ms'; export const meta = { requireCredential: true, + limit: { + duration: ms('1hour'), + max: 10, + minInterval: ms('1sec'), + }, + secure: true, } as const; diff --git a/packages/backend/src/server/api/stream/Connection.ts b/packages/backend/src/server/api/stream/Connection.ts index 41c0feccc7e37df269bb1657f06cd71470bc9c34..7dd7db24e5b417a2bfdc64858a89e78b026e8c53 100644 --- a/packages/backend/src/server/api/stream/Connection.ts +++ b/packages/backend/src/server/api/stream/Connection.ts @@ -201,6 +201,18 @@ export default class Connection { @bindThis private async onNoteStreamMessage(data: GlobalEvents['note']['payload']) { + // we must not send to the frontend information about notes from + // users who blocked the logged-in user, even when they're replies + // to notes the logged-in user can see + if (data.type === 'replied') { + const noteUserId = data.body.body.userId; + if (noteUserId !== null) { + if (this.userIdsWhoBlockingMe.has(noteUserId)) { + return; + } + } + } + this.sendMessageToWs('noteUpdated', { id: data.body.id, type: data.type, diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index 96038d9c1ea26d798cc49cfdf52939d32a4ca594..ef804b5bfdbc613814ae3e0e2b325b4c583cf6c9 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -38,8 +38,8 @@ export class UrlPreviewService { ) { this.logger = this.loggerService.getLogger('url-preview'); this.previewCache = new RedisKVCache<SummalyResult>(this.redisClient, 'summaly', { - lifetime: 1000 * 86400, - memoryCacheLifetime: 1000 * 10 * 60, + lifetime: 1000 * 60 * 60 * 24, // 1d + memoryCacheLifetime: 1000 * 60 * 10, // 10m fetcher: (key: string) => { throw new Error('the UrlPreview cache should never fetch'); }, toRedisConverter: (value) => JSON.stringify(value), fromRedisConverter: (value) => JSON.parse(value), diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue index 3bf44aea8ed8fbb04dc068b8140b56897f3ceaa2..112a84f1fd3a0a47bbfa786961c7433849d4a865 100644 --- a/packages/frontend/src/components/MkMediaList.vue +++ b/packages/frontend/src/components/MkMediaList.vue @@ -37,7 +37,7 @@ import 'photoswipe/style.css'; import XBanner from '@/components/MkMediaBanner.vue'; import XImage from '@/components/MkMediaImage.vue'; import XVideo from '@/components/MkMediaVideo.vue'; -import XModPlayer from '@/components/MkModPlayer.vue'; +import XModPlayer from '@/components/SkModPlayer.vue'; import * as os from '@/os.js'; import { FILE_TYPE_BROWSERSAFE, FILE_EXT_TRACKER_MODULES, FILE_TYPE_TRACKER_MODULES } from '@/const.js'; import { defaultStore } from '@/store.js'; diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index b8ce7ed8303af170e9685a11dfece2fe57170e82..b6ded3167b74de597bb61f470fa5c4d5ff9a913d 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only > <div v-if="appearNote.reply && inReplyToCollapsed" :class="$style.collapsedInReplyTo"> <MkAvatar :class="$style.collapsedInReplyToAvatar" :user="appearNote.reply.user" link preview/> - <MkA v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)"> + <MkA v-user-preview="appearNote.reply.userId" :class="$style.name" :to="userPage(appearNote.reply.user)"> <MkAcct :user="appearNote.reply.user"/> </MkA>: <Mfm :text="getNoteSummary(appearNote.reply)" :plain="true" :nowrap="true" :author="appearNote.reply.user" :nyaize="'respect'" :class="$style.collapsedInReplyToText" @click="inReplyToCollapsed = false"/> diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue index 5240d646612605360585ab23f3f996b61e020867..15173fbd99704b84602c0631e0d6e3201d5436b2 100644 --- a/packages/frontend/src/components/MkNotes.vue +++ b/packages/frontend/src/components/MkNotes.vue @@ -15,7 +15,6 @@ SPDX-License-Identifier: AGPL-3.0-only <template #default="{ items: notes }"> <div :class="[$style.root, { [$style.noGap]: noGap }]"> <MkDateSeparatedList - v-if="defaultStore.state.noteDesign === 'misskey'" ref="notes" v-slot="{ item: note }" :items="notes" @@ -27,34 +26,25 @@ SPDX-License-Identifier: AGPL-3.0-only > <MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note" :withHardMute="true"/> </MkDateSeparatedList> - <MkDateSeparatedList - v-else-if="defaultStore.state.noteDesign === 'sharkey'" - ref="notes" - v-slot="{ item: note }" - :items="notes" - :direction="pagination.reversed ? 'up' : 'down'" - :reversed="pagination.reversed" - :noGap="noGap" - :ad="true" - :class="$style.notes" - > - <SkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note" :withHardMute="true"/> - </MkDateSeparatedList> </div> </template> </MkPagination> </template> <script lang="ts" setup> -import { shallowRef, ref } from 'vue'; -import MkNote from '@/components/MkNote.vue'; -import SkNote from '@/components/SkNote.vue'; +import { defineAsyncComponent, shallowRef, ref } from 'vue'; import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; import MkPagination, { Paging } from '@/components/MkPagination.vue'; import { i18n } from '@/i18n.js'; import { infoImageUrl } from '@/instance.js'; import { defaultStore } from '@/store.js'; +const MkNote = defineAsyncComponent(() => + (defaultStore.state.noteDesign === 'misskey') ? import('@/components/MkNote.vue') : + (defaultStore.state.noteDesign === 'sharkey') ? import('@/components/SkNote.vue') : + null +); + const props = defineProps<{ pagination: Paging; noGap?: boolean; diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue index 68bf1bf3d8f252e43de75e4ec6aaa579d4d07c65..2dd6c21ef6d383256cbb3ff78e6142234d6b0106 100644 --- a/packages/frontend/src/components/MkNotifications.vue +++ b/packages/frontend/src/components/MkNotifications.vue @@ -14,26 +14,20 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <template #default="{ items: notifications }"> - <MkDateSeparatedList v-if="defaultStore.state.noteDesign === 'misskey'" v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true"> + <MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true"> <MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id + ':note'" :note="notification.note" :withHardMute="true"/> <XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/> </MkDateSeparatedList> - <MkDateSeparatedList v-else-if="defaultStore.state.noteDesign === 'sharkey'" v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true"> - <SkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id + ':note'" :note="notification.note" :withHardMute="true"/> - <XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/> - </MkDateSeparatedList> </template> </MkPagination> </MkPullToRefresh> </template> <script lang="ts" setup> -import { onUnmounted, onDeactivated, onMounted, computed, shallowRef, onActivated } from 'vue'; +import { defineAsyncComponent, onUnmounted, onDeactivated, onMounted, computed, shallowRef, onActivated } from 'vue'; import MkPagination from '@/components/MkPagination.vue'; import XNotification from '@/components/MkNotification.vue'; import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; -import MkNote from '@/components/MkNote.vue'; -import SkNote from '@/components/SkNote.vue'; import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; import { notificationTypes } from '@/const.js'; @@ -42,6 +36,12 @@ import { defaultStore } from '@/store.js'; import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; import * as Misskey from 'misskey-js'; +const MkNote = defineAsyncComponent(() => + (defaultStore.state.noteDesign === 'misskey') ? import('@/components/MkNote.vue') : + (defaultStore.state.noteDesign === 'sharkey') ? import('@/components/SkNote.vue') : + null +); + const props = defineProps<{ excludeTypes?: typeof notificationTypes[number][]; }>(); diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 57ed0458aad743ab25eaceea4152d8dffe0b50ea..78df70ca5cdc72348c796de4609cbfdaf47eed44 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -372,7 +372,7 @@ function watchForDraft() { } function MFMWindow() { - os.popup(defineAsyncComponent(() => import('@/components/MkMfmWindow.vue')), {}, {}, 'closed'); + os.popup(defineAsyncComponent(() => import('@/components/SkMfmWindow.vue')), {}, {}, 'closed'); } function checkMissingMention() { @@ -777,7 +777,9 @@ async function post(ev?: MouseEvent) { if (defaultStore.state.warnMissingAltText) { const filesData = toRaw(files.value); - const isMissingAltText = filesData.some(file => !file.comment); + const isMissingAltText = filesData.filter( + file => file.type.startsWith('image/') || file.type.startsWith('video/') || file.type.startsWith('audio/') + ).some(file => !file.comment); if (isMissingAltText) { const { canceled, result } = await os.actions({ diff --git a/packages/frontend/src/components/MkFormula.vue b/packages/frontend/src/components/SkFormula.vue similarity index 100% rename from packages/frontend/src/components/MkFormula.vue rename to packages/frontend/src/components/SkFormula.vue diff --git a/packages/frontend/src/components/MkMfmWindow.vue b/packages/frontend/src/components/SkMfmWindow.vue similarity index 100% rename from packages/frontend/src/components/MkMfmWindow.vue rename to packages/frontend/src/components/SkMfmWindow.vue diff --git a/packages/frontend/src/components/MkModPlayer.vue b/packages/frontend/src/components/SkModPlayer.vue similarity index 100% rename from packages/frontend/src/components/MkModPlayer.vue rename to packages/frontend/src/components/SkModPlayer.vue diff --git a/packages/frontend/src/components/SkNote.vue b/packages/frontend/src/components/SkNote.vue index e8d51c850a3b5e1ba9e0df1dfb159a2513a7a565..98f250c8b35d16cf846e49956d7554ad37ee650f 100644 --- a/packages/frontend/src/components/SkNote.vue +++ b/packages/frontend/src/components/SkNote.vue @@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="appearNote.reply && inReplyToCollapsed && !renoteCollapsed" :class="$style.collapsedInReplyTo"> <div :class="$style.collapsedInReplyToLine"></div> <MkAvatar :class="$style.collapsedInReplyToAvatar" :user="appearNote.reply.user" link preview/> - <MkA v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)"> + <MkA v-user-preview="appearNote.reply.userId" :class="$style.name" :to="userPage(appearNote.reply.user)"> <MkAcct :user="appearNote.reply.user"/> </MkA>: <Mfm :text="getNoteSummary(appearNote.reply)" :plain="true" :nowrap="true" :author="appearNote.reply.user" :nyaize="'respect'" :class="$style.collapsedInReplyToText" @click="inReplyToCollapsed = false"/> diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts index 762f921a7c806ebd6ea7006a0f501b7df72df9f6..ac563ffaf5700f9486923a15bf8e91b3591a35c1 100644 --- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts +++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts @@ -80,7 +80,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven const isBlock = props.isBlock ?? false; - const MkFormula = defineAsyncComponent(() => import('@/components/MkFormula.vue')); + const SkFormula = defineAsyncComponent(() => import('@/components/SkFormula.vue')); /** * Gen Vue Elements from MFM AST @@ -499,14 +499,14 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven } case 'mathInline': { - return [h('bdi', h(MkFormula, { + return [h('bdi', h(SkFormula, { formula: token.props.formula, block: false, }))]; } case 'mathBlock': { - return [h('bdi', { class: 'block' }, h(MkFormula, { + return [h('bdi', { class: 'block' }, h(SkFormula, { formula: token.props.formula, block: true, }))]; diff --git a/packages/frontend/src/pages/favorites.vue b/packages/frontend/src/pages/favorites.vue index dc28d93485e05aeb657f304a1b66001f66ca25a3..cb1feef0166462bb83dc0772fb4c5bd3e4569975 100644 --- a/packages/frontend/src/pages/favorites.vue +++ b/packages/frontend/src/pages/favorites.vue @@ -16,14 +16,9 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <template #default="{ items }"> - <MkDateSeparatedList v-if="defaultStore.state.noteDesign === 'misskey'" - v-slot="{ item }" :items="items" :direction="'down'" :noGap="false" :ad="false"> + <MkDateSeparatedList v-slot="{ item }" :items="items" :direction="'down'" :noGap="false" :ad="false"> <MkNote :key="item.id" :note="item.note" :class="$style.note"/> </MkDateSeparatedList> - <MkDateSeparatedList v-if="defaultStore.state.noteDesign === 'sharkey'" - v-slot="{ item }" :items="items" :direction="'down'" :noGap="false" :ad="false"> - <SkNote :key="item.id" :note="item.note" :class="$style.note"/> - </MkDateSeparatedList> </template> </MkPagination> </MkSpacer> @@ -32,14 +27,19 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import MkPagination from '@/components/MkPagination.vue'; -import MkNote from '@/components/MkNote.vue'; -import SkNote from '@/components/SkNote.vue'; import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; +import { defineAsyncComponent } from 'vue'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { infoImageUrl } from '@/instance.js'; import { defaultStore } from '@/store.js'; +const MkNote = defineAsyncComponent(() => + (defaultStore.state.noteDesign === 'misskey') ? import('@/components/MkNote.vue') : + (defaultStore.state.noteDesign === 'sharkey') ? import('@/components/SkNote.vue') : + null +); + const pagination = { endpoint: 'i/favorites' as const, limit: 10, diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue index 9b5289786fae325eb64966036727dfe3eb563794..d0bbaeddd96bd4fe961301eaf6bd4a14b24c0d08 100644 --- a/packages/frontend/src/pages/note.vue +++ b/packages/frontend/src/pages/note.vue @@ -19,14 +19,10 @@ SPDX-License-Identifier: AGPL-3.0-only <MkButton v-if="note.channelId" rounded :class="$style.loadButton" @click="showNext = 'channel'"><i class="ti ti-chevron-up"></i> <i class="ti ti-device-tv"></i></MkButton> <MkButton rounded :class="$style.loadButton" @click="showNext = 'user'"><i class="ti ti-chevron-up"></i> <i class="ti ti-user"></i></MkButton> </div> - <div v-if="defaultStore.state.noteDesign === 'misskey'" class="_margin _gaps_s"> + <div class="_margin _gaps_s"> <MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri"/> <MkNoteDetailed :key="note.id" v-model:note="note" :initialTab="initialTab" :class="$style.note" :expandAllCws="expandAllCws"/> </div> - <div v-else-if="defaultStore.state.noteDesign === 'sharkey'" class="_margin _gaps_s"> - <MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri"/> - <SkNoteDetailed :key="note.id" v-model:note="note" :initialTab="initialTab" :class="$style.note" :expandAllCws="expandAllCws"/> - </div> <div v-if="clips && clips.length > 0" class="_margin"> <div style="font-weight: bold; padding: 12px;">{{ i18n.ts.clip }}</div> <div class="_gaps"> @@ -52,12 +48,10 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, watch, ref } from 'vue'; +import { defineAsyncComponent, computed, watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; import type { Paging } from '@/components/MkPagination.vue'; -import MkNoteDetailed from '@/components/MkNoteDetailed.vue'; import MkNotes from '@/components/MkNotes.vue'; -import SkNoteDetailed from '@/components/SkNoteDetailed.vue'; import MkRemoteCaution from '@/components/MkRemoteCaution.vue'; import MkButton from '@/components/MkButton.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; @@ -67,6 +61,12 @@ import { dateString } from '@/filters/date.js'; import MkClipPreview from '@/components/MkClipPreview.vue'; import { defaultStore } from '@/store.js'; +const MkNoteDetailed = defineAsyncComponent(() => + (defaultStore.state.noteDesign === 'misskey') ? import('@/components/MkNoteDetailed.vue') : + (defaultStore.state.noteDesign === 'sharkey') ? import('@/components/SkNoteDetailed.vue') : + null +); + const props = defineProps<{ noteId: string; initialTab?: string; diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 7ac23b0aaa2efda8aa10e4077015e0ab23876ede..112394170ff42ff689d59c920b621e1c37c3fdd0 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -120,12 +120,9 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <div class="contents _gaps"> - <div v-if="user.pinnedNotes.length > 0 && defaultStore.state.noteDesign === 'misskey'" class="_gaps"> + <div v-if="user.pinnedNotes.length > 0" class="_gaps"> <MkNote v-for="note in user.pinnedNotes" :key="note.id" class="note _panel" :note="note" :pinned="true"/> </div> - <div v-else-if="user.pinnedNotes.length > 0 && defaultStore.state.noteDesign === 'sharkey'" class="_gaps"> - <SkNote v-for="note in user.pinnedNotes" :key="note.id" class="note _panel" :note="note" :pinned="true"/> - </div> <MkInfo v-else-if="$i && $i.id === user.id">{{ i18n.ts.userPagePinTip }}</MkInfo> <template v-if="narrow"> <MkLazy> @@ -171,9 +168,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { defineAsyncComponent, computed, onMounted, onUnmounted, nextTick, watch, ref } from 'vue'; import * as Misskey from 'misskey-js'; import MkTab from '@/components/MkTab.vue'; -import MkNote from '@/components/MkNote.vue'; import MkNotes from '@/components/MkNotes.vue'; -import SkNote from '@/components/SkNote.vue'; import MkFollowButton from '@/components/MkFollowButton.vue'; import MkAccountMoved from '@/components/MkAccountMoved.vue'; import MkRemoteCaution from '@/components/MkRemoteCaution.vue'; @@ -195,6 +190,12 @@ import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFf import { useRouter } from '@/router/supplier.js'; import { getStaticImageUrl } from '@/scripts/media-proxy.js'; +const MkNote = defineAsyncComponent(() => + (defaultStore.state.noteDesign === 'misskey') ? import('@/components/MkNote.vue') : + (defaultStore.state.noteDesign === 'sharkey') ? import('@/components/SkNote.vue') : + null +); + function calcAge(birthdate: string): number { const date = new Date(birthdate); const now = new Date(); diff --git a/packages/frontend/vite.replaceIcons.ts b/packages/frontend/vite.replaceIcons.ts index 3212c9a38a7f67428d4fa5d53352b98724b2d181..494a327477d0de11ac49f437d86d14c916644362 100644 --- a/packages/frontend/vite.replaceIcons.ts +++ b/packages/frontend/vite.replaceIcons.ts @@ -5,10 +5,9 @@ function iconsReplace(opts: RollupReplaceOptions) { return pluginReplace({ ...opts, preventAssignment: false, - // only replace these strings at the start of strings, remove a - // `ti-fw` it if happens to be just after, and make sure they're - // followed by a word-boundary that's not a dash - delimiters: ['(?<=["\'`])', '(?: ti-fw)?\\b(?!-)'], + // only replace these strings at the start of strings, and make + // sure they're followed by a word-boundary that's not a dash + delimiters: ['(?<=["\'`])', '\\b(?!-)'], }); }