From 296c40c5b43b7a0f796b2076911b418b9e65530f Mon Sep 17 00:00:00 2001 From: Ignas Kiela Date: Tue, 23 May 2023 10:39:42 +0300 Subject: [PATCH 01/23] fix: stop sending pings on every pong This resulted in endless ping-pong traffic on the websocket, happening every interval of network latency to the server (e.g. for me, with 40ms latency to my server, it was about every 40ms). On my server this ended up taking about 20% of foundkey's CPU usage. Now, just send pings every 30s, and check if we have received any pong's in last 60 seconds to check that the connection is still alive. Changelog: Fixed --- packages/backend/src/server/api/streaming.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/backend/src/server/api/streaming.ts b/packages/backend/src/server/api/streaming.ts index eefbcc216..5a1c79831 100644 --- a/packages/backend/src/server/api/streaming.ts +++ b/packages/backend/src/server/api/streaming.ts @@ -44,18 +44,21 @@ export const initializeStreamingServer = (server: http.Server): void => { const main = new Connection(socket, ev, user, app); // ping/pong mechanism - let pingTimeout = null; - function startHeartbeat() { - if (pingTimeout) clearTimeout(pingTimeout); - + let pingTimeout: NodeJS.Timeout | null = null; + let disconnectTimeout = setTimeout(() => { + socket.terminate(); + }, 60 * SECOND);; + function sendPing() { socket.ping(); pingTimeout = setTimeout(() => { - socket.terminate(); + sendPing(); }, 30 * SECOND); } - startHeartbeat(); - socket.on('ping', () => { startHeartbeat(); }); - socket.on('pong', () => { startHeartbeat(); }); + function onPong() { + disconnectTimeout.refresh() + } + sendPing(); + socket.on('pong', onPong); // keep user "online" while a stream is connected const intervalId = user ? setInterval(() => { @@ -75,6 +78,7 @@ export const initializeStreamingServer = (server: http.Server): void => { redisClient.off('message', onRedisMessage); if (intervalId) clearInterval(intervalId); if (pingTimeout) clearTimeout(pingTimeout); + if (disconnectTimeout) clearTimeout(disconnectTimeout); }); }); }); From 6b9a3259f5b4da2066131ca3ae49fc991b129350 Mon Sep 17 00:00:00 2001 From: Ignas Kiela Date: Tue, 23 May 2023 12:09:15 +0300 Subject: [PATCH 02/23] fix: log levels order Most important messages are the ones with the highest value This is a fixup for commit c5327f74d42a811e4a924b0f72b8cd8aaa0d0b71. --- packages/backend/src/services/logger.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/backend/src/services/logger.ts b/packages/backend/src/services/logger.ts index 5c24186b6..9d4190ca3 100644 --- a/packages/backend/src/services/logger.ts +++ b/packages/backend/src/services/logger.ts @@ -12,11 +12,11 @@ type Domain = { }; export const LEVELS = { - error: 0, - warning: 1, - success: 2, - info: 3, - debug: 4, + error: 5, + warning: 4, + success: 3, + info: 2, + debug: 1, }; export type Level = LEVELS[keyof LEVELS]; From 38c2d86983876241f5605978605cb11941bec07f Mon Sep 17 00:00:00 2001 From: Johann150 Date: Tue, 23 May 2023 19:43:04 +0200 Subject: [PATCH 03/23] add log level environment variable To avoid a circular dependency this requires moving the log level definitions. Also to avoid a circular dependency the env.ts file cannot use a logger and instead uses plain `console.log`. --- packages/backend/src/env.ts | 25 +++++++++++++--- packages/backend/src/services/logger.ts | 39 ++++++++++--------------- 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/packages/backend/src/env.ts b/packages/backend/src/env.ts index 1b678edc4..f4aae6161 100644 --- a/packages/backend/src/env.ts +++ b/packages/backend/src/env.ts @@ -1,4 +1,12 @@ -const envOption = { +export const LOG_LEVELS = { + error: 5, + warning: 4, + success: 3, + info: 2, + debug: 1, +}; + +export const envOption = { onlyQueue: false, onlyServer: false, noDaemons: false, @@ -7,14 +15,23 @@ const envOption = { withLogTime: false, quiet: false, slow: false, + logLevel: LOG_LEVELS.info, }; for (const key of Object.keys(envOption) as (keyof typeof envOption)[]) { - if (process.env['MK_' + key.replace(/[A-Z]/g, letter => `_${letter}`).toUpperCase()]) envOption[key] = true; + const value = process.env['MK_' + key.replace(/[A-Z]/g, letter => `_${letter}`).toUpperCase()]; + if (value) { + if (key === 'logLevel') { + if (value.toLowerCase() in LOG_LEVELS) { + envOption.logLevel = LOG_LEVELS[value.toLowerCase()]; + } + console.log('Unknown log level ' + JSON.stringify(value.toLowerCase()) + ', defaulting to "info"'); + } else { + envOption[key] = true; + } + } } if (process.env.NODE_ENV === 'test') envOption.disableClustering = true; if (process.env.NODE_ENV === 'test') envOption.quiet = true; if (process.env.NODE_ENV === 'test') envOption.noDaemons = true; - -export { envOption }; diff --git a/packages/backend/src/services/logger.ts b/packages/backend/src/services/logger.ts index 9d4190ca3..070716286 100644 --- a/packages/backend/src/services/logger.ts +++ b/packages/backend/src/services/logger.ts @@ -3,7 +3,7 @@ import chalk from 'chalk'; import convertColor from 'color-convert'; import { format as dateFormat } from 'date-fns'; import config from '@/config/index.js'; -import { envOption } from '@/env.js'; +import { envOption, LOG_LEVELS } from '@/env.js'; import type { KEYWORD } from 'color-convert/conversions.js'; type Domain = { @@ -11,14 +11,7 @@ type Domain = { color?: KEYWORD; }; -export const LEVELS = { - error: 5, - warning: 4, - success: 3, - info: 2, - debug: 1, -}; -export type Level = LEVELS[keyof LEVELS]; +export type Level = LOG_LEVELS[keyof LOG_LEVELS]; /** * Class that facilitates recording log messages to the console. @@ -38,7 +31,7 @@ export default class Logger { * @param color Log message color * @param store Whether to store messages */ - constructor(domain: string, color?: KEYWORD, store = true, minLevel: Level = LEVELS.info) { + constructor(domain: string, color?: KEYWORD, store = true, minLevel: Level = LOG_LEVELS.info) { this.domain = { name: domain, color, @@ -54,7 +47,7 @@ export default class Logger { * @param store Whether to store messages * @returns A Logger instance whose parent logger is this instance. */ - public createSubLogger(domain: string, color?: KEYWORD, store = true, minLevel: Level = LEVELS.info): Logger { + public createSubLogger(domain: string, color?: KEYWORD, store = true, minLevel: Level = LOG_LEVELS.info): Logger { const logger = new Logger(domain, color, store, minLevel); logger.parentLogger = this; return logger; @@ -89,7 +82,7 @@ export default class Logger { let levelDisplay; let messageDisplay; switch (level) { - case LEVELS.error: + case LOG_LEVELS.error: if (important) { levelDisplay = chalk.bgRed.white('ERR '); } else { @@ -97,11 +90,11 @@ export default class Logger { } messageDisplay = chalk.red(message); break; - case LEVELS.warning: + case LOG_LEVELS.warning: levelDisplay = chalk.yellow('WARN'); messageDisplay = chalk.yellow(message); break; - case LEVELS.success: + case LOG_LEVELS.success: if (important) { levelDisplay = chalk.bgGreen.white('DONE'); } else { @@ -109,11 +102,11 @@ export default class Logger { } messageDisplay = chalk.green(message); break; - case LEVELS.info: + case LOG_LEVELS.info: levelDisplay = chalk.blue('INFO'); messageDisplay = message; break; - case LEVELS.debug: default: + case LOG_LEVELS.debug: default: levelDisplay = chalk.gray('VERB'); messageDisplay = chalk.gray(message); break; @@ -133,11 +126,11 @@ export default class Logger { */ public error(err: string | Error, important = false): void { if (err instanceof Error) { - this.log(LEVELS.error, err.toString(), important); + this.log(LOG_LEVELS.error, err.toString(), important); } else if (typeof err === 'object') { - this.log(LEVELS.error, `${(err as any).message || (err as any).name || err}`, important); + this.log(LOG_LEVELS.error, `${(err as any).message || (err as any).name || err}`, important); } else { - this.log(LEVELS.error, `${err}`, important); + this.log(LOG_LEVELS.error, `${err}`, important); } } @@ -148,7 +141,7 @@ export default class Logger { * @param important Whether this warning is important */ public warn(message: string, important = false): void { - this.log(LEVELS.warning, message, important); + this.log(LOG_LEVELS.warning, message, important); } /** @@ -158,7 +151,7 @@ export default class Logger { * @param important Whether this success message is important */ public succ(message: string, important = false): void { - this.log(LEVELS.success, message, important); + this.log(LOG_LEVELS.success, message, important); } /** @@ -168,7 +161,7 @@ export default class Logger { * @param important Whether this debug message is important */ public debug(message: string, important = false): void { - this.log(LEVELS.debug, message, important); + this.log(LOG_LEVELS.debug, message, important); } /** @@ -178,6 +171,6 @@ export default class Logger { * @param important Whether this info message is important */ public info(message: string, important = false): void { - this.log(LEVELS.info, message, important); + this.log(LOG_LEVELS.info, message, important); } } From 239a52eb995caf8b6d60a1742209b49fab918f1e Mon Sep 17 00:00:00 2001 From: Johann150 Date: Tue, 23 May 2023 19:45:33 +0200 Subject: [PATCH 04/23] add "quiet" log level This log level replaces the "MK_QUIET" environment variable to unify the interface in a sensible way. This also removes the "MK_VERBOSE" environment variable which was unused. --- packages/backend/src/boot/index.ts | 4 ++-- packages/backend/src/boot/master.ts | 4 ++-- packages/backend/src/env.ts | 11 ++++++----- packages/backend/src/services/logger.ts | 1 - 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/backend/src/boot/index.ts b/packages/backend/src/boot/index.ts index 46dc2d258..2580920f1 100644 --- a/packages/backend/src/boot/index.ts +++ b/packages/backend/src/boot/index.ts @@ -3,7 +3,7 @@ import chalk from 'chalk'; import Xev from 'xev'; import Logger from '@/services/logger.js'; -import { envOption } from '@/env.js'; +import { envOption, LOG_LEVELS } from '@/env.js'; // for typeorm import 'reflect-metadata'; @@ -66,7 +66,7 @@ cluster.on('exit', worker => { }); // Display detail of unhandled promise rejection -if (!envOption.quiet) { +if (envOption.logLevel !== LOG_LEVELS.quiet) { process.on('unhandledRejection', console.dir); } diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts index 7a27deb9a..26303d140 100644 --- a/packages/backend/src/boot/master.ts +++ b/packages/backend/src/boot/master.ts @@ -11,7 +11,7 @@ import Logger from '@/services/logger.js'; import { loadConfig } from '@/config/load.js'; import { Config } from '@/config/types.js'; import { showMachineInfo } from '@/misc/show-machine-info.js'; -import { envOption } from '@/env.js'; +import { envOption, LOG_LEVELS } from '@/env.js'; import { db, initDb } from '@/db/postgre.js'; const _filename = fileURLToPath(import.meta.url); @@ -25,7 +25,7 @@ const bootLogger = logger.createSubLogger('boot', 'magenta', false); const themeColor = chalk.hex('#86b300'); function greet(): void { - if (!envOption.quiet) { + if (envOption.logLevel !== LOG_LEVELS.quiet) { //#region FoundKey logo console.log(themeColor(' ___ _ _ __ ')); console.log(themeColor(' | __|__ _ _ _ _ __| | |/ /___ _ _ ')); diff --git a/packages/backend/src/env.ts b/packages/backend/src/env.ts index f4aae6161..586a2fb62 100644 --- a/packages/backend/src/env.ts +++ b/packages/backend/src/env.ts @@ -1,4 +1,5 @@ export const LOG_LEVELS = { + quiet: 6, error: 5, warning: 4, success: 3, @@ -11,9 +12,7 @@ export const envOption = { onlyServer: false, noDaemons: false, disableClustering: false, - verbose: false, withLogTime: false, - quiet: false, slow: false, logLevel: LOG_LEVELS.info, }; @@ -32,6 +31,8 @@ for (const key of Object.keys(envOption) as (keyof typeof envOption)[]) { } } -if (process.env.NODE_ENV === 'test') envOption.disableClustering = true; -if (process.env.NODE_ENV === 'test') envOption.quiet = true; -if (process.env.NODE_ENV === 'test') envOption.noDaemons = true; +if (process.env.NODE_ENV === 'test') { + envOption.disableClustering = true; + envOption.logLevel = LOG_LEVELS.quiet; + envOption.noDaemons = true; +} diff --git a/packages/backend/src/services/logger.ts b/packages/backend/src/services/logger.ts index 070716286..67fb8407c 100644 --- a/packages/backend/src/services/logger.ts +++ b/packages/backend/src/services/logger.ts @@ -62,7 +62,6 @@ export default class Logger { * @param subDomains Names of sub-loggers to be added. */ private log(level: Level, message: string, important = false, subDomains: Domain[] = [], _store = true): void { - if (envOption.quiet) return; const store = _store && this.store; // Check against the configured log level. From e6a8173378bb60ff34e3ab2112e55a205500dee3 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Tue, 23 May 2023 19:48:48 +0200 Subject: [PATCH 05/23] refactor onlyQueue and onlyServer configuration Instead of checking this configuration in the respective component (queue) or not at all (server), the configuration can be checked when starting the respective workers. --- packages/backend/src/boot/master.ts | 17 +++++++++++++---- packages/backend/src/queue/index.ts | 3 --- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts index 26303d140..c53dc12b1 100644 --- a/packages/backend/src/boot/master.ts +++ b/packages/backend/src/boot/master.ts @@ -141,15 +141,24 @@ async function connectDb(): Promise { async function spawnWorkers(clusterLimits: Required): Promise { const modes = ['web' as const, 'queue' as const]; + + const clusters = structuredClone(clusterLimits); + + if (envOption.onlyQueue) { + clusters.web = 0; + } else if (envOption.onlyServer) { + clusters.queue = 0; + } + const cpus = os.cpus().length; - for (const mode of modes.filter(mode => clusterLimits[mode] > cpus)) { + for (const mode of modes.filter(mode => clusters[mode] > cpus)) { bootLogger.warn(`configuration warning: cluster limit for ${mode} exceeds number of cores (${cpus})`); } - const total = modes.reduce((acc, mode) => acc + clusterLimits[mode], 0); + const total = modes.reduce((acc, mode) => acc + clusters[mode], 0); const workers = new Array(total); - workers.fill('web', 0, clusterLimits.web); - workers.fill('queue', clusterLimits.web); + workers.fill('web', 0, clusters.web); + workers.fill('queue', clusters.web); bootLogger.info(`Starting ${total} workers...`); await Promise.all(workers.map(mode => spawnWorker(mode))); diff --git a/packages/backend/src/queue/index.ts b/packages/backend/src/queue/index.ts index 558f54ff0..0d0a89f14 100644 --- a/packages/backend/src/queue/index.ts +++ b/packages/backend/src/queue/index.ts @@ -5,7 +5,6 @@ import config from '@/config/index.js'; import { DriveFile } from '@/models/entities/drive-file.js'; import { Webhook, webhookEventTypes } from '@/models/entities/webhook.js'; import { IActivity } from '@/remote/activitypub/type.js'; -import { envOption } from '@/env.js'; import { MINUTE } from '@/const.js'; import processDeliver from './processors/deliver.js'; @@ -289,8 +288,6 @@ export function webhookDeliver(webhook: Webhook, type: typeof webhookEventTypes[ } export default function() { - if (envOption.onlyServer) return; - deliverQueue.process(config.deliverJobConcurrency, processDeliver); inboxQueue.process(config.inboxJobConcurrency, processInbox); endedPollNotificationQueue.process(endedPollNotification); From efa8305e0b6d05f146dee5acdbbedb1a92f604ca Mon Sep 17 00:00:00 2001 From: Johann150 Date: Tue, 23 May 2023 19:49:10 +0200 Subject: [PATCH 06/23] BREAKING rename environment variables from MK_... to FK_... Changelog: Changed --- packages/backend/src/env.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/env.ts b/packages/backend/src/env.ts index 586a2fb62..e89b83567 100644 --- a/packages/backend/src/env.ts +++ b/packages/backend/src/env.ts @@ -18,7 +18,7 @@ export const envOption = { }; for (const key of Object.keys(envOption) as (keyof typeof envOption)[]) { - const value = process.env['MK_' + key.replace(/[A-Z]/g, letter => `_${letter}`).toUpperCase()]; + const value = process.env['FK_' + key.replace(/[A-Z]/g, letter => `_${letter}`).toUpperCase()]; if (value) { if (key === 'logLevel') { if (value.toLowerCase() in LOG_LEVELS) { From 4a77e93dfd41d378213444fe555ddca573f62c4c Mon Sep 17 00:00:00 2001 From: Johann150 Date: Tue, 23 May 2023 19:51:55 +0200 Subject: [PATCH 07/23] document environment variables --- docs/install-docker.md | 3 ++- docs/install.md | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/install-docker.md b/docs/install-docker.md index 9444c5ace..435b09bd6 100644 --- a/docs/install-docker.md +++ b/docs/install-docker.md @@ -38,10 +38,11 @@ cp .config/docker_example.env .config/docker.env Edit `default.yml` and `docker.env` according to the instructions in the files. You will need to set the database host to `db` and Redis host to `redis` in order to use the internal container network for these services. - Edit `docker-compose.yml` if necessary. (e.g. if you want to change the port). If you are using SELinux (eg. you're on Fedora or a RHEL derivative), you'll want to add the `Z` mount flag to the volume mounts to allow the containers to access the contents of those volumes. +Also check out the [Configure Foundkey](./install.md#configure-foundkey) section in the ordinary installation instructions. + ## Build and initialize The following command will build FoundKey and initialize the database. This will take some time. diff --git a/docs/install.md b/docs/install.md index 055c5a590..ce5452a1d 100644 --- a/docs/install.md +++ b/docs/install.md @@ -78,6 +78,21 @@ There are instructions for setting up [nginx](./nginx.md) for this purpose. ### Changing the default Reaction You can change the default reaction that is used when an ActivityPub "Like" is received from '👍' to '⭐' by changing the boolean value `meta.useStarForReactionFallback` in the databse respectively. +### Environment variables +There are some behaviour changes which can be accomplished using environment variables. + +|variable name|meaning| +|---|---| +|`FK_ONLY_QUEUE`|If set, only the queue processing will be run. The frontend will not be available. Cannot be combined with `FK_ONLY_SERVER` or `FK_DISABLE_CLUSTERING`.| +|`FK_ONLY_SERVER`|If set, only the frontend will be run. Queues will not be processed. Cannot be combined with `FK_ONLY_QUEUE` or `FK_DISABLE_CLUSTERING`.| +|`FK_NO_DAEMONS`|If set, the server statistics and queue statistics will not be run.| +|`FK_DISABLE_CLUSTERING`|If set, all work will be done in a single thread instead of different threads for frontend and queue. (not recommended)| +|`FK_WITH_LOG_TIME`|If set, a timestamp will be appended to all log messages.| +|`FK_SLOW`|If set, all requests will be delayed by 3s. (not recommended, useful for testing)| +|`FK_LOG_LEVEL`|Sets the log level. Messages below the set log level will be suppressed. Available log levels are `quiet` (suppress all), `error`, `warning`, `success`, `info`, `debug`.| + +If the `NODE_ENV` environment variable is set to `testing`, then the flags `FK_DISABLE_CLUSTERING` and `FK_NO_DAEMONS` will always be set, and the log level will always be `quiet`. + ## Build FoundKey Build foundkey with the following: From 2fde652b4a77061a9e6e85673e694234f0747212 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Mon, 9 Jan 2023 21:27:05 +0100 Subject: [PATCH 08/23] server: fix drive quota for remote users This deletes as many files as necessary to ensure the drive quota for remote users is kept. Previously only one file would have been deleted for each file added. Changelog: Fixed Co-authored-by: CGsama Co-authored-by: tamaina --- .../backend/src/services/drive/add-file.ts | 53 ++++++++++++------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts index 9cdade177..25ca7103c 100644 --- a/packages/backend/src/services/drive/add-file.ts +++ b/packages/backend/src/services/drive/add-file.ts @@ -2,9 +2,10 @@ import * as fs from 'node:fs'; import { v4 as uuid } from 'uuid'; import S3 from 'aws-sdk/clients/s3.js'; -import { IsNull } from 'typeorm'; +import { In, IsNull } from 'typeorm'; import sharp from 'sharp'; +import { db } from '@/db/postgre.js'; import { FILE_TYPE_BROWSERSAFE } from '@/const.js'; import { publishMainStream, publishDriveStream } from '@/services/stream.js'; import { fetchMeta } from '@/misc/fetch-meta.js'; @@ -290,25 +291,36 @@ async function upload(key: string, stream: fs.ReadStream | Buffer, _type: string if (result) logger.debug(`Uploaded: ${result.Bucket}/${result.Key} => ${result.Location}`); } -async function deleteOldFile(user: IRemoteUser): Promise { - const q = DriveFiles.createQueryBuilder('file') - .where('file.userId = :userId', { userId: user.id }) - .andWhere('NOT file.isLink'); +async function expireOldFiles(user: IRemoteUser, driveCapacity: number): Promise { + // Delete as many files as necessary so the total usage is below driveCapacity, + // oldest files first, and exclude avatar and banner. + // + // Using a window function, i.e. `OVER (ORDER BY "createdAt" DESC)` means that + // the `SUM` will be a running total. + const exceededFileIds = await db.query('SELECT "id" FROM (' + + 'SELECT "id", SUM("size") OVER (ORDER BY "createdAt" DESC) AS "total" FROM "drive_file" WHERE "userId" = $1 AND NOT "isLink"' + + (user.avatarId ? ' AND "id" != $2' : '') + + (user.bannerId ? ' AND "id" != $3' : '') + + ') AS "totals" WHERE "total" > $4', + [ + user.id, + user.avatarId ?? '', + user.bannerId ?? '', + driveCapacity, + ] + ); - if (user.avatarId) { - q.andWhere('file.id != :avatarId', { avatarId: user.avatarId }); + if (exceededFileIds.length === 0) { + // no files to expire, avatar and banner if present are already the only files + throw new Error('remote user drive quota met by avatar and banner'); } - if (user.bannerId) { - q.andWhere('file.id != :bannerId', { bannerId: user.bannerId }); - } + const files = await DriveFiles.findBy({ + id: In(exceededFileIds.map(x => x.id)), + }); - q.orderBy('file.id', 'ASC'); - - const oldFile = await q.getOne(); - - if (oldFile) { - deleteFile(oldFile, true); + for (const file of files) { + deleteFile(file, true); } } @@ -373,19 +385,20 @@ export async function addFile({ //#region Check drive usage if (user && !isLink) { const usage = await DriveFiles.calcDriveUsageOf(user.id); + const isLocalUser = Users.isLocalUser(user); const instance = await fetchMeta(); - const driveCapacity = 1024 * 1024 * (Users.isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb); + const driveCapacity = 1024 * 1024 * (isLocalUser ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb); logger.debug(`drive usage is ${usage} (max: ${driveCapacity})`); // If usage limit exceeded if (usage + info.size > driveCapacity) { - if (Users.isLocalUser(user)) { + if (isLocalUser) { throw new Error('no-free-space'); } else { - // delete oldest file (excluding banner and avatar) - deleteOldFile(await Users.findOneByOrFail({ id: user.id }) as IRemoteUser); + // delete older files to make space for new file + expireOldFiles(await Users.findOneByOrFail({ id: user.id }) as IRemoteUser, driveCapacity - info.size); } } } From 431239316983ced7ffde03cacd7b7a87371b5f92 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Wed, 1 Feb 2023 23:22:26 +0100 Subject: [PATCH 09/23] Revert 'Revert "server: fix user deletion race condition"' This reverts commit bb3ec8bafe5d16eea061929271f72032c8aeb6f3. --- .../1673201544000-deletion-progress.js | 18 +++++++++++++ packages/backend/src/models/entities/user.ts | 8 +++--- .../backend/src/models/repositories/user.ts | 2 +- packages/backend/src/queue/index.ts | 26 +++++++++++++++---- .../queue/processors/system/check-expired.ts | 7 ++++- packages/backend/src/queue/types.ts | 2 ++ .../src/remote/activitypub/deliver-manager.ts | 18 ++++++++----- .../remote/activitypub/kernel/delete/actor.ts | 2 +- .../api/endpoints/admin/users/delete.ts | 1 + .../server/api/endpoints/i/delete-account.ts | 2 +- .../backend/src/services/delete-account.ts | 2 +- packages/backend/src/services/suspend-user.ts | 5 +++- 12 files changed, 71 insertions(+), 22 deletions(-) create mode 100644 packages/backend/migration/1673201544000-deletion-progress.js diff --git a/packages/backend/migration/1673201544000-deletion-progress.js b/packages/backend/migration/1673201544000-deletion-progress.js new file mode 100644 index 000000000..90aa5cbf8 --- /dev/null +++ b/packages/backend/migration/1673201544000-deletion-progress.js @@ -0,0 +1,18 @@ +export class deletionProgress1673201544000 { + name = 'deletionProgress1673201544000'; + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" RENAME COLUMN "isDeleted" TO "isDeletedOld"`); + await queryRunner.query(`ALTER TABLE "user" ADD "isDeleted" integer`); + await queryRunner.query(`UPDATE "user" SET "isDeleted" = CASE WHEN "host" IS NULL THEN -1 ELSE 0 END WHERE "isDeletedOld"`); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isDeletedOld"`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" RENAME COLUMN "isDeleted" TO "isDeletedOld"`); + await queryRunner.query(`ALTER TABLE "user" ADD "isDeleted" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`UPDATE "user" SET "isDeleted" = "isDeletedOld" IS NOT NULL`); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isDeletedOld"`); + } +} + diff --git a/packages/backend/src/models/entities/user.ts b/packages/backend/src/models/entities/user.ts index 0c1e8092f..f2d53a984 100644 --- a/packages/backend/src/models/entities/user.ts +++ b/packages/backend/src/models/entities/user.ts @@ -163,11 +163,11 @@ export class User { // Indicates the user was deleted by an admin. // The users' data is not deleted from the database to keep them from reappearing. // A hard delete of the record may follow if we receive a matching Delete activity. - @Column('boolean', { - default: false, - comment: 'Whether the User is deleted.', + @Column('integer', { + nullable: true, + comment: 'How many delivery jobs are outstanding before the deletion is completed.', }) - public isDeleted: boolean; + public isDeleted: number | null; @Column('varchar', { length: 128, array: true, default: '{}', diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts index aea591229..6549c24c6 100644 --- a/packages/backend/src/models/repositories/user.ts +++ b/packages/backend/src/models/repositories/user.ts @@ -381,7 +381,7 @@ export const UserRepository = db.getRepository(User).extend({ autoAcceptFollowed: profile!.autoAcceptFollowed, noCrawle: profile!.noCrawle, isExplorable: user.isExplorable, - isDeleted: user.isDeleted, + isDeleted: user.isDeleted != null, hideOnlineStatus: user.hideOnlineStatus, hasUnreadSpecifiedNotes: NoteUnreads.count({ where: { userId: user.id, isSpecified: true }, diff --git a/packages/backend/src/queue/index.ts b/packages/backend/src/queue/index.ts index 0d0a89f14..4d4d9257d 100644 --- a/packages/backend/src/queue/index.ts +++ b/packages/backend/src/queue/index.ts @@ -1,7 +1,9 @@ import httpSignature from '@peertube/http-signature'; import { v4 as uuid } from 'uuid'; +import Bull from 'bull'; import config from '@/config/index.js'; +import { Users } from '@/models/index.js'; import { DriveFile } from '@/models/entities/drive-file.js'; import { Webhook, webhookEventTypes } from '@/models/entities/webhook.js'; import { IActivity } from '@/remote/activitypub/type.js'; @@ -17,7 +19,7 @@ import { endedPollNotification } from './processors/ended-poll-notification.js'; import { queueLogger } from './logger.js'; import { getJobInfo } from './get-job-info.js'; import { systemQueue, dbQueue, deliverQueue, inboxQueue, objectStorageQueue, endedPollNotificationQueue, webhookDeliverQueue } from './queues.js'; -import { ThinUser } from './types.js'; +import { DeliverJobData, ThinUser } from './types.js'; function renderError(e: Error): any { return { @@ -34,6 +36,12 @@ const inboxLogger = queueLogger.createSubLogger('inbox'); const dbLogger = queueLogger.createSubLogger('db'); const objectStorageLogger = queueLogger.createSubLogger('objectStorage'); +async function deletionRefCount(job: Bull.Job): Promise { + if (job.data.deletingUserId) { + await Users.decrement({ id: job.data.deletingUserId }, 'isDeleted', 1); + } +} + systemQueue .on('waiting', (jobId) => systemLogger.debug(`waiting id=${jobId}`)) .on('active', (job) => systemLogger.debug(`active id=${job.id}`)) @@ -45,8 +53,14 @@ systemQueue deliverQueue .on('waiting', (jobId) => deliverLogger.debug(`waiting id=${jobId}`)) .on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) - .on('failed', (job, err) => deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`)) + .on('completed', async (job, result) => { + deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`); + await deletionRefCount(job); + }) + .on('failed', async (job, err) => { + deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`); + await deletionRefCount(job); + }) .on('error', (job: any, err: Error) => deliverLogger.error(`error ${err}`)) .on('stalled', (job) => deliverLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`)); @@ -82,7 +96,7 @@ webhookDeliverQueue .on('error', (job: any, err: Error) => webhookLogger.error(`error ${err}`)) .on('stalled', (job) => webhookLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`)); -export function deliver(user: ThinUser, content: unknown, to: string | null) { +export function deliver(user: ThinUser, content: unknown, to: string | null, deletingUserId?: string) { if (content == null) return null; if (to == null) return null; @@ -92,6 +106,7 @@ export function deliver(user: ThinUser, content: unknown, to: string | null) { }, content, to, + deletingUserId, }; return deliverQueue.add(data, { @@ -323,8 +338,9 @@ export default function() { } export function destroy() { - deliverQueue.once('cleaned', (jobs, status) => { + deliverQueue.once('cleaned', async (jobs, status) => { deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`); + await Promise.all(jobs.map(job => deletionRefCount(job)); }); deliverQueue.clean(0, 'delayed'); diff --git a/packages/backend/src/queue/processors/system/check-expired.ts b/packages/backend/src/queue/processors/system/check-expired.ts index eeb6149bb..71bd498e9 100644 --- a/packages/backend/src/queue/processors/system/check-expired.ts +++ b/packages/backend/src/queue/processors/system/check-expired.ts @@ -1,6 +1,6 @@ import Bull from 'bull'; import { In, LessThan } from 'typeorm'; -import { AttestationChallenges, AuthSessions, Mutings, Notifications, PasswordResetRequests, Signins } from '@/models/index.js'; +import { AttestationChallenges, AuthSessions, Mutings, Notifications, PasswordResetRequests, Signins, Users } from '@/models/index.js'; import { publishUserEvent } from '@/services/stream.js'; import { MINUTE, MONTH } from '@/const.js'; import { queueLogger } from '@/queue/logger.js'; @@ -52,6 +52,11 @@ export async function checkExpired(job: Bull.Job>, done: createdAt: OlderThan(3 * MONTH), }); + await Users.delete({ + // delete users where the deletion status reference count has come down to zero + isDeleted: 0, + }); + logger.succ('Deleted expired data.'); done(); diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 82bd28703..d745a1fc7 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -12,6 +12,8 @@ export type DeliverJobData = { content: unknown; /** inbox URL to deliver */ to: string; + /** set if this job is part of a user deletion, on completion or failure the isDeleted field needs to be decremented */ + deletingUserId?: string; }; export type InboxJobData = { diff --git a/packages/backend/src/remote/activitypub/deliver-manager.ts b/packages/backend/src/remote/activitypub/deliver-manager.ts index 4bc651c98..dfadef150 100644 --- a/packages/backend/src/remote/activitypub/deliver-manager.ts +++ b/packages/backend/src/remote/activitypub/deliver-manager.ts @@ -88,10 +88,10 @@ export class DeliverManager { /** * Execute delivers */ - public async execute() { + public async execute(deletingUserId?: string) { if (!Users.isLocalUser(this.actor)) return; - const inboxes = new Set(); + let inboxes = new Set(); /* build inbox list @@ -150,13 +150,17 @@ export class DeliverManager { )), ); - // deliver - for (const inbox of inboxes) { - // skip instances as indicated - if (instancesToSkip.includes(new URL(inbox).host)) continue; + inboxes = inboxes.entries() + .filter(inbox => !instancesToSkip.includes(new URL(inbox).host)); - deliver(this.actor, this.activity, inbox); + if (deletingUserId) { + await Users.update(deletingUserId, { + // set deletion job count for reference counting before queueing jobs + isDeleted: inboxes.length, + }); } + + inboxes.forEach(inbox => deliver(this.actor, this.activity, inbox, deletingUserId)); } } diff --git a/packages/backend/src/remote/activitypub/kernel/delete/actor.ts b/packages/backend/src/remote/activitypub/kernel/delete/actor.ts index 9513ea22f..a191c626a 100644 --- a/packages/backend/src/remote/activitypub/kernel/delete/actor.ts +++ b/packages/backend/src/remote/activitypub/kernel/delete/actor.ts @@ -16,7 +16,7 @@ export async function deleteActor(actor: IRemoteUser, uri: string): Promise { Users.findOneByOrFail({ id: user.id }), ]); - if (userDetailed.isDeleted) { + if (userDetailed.isDeleted != null) { return; } diff --git a/packages/backend/src/services/delete-account.ts b/packages/backend/src/services/delete-account.ts index 2fa6e004b..a52b5b5d4 100644 --- a/packages/backend/src/services/delete-account.ts +++ b/packages/backend/src/services/delete-account.ts @@ -9,7 +9,7 @@ export async function deleteAccount(user: { }): Promise { await Promise.all([ Users.update(user.id, { - isDeleted: true, + isDeleted: -1, }), // revoke all of the users access tokens to block API access AccessTokens.delete({ diff --git a/packages/backend/src/services/suspend-user.ts b/packages/backend/src/services/suspend-user.ts index 11e6266a0..7ed6bff80 100644 --- a/packages/backend/src/services/suspend-user.ts +++ b/packages/backend/src/services/suspend-user.ts @@ -6,6 +6,9 @@ import { User } from '@/models/entities/user.js'; import { Users } from '@/models/index.js'; import { publishInternalEvent } from '@/services/stream.js'; +/** + * Sends an internal event and for local users queues the delete activites. + */ export async function doPostSuspend(user: { id: User['id']; host: User['host'] }): Promise { publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true }); @@ -15,6 +18,6 @@ export async function doPostSuspend(user: { id: User['id']; host: User['host'] } // deliver to all of known network const dm = new DeliverManager(user, content); dm.addEveryone(); - await dm.execute(); + await dm.execute(user.id); } } From e584937b4f7dcd8f5a9a35d68c690ac1efacbac9 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Wed, 1 Feb 2023 23:35:18 +0100 Subject: [PATCH 10/23] fix: missing paren, type error --- packages/backend/src/queue/index.ts | 2 +- packages/backend/src/remote/activitypub/deliver-manager.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/queue/index.ts b/packages/backend/src/queue/index.ts index 4d4d9257d..182202260 100644 --- a/packages/backend/src/queue/index.ts +++ b/packages/backend/src/queue/index.ts @@ -340,7 +340,7 @@ export default function() { export function destroy() { deliverQueue.once('cleaned', async (jobs, status) => { deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`); - await Promise.all(jobs.map(job => deletionRefCount(job)); + await Promise.all(jobs.map(job => deletionRefCount(job))); }); deliverQueue.clean(0, 'delayed'); diff --git a/packages/backend/src/remote/activitypub/deliver-manager.ts b/packages/backend/src/remote/activitypub/deliver-manager.ts index dfadef150..469464cec 100644 --- a/packages/backend/src/remote/activitypub/deliver-manager.ts +++ b/packages/backend/src/remote/activitypub/deliver-manager.ts @@ -150,17 +150,17 @@ export class DeliverManager { )), ); - inboxes = inboxes.entries() + const filteredInboxes = Array.from(inboxes) .filter(inbox => !instancesToSkip.includes(new URL(inbox).host)); if (deletingUserId) { await Users.update(deletingUserId, { // set deletion job count for reference counting before queueing jobs - isDeleted: inboxes.length, + isDeleted: filteredInboxes.length, }); } - inboxes.forEach(inbox => deliver(this.actor, this.activity, inbox, deletingUserId)); + filteredInboxes.forEach(inbox => deliver(this.actor, this.activity, inbox, deletingUserId)); } } From 4307010a9fe6ba347ea7795710548c22adaceda5 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Mon, 1 May 2023 12:50:39 +0200 Subject: [PATCH 11/23] fix new occurences of isDeleted --- packages/backend/src/server/api/common/getters.ts | 6 +++--- packages/backend/src/server/api/private/signin.ts | 2 +- packages/backend/src/server/web/index.ts | 8 ++++---- packages/backend/src/services/user-cache.ts | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/backend/src/server/api/common/getters.ts b/packages/backend/src/server/api/common/getters.ts index 97059f987..f15ae39f8 100644 --- a/packages/backend/src/server/api/common/getters.ts +++ b/packages/backend/src/server/api/common/getters.ts @@ -32,7 +32,7 @@ export async function getNote(noteId: Note['id'], me: { id: User['id'] } | null) export async function getUser(userId: User['id'], includeSuspended = false) { const user = await Users.findOneBy({ id: userId, - isDeleted: false, + isDeleted: IsNull(), ...(includeSuspended ? {} : {isSuspended: false}), }); @@ -50,7 +50,7 @@ export async function getRemoteUser(userId: User['id'], includeSuspended = false const user = await Users.findOneBy({ id: userId, host: Not(IsNull()), - isDeleted: false, + isDeleted: IsNull(), ...(includeSuspended ? {} : {isSuspended: false}), }); @@ -68,7 +68,7 @@ export async function getLocalUser(userId: User['id'], includeSuspended = false) const user = await Users.findOneBy({ id: userId, host: IsNull(), - isDeleted: false, + isDeleted: IsNull(), ...(includeSuspended ? {} : {isSuspended: false}), }); diff --git a/packages/backend/src/server/api/private/signin.ts b/packages/backend/src/server/api/private/signin.ts index b09262bb1..75d2a56de 100644 --- a/packages/backend/src/server/api/private/signin.ts +++ b/packages/backend/src/server/api/private/signin.ts @@ -47,7 +47,7 @@ export default async (ctx: Koa.Context) => { const user = await Users.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull(), - isDeleted: false, + isDeleted: IsNull(), }) as ILocalUser; if (user == null) { diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts index 7c04077d4..2f9a76c84 100644 --- a/packages/backend/src/server/web/index.ts +++ b/packages/backend/src/server/web/index.ts @@ -223,7 +223,7 @@ const getFeed = async (acct: string) => { usernameLower: username.toLowerCase(), host: host ?? IsNull(), isSuspended: false, - isDeleted: false, + isDeleted: IsNull(), }); return user && await packFeed(user); @@ -273,7 +273,7 @@ router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => { usernameLower: username.toLowerCase(), host: host ?? IsNull(), isSuspended: false, - isDeleted: false, + isDeleted: IsNull(), }); if (user != null) { @@ -306,7 +306,7 @@ router.get('/users/:user', async ctx => { id: ctx.params.user, host: IsNull(), isSuspended: false, - isDeleted: false, + isDeleted: IsNull(), }); if (user == null) { @@ -423,7 +423,7 @@ router.get('/@:user/pages/:page', async (ctx, next) => { usernameLower: username.toLowerCase(), host: host ?? IsNull(), isSuspended: false, - isDeleted: false, + isDeleted: IsNull(), }); if (user == null) return; diff --git a/packages/backend/src/services/user-cache.ts b/packages/backend/src/services/user-cache.ts index 77c5d0541..a886e398d 100644 --- a/packages/backend/src/services/user-cache.ts +++ b/packages/backend/src/services/user-cache.ts @@ -6,15 +6,15 @@ import { subscriber } from '@/db/redis.js'; export const userByIdCache = new Cache( Infinity, - async (id) => await Users.findOneBy({ id, isDeleted: false }) ?? undefined, + async (id) => await Users.findOneBy({ id, isDeleted: IsNull() }) ?? undefined, ); export const localUserByNativeTokenCache = new Cache( Infinity, - async (token) => await Users.findOneBy({ token, host: IsNull(), isDeleted: false }) as ILocalUser | null ?? undefined, + async (token) => await Users.findOneBy({ token, host: IsNull(), isDeleted: isNull() }) as ILocalUser | null ?? undefined, ); export const uriPersonCache = new Cache( Infinity, - async (uri) => await Users.findOneBy({ uri, isDeleted: false }) ?? undefined, + async (uri) => await Users.findOneBy({ uri, isDeleted: IsNull() }) ?? undefined, ); subscriber.on('message', async (_, data) => { From c81368f61747a8d03b8ab98082278057ee864ef5 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Tue, 23 May 2023 21:59:14 +0200 Subject: [PATCH 12/23] update changelog and version number --- CHANGELOG.md | 66 +++++++++++++++++++++++++++++++ package.json | 2 +- packages/backend/package.json | 2 +- packages/client/package.json | 2 +- packages/foundkey-js/package.json | 2 +- packages/sw/package.json | 2 +- 6 files changed, 71 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85229a4c9..1e1099f2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,72 @@ Unreleased changes should not be listed in this file. Instead, run `git shortlog --format='%h %s' --group=trailer:changelog ..` to see unreleased changes; replace `` with the tag you wish to compare from. If you are a contributor, please read [CONTRIBUTING.md, section "Changelog Trailer"](./CONTRIBUTING.md#changelog-trailer) on what to do instead. +## 13.0.0-preview5 - 2023-05-23 +This release contains 6 breaking changes and 1 security update. + +### Security +- client: check input for aiscript +- server: validate filenames and emoji names on emoji import +- server: check URL schema of ActivityPub URIs +- server: check schema for URL previews +- server: update summaly dependency + +### Added +- client: impolement filtering and sorting in drive +- client: add "nobody" follower/following visibility +- client: re-add flag to require approval for bot follows +- client: show waveform on audio player +- client: add new deepl languages +- client: add instructions on remote interaction (when signed out) +- client: show follow button when not logged in +- server: show worker mode in process names +- server: drive endpoint to fetch files and folders combined +- activitypub: implement receiving account moves + +### Changed +- **BREAKING** server: restructure endpoints related to user administration +- **BREAKING** server: refactor streaming API data structures +- **BREAKING** server: rename configuration environment variables + The environment variables that could be used for configuration which were previously prefixed with `MK_` + are now prefixed with `FK_` instead. +- server: improve error message for invalidating follows +- server: add pagination to file attachment timeline + +### Fixed +- **BREAKING** server: properly respect follower/following visibility setting on statistics endpoint + This affects the endpoint `/api/users/stats`. +- improve documentation for `fetch-rss` endpoint +- client: fix authentication error in RSS widget +- client: fix attached files and account switcher combination in new note form +- client: improved module tracker file detection +- client: fix follow requests pagination +- client: Theme creator breaks after creating a theme +- client: replace error UUIDs with error codes +- client: allow opening links in new tab + The usual 3rd button click (usually mouse wheel) or Ctrl+Click should now work to open a link in a new tab. +- client: fix drive item updates inserting duplicate entries +- client: improve error messages for failed uploads +- client: stop unnecessary network congestion by websocket ping mechanism +- server: don't fail if a system user was already created +- server: better matching for MFM mentions +- server: fix rate limit for adding reactions +- server: check instance description length limit +- server: dont error on generating RSS feeds for profiles without public posts +- server: group delivering `Delete` activities to improve performance +- server: fix drive quota for remote users +- server: user deletion race condition (again) + +### Removed +- **BREAKING** server: remove unused API parameters `sinceId` and `untilId` from `/api/notes/reactions`. +- **BREAKING** server: remove syslog integration + If you used syslog before, the syslog protocoll will no longer be connected to. + The configuration entries for `syslog` will be ignored, you should remove them if they are set. +- client: remove `driveFolderBg` theme colour +- activitypub: remove `_misskey_content` attribute +- activitypub: remove `_misskey_reaction` attribute +- activitypub: remove `_misskey_votes` attribute +- foundkey-js: remove unused definitions for Ads and detailed instance metadata + ## 13.0.0-preview4 - 2023-02-05 This release contains 6 breaking changes, including changes to the configuration file format. diff --git a/package.json b/package.json index f83572c9a..831964a0f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "foundkey", - "version": "13.0.0-preview4", + "version": "13.0.0-preview5", "repository": { "type": "git", "url": "https://akkoma.dev/FoundKeyGang/FoundKey.git" diff --git a/packages/backend/package.json b/packages/backend/package.json index d9c830baf..6c62d9231 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "backend", - "version": "13.0.0-preview4", + "version": "13.0.0-preview5", "main": "./index.js", "private": true, "type": "module", diff --git a/packages/client/package.json b/packages/client/package.json index d9a994d4a..e8ce60b35 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "client", - "version": "13.0.0-preview4", + "version": "13.0.0-preview5", "private": true, "scripts": { "watch": "vite build --watch --mode development", diff --git a/packages/foundkey-js/package.json b/packages/foundkey-js/package.json index de0862919..b01ff78e5 100644 --- a/packages/foundkey-js/package.json +++ b/packages/foundkey-js/package.json @@ -1,6 +1,6 @@ { "name": "foundkey-js", - "version": "13.0.0-preview4", + "version": "13.0.0-preview5", "description": "Fork of misskey-js for Foundkey", "type": "module", "main": "./built/index.js", diff --git a/packages/sw/package.json b/packages/sw/package.json index 41bf30605..c4023cb30 100644 --- a/packages/sw/package.json +++ b/packages/sw/package.json @@ -1,6 +1,6 @@ { "name": "sw", - "version": "13.0.0-preview4", + "version": "13.0.0-preview5", "private": true, "scripts": { "watch": "node build.js watch", From e75123602f0b9ceaa98c8ea09312bf4d2c4739d6 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Tue, 23 May 2023 22:44:15 +0200 Subject: [PATCH 13/23] fix reference error from isNull --- packages/backend/src/services/user-cache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/services/user-cache.ts b/packages/backend/src/services/user-cache.ts index a886e398d..9638f8101 100644 --- a/packages/backend/src/services/user-cache.ts +++ b/packages/backend/src/services/user-cache.ts @@ -10,7 +10,7 @@ export const userByIdCache = new Cache( ); export const localUserByNativeTokenCache = new Cache( Infinity, - async (token) => await Users.findOneBy({ token, host: IsNull(), isDeleted: isNull() }) as ILocalUser | null ?? undefined, + async (token) => await Users.findOneBy({ token, host: IsNull(), isDeleted: IsNull() }) as ILocalUser | null ?? undefined, ); export const uriPersonCache = new Cache( Infinity, From dfe12cba756342aeb039e263f6787ca8fc62fb0b Mon Sep 17 00:00:00 2001 From: Johann150 Date: Tue, 23 May 2023 22:44:56 +0200 Subject: [PATCH 14/23] show stack trace from API handler --- packages/backend/src/server/api/api-handler.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/server/api/api-handler.ts b/packages/backend/src/server/api/api-handler.ts index 3bde09602..e5a8dbcf6 100644 --- a/packages/backend/src/server/api/api-handler.ts +++ b/packages/backend/src/server/api/api-handler.ts @@ -45,7 +45,10 @@ export async function handler(endpoint: IEndpoint, ctx: Koa.Context): Promise Date: Tue, 23 May 2023 22:56:27 +0200 Subject: [PATCH 15/23] server: remove unnecessary complex loop --- .../src/queue/processors/db/delete-account.ts | 32 ++++++------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/packages/backend/src/queue/processors/db/delete-account.ts b/packages/backend/src/queue/processors/db/delete-account.ts index 84e28f25d..0bdf8c5af 100644 --- a/packages/backend/src/queue/processors/db/delete-account.ts +++ b/packages/backend/src/queue/processors/db/delete-account.ts @@ -46,29 +46,17 @@ export async function deleteAccount(job: Bull.Job): Promise } { // Delete files - let cursor: DriveFile['id'] | null = null; + const files = await DriveFiles.find({ + where: { + userId: user.id, + }, + order: { + id: 1, + }, + }) as DriveFile[]; - while (true) { - const files = await DriveFiles.find({ - where: { - userId: user.id, - ...(cursor ? { id: MoreThan(cursor) } : {}), - }, - take: 10, - order: { - id: 1, - }, - }) as DriveFile[]; - - if (files.length === 0) { - break; - } - - cursor = files[files.length - 1].id; - - for (const file of files) { - await deleteFileSync(file); - } + for (const file of files) { + await deleteFileSync(file); } logger.succ('All of files deleted'); From 79acdd7652d0a1f293ed59f539ed68ae4e454a42 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Tue, 23 May 2023 22:56:51 +0200 Subject: [PATCH 16/23] fix: properly await file deletion --- packages/backend/src/services/drive/delete-file.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/services/drive/delete-file.ts b/packages/backend/src/services/drive/delete-file.ts index 2fe8993a8..3938c7116 100644 --- a/packages/backend/src/services/drive/delete-file.ts +++ b/packages/backend/src/services/drive/delete-file.ts @@ -60,14 +60,14 @@ export async function deleteFileSync(file: DriveFile, isExpired = false): Promis await Promise.all(promises); } - postProcess(file, isExpired); + await postProcess(file, isExpired); } async function postProcess(file: DriveFile, isExpired = false): Promise { // Turn into a direct link after expiring a remote file. if (isExpired && file.userHost != null && file.uri != null) { const id = uuid(); - DriveFiles.update(file.id, { + await DriveFiles.update(file.id, { isLink: true, url: file.uri, thumbnailUrl: null, @@ -78,14 +78,14 @@ async function postProcess(file: DriveFile, isExpired = false): Promise { webpublicAccessKey: 'webpublic-' + id, }); } else { - DriveFiles.delete(file.id); + await DriveFiles.delete(file.id); } // update statistics - driveChart.update(file, false); - perUserDriveChart.update(file, false); + await driveChart.update(file, false); + await perUserDriveChart.update(file, false); if (file.userHost != null) { - instanceChart.updateDrive(file, false); + await instanceChart.updateDrive(file, false); } } From a3c7571670077a5baaf872eb4c014b3e0ff049db Mon Sep 17 00:00:00 2001 From: Johann150 Date: Tue, 23 May 2023 23:56:30 +0200 Subject: [PATCH 17/23] add missing comma --- packages/backend/src/models/repositories/user.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts index 6549c24c6..ddcb97f90 100644 --- a/packages/backend/src/models/repositories/user.ts +++ b/packages/backend/src/models/repositories/user.ts @@ -258,7 +258,7 @@ export const UserRepository = db.getRepository(User).extend({ case 'nobody': return false; } - } + }, async pack( src: User['id'] | User, From ffdb112867c13d137e2dc6f9843e0778500784f7 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Fri, 26 May 2023 20:53:47 +0200 Subject: [PATCH 18/23] foundkey-js: remove favorites --- packages/foundkey-js/src/api.types.ts | 5 +---- packages/foundkey-js/src/consts.ts | 2 -- packages/foundkey-js/src/entities.ts | 7 ------- 3 files changed, 1 insertion(+), 13 deletions(-) diff --git a/packages/foundkey-js/src/api.types.ts b/packages/foundkey-js/src/api.types.ts index 4c6b098f5..73dcf32da 100644 --- a/packages/foundkey-js/src/api.types.ts +++ b/packages/foundkey-js/src/api.types.ts @@ -1,7 +1,7 @@ import { Announcement, Antenna, App, AuthSession, Blocking, Channel, Clip, DateString, InstanceMetadata, DriveFile, DriveFolder, Following, FollowingFolloweePopulated, FollowingFollowerPopulated, FollowRequest, Instance, MeDetailed, - Note, NoteFavorite, OriginType, Page, ServerInfo, Stats, User, UserDetailed, UserGroup, UserList, UserSorting, Notification, NoteReaction, Signin, MessagingMessage, + Note, OriginType, Page, ServerInfo, Stats, User, UserDetailed, UserGroup, UserList, UserSorting, Notification, NoteReaction, Signin, MessagingMessage, } from './entities.js'; type TODO = Record | null; @@ -304,7 +304,6 @@ export type Endpoints = { 'i/export-mute': { req: TODO; res: TODO; }; 'i/export-notes': { req: TODO; res: TODO; }; 'i/export-user-lists': { req: TODO; res: TODO; }; - 'i/favorites': { req: { limit?: number; sinceId?: NoteFavorite['id']; untilId?: NoteFavorite['id']; }; res: NoteFavorite[]; }; 'i/get-word-muted-notes-count': { req: TODO; res: TODO; }; 'i/import-blocking': { req: TODO; res: TODO; }; 'i/import-following': { req: TODO; res: TODO; }; @@ -411,8 +410,6 @@ export type Endpoints = { }; }; res: { createdNote: Note }; }; 'notes/delete': { req: { noteId: Note['id']; }; res: null; }; - 'notes/favorites/create': { req: { noteId: Note['id']; }; res: null; }; - 'notes/favorites/delete': { req: { noteId: Note['id']; }; res: null; }; 'notes/featured': { req: TODO; res: Note[]; }; 'notes/global-timeline': { req: { limit?: number; sinceId?: Note['id']; untilId?: Note['id']; sinceDate?: number; untilDate?: number; }; res: Note[]; }; 'notes/hybrid-timeline': { req: { limit?: number; sinceId?: Note['id']; untilId?: Note['id']; sinceDate?: number; untilDate?: number; }; res: Note[]; }; diff --git a/packages/foundkey-js/src/consts.ts b/packages/foundkey-js/src/consts.ts index ed9fd8e53..1c941d3d5 100644 --- a/packages/foundkey-js/src/consts.ts +++ b/packages/foundkey-js/src/consts.ts @@ -13,8 +13,6 @@ export const permissions = [ 'write:blocks', 'read:drive', 'write:drive', - 'read:favorites', - 'write:favorites', 'read:following', 'write:following', 'read:messaging', diff --git a/packages/foundkey-js/src/entities.ts b/packages/foundkey-js/src/entities.ts index a83a8c123..173f0f28f 100644 --- a/packages/foundkey-js/src/entities.ts +++ b/packages/foundkey-js/src/entities.ts @@ -385,13 +385,6 @@ export type AuthSession = { export type Clip = TODO; -export type NoteFavorite = { - id: ID; - createdAt: DateString; - noteId: Note['id']; - note: Note; -}; - export type FollowRequest = { id: ID; follower: User; From 6695171b6aa28843f1cb49ebfbdaa1b0950e66f9 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Fri, 26 May 2023 20:57:45 +0200 Subject: [PATCH 19/23] remove outdated misskey-js api report --- packages/foundkey-js/etc/misskey-js.api.md | 2659 -------------------- 1 file changed, 2659 deletions(-) delete mode 100644 packages/foundkey-js/etc/misskey-js.api.md diff --git a/packages/foundkey-js/etc/misskey-js.api.md b/packages/foundkey-js/etc/misskey-js.api.md deleted file mode 100644 index 706f0216f..000000000 --- a/packages/foundkey-js/etc/misskey-js.api.md +++ /dev/null @@ -1,2659 +0,0 @@ -## API Report File for "misskey-js" - -> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). - -```ts - -import { EventEmitter } from 'eventemitter3'; - -// @public (undocumented) -export type Acct = { - username: string; - host: string | null; -}; - -// Warning: (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts -// -// @public (undocumented) -type Ad = TODO_2; - -// @public (undocumented) -type Announcement = { - id: ID; - createdAt: DateString; - updatedAt: DateString | null; - text: string; - title: string; - imageUrl: string | null; - isRead?: boolean; -}; - -// @public (undocumented) -type Antenna = { - id: ID; - createdAt: DateString; - name: string; - keywords: string[][]; - excludeKeywords: string[][]; - src: 'home' | 'all' | 'users' | 'list' | 'group'; - userListId: ID | null; - userGroupId: ID | null; - users: string[]; - caseSensitive: boolean; - notify: boolean; - withReplies: boolean; - withFile: boolean; - hasUnreadNote: boolean; -}; - -declare namespace api { - export { - isAPIError, - APIError, - FetchLike, - APIClient - } -} -export { api } - -// @public (undocumented) -class APIClient { - constructor(opts: { - origin: APIClient['origin']; - credential?: APIClient['credential']; - fetch?: APIClient['fetch'] | null | undefined; - }); - // (undocumented) - credential: string | null | undefined; - // (undocumented) - fetch: FetchLike; - // (undocumented) - origin: string; - // Warning: (ae-forgotten-export) The symbol "IsCaseMatched" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "GetCaseResult" needs to be exported by the entry point index.d.ts - // - // (undocumented) - request(endpoint: E, params?: P, credential?: string | null | undefined): Promise extends true ? GetCaseResult : IsCaseMatched extends true ? GetCaseResult : IsCaseMatched extends true ? GetCaseResult : IsCaseMatched extends true ? GetCaseResult : IsCaseMatched extends true ? GetCaseResult : IsCaseMatched extends true ? GetCaseResult : IsCaseMatched extends true ? GetCaseResult : IsCaseMatched extends true ? GetCaseResult : IsCaseMatched extends true ? GetCaseResult : IsCaseMatched extends true ? GetCaseResult : Endpoints[E]['res']['$switch']['$default'] : Endpoints[E]['res']>; -} - -// @public (undocumented) -type APIError = { - id: string; - code: string; - message: string; - kind: 'client' | 'server'; - info: Record; -}; - -// @public (undocumented) -type App = TODO_2; - -// @public (undocumented) -type AuthSession = { - id: ID; - app: App; - token: string; -}; - -// @public (undocumented) -type Blocking = { - id: ID; - createdAt: DateString; - blockeeId: User['id']; - blockee: UserDetailed; -}; - -// @public (undocumented) -type Channel = { - id: ID; -}; - -// Warning: (ae-forgotten-export) The symbol "AnyOf" needs to be exported by the entry point index.d.ts -// -// @public (undocumented) -export abstract class ChannelConnection = any> extends EventEmitter { - constructor(stream: Stream, channel: string, name?: string); - // (undocumented) - channel: string; - // (undocumented) - abstract dispose(): void; - // (undocumented) - abstract id: string; - // (undocumented) - inCount: number; - // (undocumented) - name?: string; - // (undocumented) - outCount: number; - // (undocumented) - send(type: T, body: Channel['receives'][T]): void; - // (undocumented) - protected stream: Stream; -} - -// @public (undocumented) -export type Channels = { - main: { - params: null; - events: { - notification: (payload: Notification_2) => void; - mention: (payload: Note) => void; - reply: (payload: Note) => void; - renote: (payload: Note) => void; - follow: (payload: User) => void; - followed: (payload: User) => void; - unfollow: (payload: User) => void; - meUpdated: (payload: MeDetailed) => void; - pageEvent: (payload: PageEvent) => void; - urlUploadFinished: (payload: { - marker: string; - file: DriveFile; - }) => void; - readAllNotifications: () => void; - unreadNotification: (payload: Notification_2) => void; - unreadMention: (payload: Note['id']) => void; - readAllUnreadMentions: () => void; - unreadSpecifiedNote: (payload: Note['id']) => void; - readAllUnreadSpecifiedNotes: () => void; - readAllMessagingMessages: () => void; - messagingMessage: (payload: MessagingMessage) => void; - unreadMessagingMessage: (payload: MessagingMessage) => void; - readAllAntennas: () => void; - unreadAntenna: (payload: Antenna) => void; - readAllAnnouncements: () => void; - readAllChannels: () => void; - unreadChannel: (payload: Note['id']) => void; - myTokenRegenerated: () => void; - reversiNoInvites: () => void; - reversiInvited: (payload: FIXME) => void; - signin: (payload: FIXME) => void; - registryUpdated: (payload: { - scope?: string[]; - key: string; - value: any | null; - }) => void; - driveFileCreated: (payload: DriveFile) => void; - readAntenna: (payload: Antenna) => void; - }; - receives: null; - }; - homeTimeline: { - params: null; - events: { - note: (payload: Note) => void; - }; - receives: null; - }; - localTimeline: { - params: null; - events: { - note: (payload: Note) => void; - }; - receives: null; - }; - hybridTimeline: { - params: null; - events: { - note: (payload: Note) => void; - }; - receives: null; - }; - globalTimeline: { - params: null; - events: { - note: (payload: Note) => void; - }; - receives: null; - }; - messaging: { - params: { - otherparty?: User['id'] | null; - group?: UserGroup['id'] | null; - }; - events: { - message: (payload: MessagingMessage) => void; - deleted: (payload: MessagingMessage['id']) => void; - read: (payload: MessagingMessage['id'][]) => void; - typers: (payload: User[]) => void; - }; - receives: { - read: { - id: MessagingMessage['id']; - }; - }; - }; - serverStats: { - params: null; - events: { - stats: (payload: FIXME) => void; - }; - receives: { - requestLog: { - id: string | number; - length: number; - }; - }; - }; - queueStats: { - params: null; - events: { - stats: (payload: FIXME) => void; - }; - receives: { - requestLog: { - id: string | number; - length: number; - }; - }; - }; -}; - -// @public (undocumented) -type Clip = TODO_2; - -// @public (undocumented) -type CustomEmoji = { - id: string; - name: string; - url: string; - category: string; - aliases: string[]; -}; - -// @public (undocumented) -type DateString = string; - -// @public (undocumented) -type DetailedInstanceMetadata = LiteInstanceMetadata & { - features: Record; -}; - -// @public (undocumented) -type DriveFile = { - id: ID; - createdAt: DateString; - isSensitive: boolean; - name: string; - thumbnailUrl: string; - url: string; - type: string; - size: number; - md5: string; - blurhash: string; - properties: Record; -}; - -// @public (undocumented) -type DriveFolder = TODO_2; - -// @public (undocumented) -export type Endpoints = { - 'admin/abuse-user-reports': { - req: TODO; - res: TODO; - }; - 'admin/delete-all-files-of-a-user': { - req: { - userId: User['id']; - }; - res: null; - }; - 'admin/delete-logs': { - req: NoParams; - res: null; - }; - 'admin/get-index-stats': { - req: TODO; - res: TODO; - }; - 'admin/get-table-stats': { - req: TODO; - res: TODO; - }; - 'admin/invite': { - req: TODO; - res: TODO; - }; - 'admin/logs': { - req: TODO; - res: TODO; - }; - 'admin/reset-password': { - req: TODO; - res: TODO; - }; - 'admin/resolve-abuse-user-report': { - req: TODO; - res: TODO; - }; - 'admin/resync-chart': { - req: TODO; - res: TODO; - }; - 'admin/send-email': { - req: TODO; - res: TODO; - }; - 'admin/server-info': { - req: TODO; - res: TODO; - }; - 'admin/show-moderation-logs': { - req: TODO; - res: TODO; - }; - 'admin/show-user': { - req: TODO; - res: TODO; - }; - 'admin/show-users': { - req: TODO; - res: TODO; - }; - 'admin/silence-user': { - req: TODO; - res: TODO; - }; - 'admin/suspend-user': { - req: TODO; - res: TODO; - }; - 'admin/unsilence-user': { - req: TODO; - res: TODO; - }; - 'admin/unsuspend-user': { - req: TODO; - res: TODO; - }; - 'admin/update-meta': { - req: TODO; - res: TODO; - }; - 'admin/vacuum': { - req: TODO; - res: TODO; - }; - 'admin/accounts/create': { - req: TODO; - res: TODO; - }; - 'admin/ad/create': { - req: TODO; - res: TODO; - }; - 'admin/ad/delete': { - req: { - id: Ad['id']; - }; - res: null; - }; - 'admin/ad/list': { - req: TODO; - res: TODO; - }; - 'admin/ad/update': { - req: TODO; - res: TODO; - }; - 'admin/announcements/create': { - req: TODO; - res: TODO; - }; - 'admin/announcements/delete': { - req: { - id: Announcement['id']; - }; - res: null; - }; - 'admin/announcements/list': { - req: TODO; - res: TODO; - }; - 'admin/announcements/update': { - req: TODO; - res: TODO; - }; - 'admin/drive/clean-remote-files': { - req: TODO; - res: TODO; - }; - 'admin/drive/cleanup': { - req: TODO; - res: TODO; - }; - 'admin/drive/files': { - req: TODO; - res: TODO; - }; - 'admin/drive/show-file': { - req: TODO; - res: TODO; - }; - 'admin/emoji/add': { - req: TODO; - res: TODO; - }; - 'admin/emoji/copy': { - req: TODO; - res: TODO; - }; - 'admin/emoji/list-remote': { - req: TODO; - res: TODO; - }; - 'admin/emoji/list': { - req: TODO; - res: TODO; - }; - 'admin/emoji/remove': { - req: TODO; - res: TODO; - }; - 'admin/emoji/update': { - req: TODO; - res: TODO; - }; - 'admin/federation/delete-all-files': { - req: { - host: string; - }; - res: null; - }; - 'admin/federation/refresh-remote-instance-metadata': { - req: TODO; - res: TODO; - }; - 'admin/federation/remove-all-following': { - req: TODO; - res: TODO; - }; - 'admin/federation/update-instance': { - req: TODO; - res: TODO; - }; - 'admin/moderators/add': { - req: TODO; - res: TODO; - }; - 'admin/moderators/remove': { - req: TODO; - res: TODO; - }; - 'admin/promo/create': { - req: TODO; - res: TODO; - }; - 'admin/queue/clear': { - req: TODO; - res: TODO; - }; - 'admin/queue/deliver-delayed': { - req: TODO; - res: TODO; - }; - 'admin/queue/inbox-delayed': { - req: TODO; - res: TODO; - }; - 'admin/queue/jobs': { - req: TODO; - res: TODO; - }; - 'admin/queue/stats': { - req: TODO; - res: TODO; - }; - 'admin/relays/add': { - req: TODO; - res: TODO; - }; - 'admin/relays/list': { - req: TODO; - res: TODO; - }; - 'admin/relays/remove': { - req: TODO; - res: TODO; - }; - 'announcements': { - req: { - limit?: number; - withUnreads?: boolean; - sinceId?: Announcement['id']; - untilId?: Announcement['id']; - }; - res: Announcement[]; - }; - 'antennas/create': { - req: TODO; - res: Antenna; - }; - 'antennas/delete': { - req: { - antennaId: Antenna['id']; - }; - res: null; - }; - 'antennas/list': { - req: NoParams; - res: Antenna[]; - }; - 'antennas/notes': { - req: { - antennaId: Antenna['id']; - limit?: number; - sinceId?: Note['id']; - untilId?: Note['id']; - }; - res: Note[]; - }; - 'antennas/show': { - req: { - antennaId: Antenna['id']; - }; - res: Antenna; - }; - 'antennas/update': { - req: TODO; - res: Antenna; - }; - 'ap/get': { - req: { - uri: string; - }; - res: Record; - }; - 'ap/show': { - req: { - uri: string; - }; - res: { - type: 'Note'; - object: Note; - } | { - type: 'User'; - object: UserDetailed; - }; - }; - 'app/create': { - req: TODO; - res: App; - }; - 'app/show': { - req: { - appId: App['id']; - }; - res: App; - }; - 'auth/accept': { - req: { - token: string; - }; - res: null; - }; - 'auth/session/generate': { - req: { - appSecret: string; - }; - res: { - token: string; - url: string; - }; - }; - 'auth/session/show': { - req: { - token: string; - }; - res: AuthSession; - }; - 'auth/session/userkey': { - req: { - appSecret: string; - token: string; - }; - res: { - accessToken: string; - user: User; - }; - }; - 'blocking/create': { - req: { - userId: User['id']; - }; - res: UserDetailed; - }; - 'blocking/delete': { - req: { - userId: User['id']; - }; - res: UserDetailed; - }; - 'blocking/list': { - req: { - limit?: number; - sinceId?: Blocking['id']; - untilId?: Blocking['id']; - }; - res: Blocking[]; - }; - 'channels/create': { - req: TODO; - res: TODO; - }; - 'channels/featured': { - req: TODO; - res: TODO; - }; - 'channels/follow': { - req: TODO; - res: TODO; - }; - 'channels/followed': { - req: TODO; - res: TODO; - }; - 'channels/owned': { - req: TODO; - res: TODO; - }; - 'channels/pin-note': { - req: TODO; - res: TODO; - }; - 'channels/show': { - req: TODO; - res: TODO; - }; - 'channels/timeline': { - req: TODO; - res: TODO; - }; - 'channels/unfollow': { - req: TODO; - res: TODO; - }; - 'channels/update': { - req: TODO; - res: TODO; - }; - 'charts/active-users': { - req: { - span: 'day' | 'hour'; - limit?: number; - offset?: number | null; - }; - res: { - local: { - users: number[]; - }; - remote: { - users: number[]; - }; - }; - }; - 'charts/drive': { - req: { - span: 'day' | 'hour'; - limit?: number; - offset?: number | null; - }; - res: { - local: { - decCount: number[]; - decSize: number[]; - incCount: number[]; - incSize: number[]; - totalCount: number[]; - totalSize: number[]; - }; - remote: { - decCount: number[]; - decSize: number[]; - incCount: number[]; - incSize: number[]; - totalCount: number[]; - totalSize: number[]; - }; - }; - }; - 'charts/federation': { - req: { - span: 'day' | 'hour'; - limit?: number; - offset?: number | null; - }; - res: { - instance: { - dec: number[]; - inc: number[]; - total: number[]; - }; - }; - }; - 'charts/hashtag': { - req: { - span: 'day' | 'hour'; - limit?: number; - offset?: number | null; - }; - res: TODO; - }; - 'charts/instance': { - req: { - span: 'day' | 'hour'; - limit?: number; - offset?: number | null; - host: string; - }; - res: { - drive: { - decFiles: number[]; - decUsage: number[]; - incFiles: number[]; - incUsage: number[]; - totalFiles: number[]; - totalUsage: number[]; - }; - followers: { - dec: number[]; - inc: number[]; - total: number[]; - }; - following: { - dec: number[]; - inc: number[]; - total: number[]; - }; - notes: { - dec: number[]; - inc: number[]; - total: number[]; - diffs: { - normal: number[]; - renote: number[]; - reply: number[]; - }; - }; - requests: { - failed: number[]; - received: number[]; - succeeded: number[]; - }; - users: { - dec: number[]; - inc: number[]; - total: number[]; - }; - }; - }; - 'charts/network': { - req: { - span: 'day' | 'hour'; - limit?: number; - offset?: number | null; - }; - res: TODO; - }; - 'charts/notes': { - req: { - span: 'day' | 'hour'; - limit?: number; - offset?: number | null; - }; - res: { - local: { - dec: number[]; - inc: number[]; - total: number[]; - diffs: { - normal: number[]; - renote: number[]; - reply: number[]; - }; - }; - remote: { - dec: number[]; - inc: number[]; - total: number[]; - diffs: { - normal: number[]; - renote: number[]; - reply: number[]; - }; - }; - }; - }; - 'charts/user/drive': { - req: { - span: 'day' | 'hour'; - limit?: number; - offset?: number | null; - userId: User['id']; - }; - res: { - decCount: number[]; - decSize: number[]; - incCount: number[]; - incSize: number[]; - totalCount: number[]; - totalSize: number[]; - }; - }; - 'charts/user/following': { - req: { - span: 'day' | 'hour'; - limit?: number; - offset?: number | null; - userId: User['id']; - }; - res: TODO; - }; - 'charts/user/notes': { - req: { - span: 'day' | 'hour'; - limit?: number; - offset?: number | null; - userId: User['id']; - }; - res: { - dec: number[]; - inc: number[]; - total: number[]; - diffs: { - normal: number[]; - renote: number[]; - reply: number[]; - }; - }; - }; - 'charts/user/reactions': { - req: { - span: 'day' | 'hour'; - limit?: number; - offset?: number | null; - userId: User['id']; - }; - res: TODO; - }; - 'charts/users': { - req: { - span: 'day' | 'hour'; - limit?: number; - offset?: number | null; - }; - res: { - local: { - dec: number[]; - inc: number[]; - total: number[]; - }; - remote: { - dec: number[]; - inc: number[]; - total: number[]; - }; - }; - }; - 'clips/add-note': { - req: TODO; - res: TODO; - }; - 'clips/create': { - req: TODO; - res: TODO; - }; - 'clips/delete': { - req: { - clipId: Clip['id']; - }; - res: null; - }; - 'clips/list': { - req: TODO; - res: TODO; - }; - 'clips/notes': { - req: TODO; - res: TODO; - }; - 'clips/show': { - req: TODO; - res: TODO; - }; - 'clips/update': { - req: TODO; - res: TODO; - }; - 'drive': { - req: NoParams; - res: { - capacity: number; - usage: number; - }; - }; - 'drive/files': { - req: { - folderId?: DriveFolder['id'] | null; - type?: DriveFile['type'] | null; - limit?: number; - sinceId?: DriveFile['id']; - untilId?: DriveFile['id']; - }; - res: DriveFile[]; - }; - 'drive/files/attached-notes': { - req: TODO; - res: TODO; - }; - 'drive/files/check-existence': { - req: TODO; - res: TODO; - }; - 'drive/files/create': { - req: TODO; - res: TODO; - }; - 'drive/files/delete': { - req: { - fileId: DriveFile['id']; - }; - res: null; - }; - 'drive/files/find-by-hash': { - req: TODO; - res: TODO; - }; - 'drive/files/find': { - req: { - name: string; - folderId?: DriveFolder['id'] | null; - }; - res: DriveFile[]; - }; - 'drive/files/show': { - req: { - fileId?: DriveFile['id']; - url?: string; - }; - res: DriveFile; - }; - 'drive/files/update': { - req: { - fileId: DriveFile['id']; - folderId?: DriveFolder['id'] | null; - name?: string; - isSensitive?: boolean; - comment?: string | null; - }; - res: DriveFile; - }; - 'drive/files/upload-from-url': { - req: { - url: string; - folderId?: DriveFolder['id'] | null; - isSensitive?: boolean; - comment?: string | null; - marker?: string | null; - force?: boolean; - }; - res: null; - }; - 'drive/folders': { - req: { - folderId?: DriveFolder['id'] | null; - limit?: number; - sinceId?: DriveFile['id']; - untilId?: DriveFile['id']; - }; - res: DriveFolder[]; - }; - 'drive/folders/create': { - req: { - name?: string; - parentId?: DriveFolder['id'] | null; - }; - res: DriveFolder; - }; - 'drive/folders/delete': { - req: { - folderId: DriveFolder['id']; - }; - res: null; - }; - 'drive/folders/find': { - req: { - name: string; - parentId?: DriveFolder['id'] | null; - }; - res: DriveFolder[]; - }; - 'drive/folders/show': { - req: { - folderId: DriveFolder['id']; - }; - res: DriveFolder; - }; - 'drive/folders/update': { - req: { - folderId: DriveFolder['id']; - name?: string; - parentId?: DriveFolder['id'] | null; - }; - res: DriveFolder; - }; - 'drive/stream': { - req: { - type?: DriveFile['type'] | null; - limit?: number; - sinceId?: DriveFile['id']; - untilId?: DriveFile['id']; - }; - res: DriveFile[]; - }; - 'endpoint': { - req: { - endpoint: string; - }; - res: { - params: { - name: string; - type: string; - }[]; - }; - }; - 'endpoints': { - req: NoParams; - res: string[]; - }; - 'federation/dns': { - req: { - host: string; - }; - res: { - a: string[]; - aaaa: string[]; - cname: string[]; - txt: string[]; - }; - }; - 'federation/followers': { - req: { - host: string; - limit?: number; - sinceId?: Following['id']; - untilId?: Following['id']; - }; - res: FollowingFolloweePopulated[]; - }; - 'federation/following': { - req: { - host: string; - limit?: number; - sinceId?: Following['id']; - untilId?: Following['id']; - }; - res: FollowingFolloweePopulated[]; - }; - 'federation/instances': { - req: { - host?: string | null; - blocked?: boolean | null; - notResponding?: boolean | null; - suspended?: boolean | null; - federating?: boolean | null; - subscribing?: boolean | null; - publishing?: boolean | null; - limit?: number; - offset?: number; - sort?: '+pubSub' | '-pubSub' | '+notes' | '-notes' | '+users' | '-users' | '+following' | '-following' | '+followers' | '-followers' | '+caughtAt' | '-caughtAt' | '+lastCommunicatedAt' | '-lastCommunicatedAt' | '+driveUsage' | '-driveUsage' | '+driveFiles' | '-driveFiles'; - }; - res: Instance[]; - }; - 'federation/show-instance': { - req: { - host: string; - }; - res: Instance; - }; - 'federation/update-remote-user': { - req: { - userId: User['id']; - }; - res: null; - }; - 'federation/users': { - req: { - host: string; - limit?: number; - sinceId?: User['id']; - untilId?: User['id']; - }; - res: UserDetailed[]; - }; - 'following/create': { - req: { - userId: User['id']; - }; - res: User; - }; - 'following/delete': { - req: { - userId: User['id']; - }; - res: User; - }; - 'following/requests/accept': { - req: { - userId: User['id']; - }; - res: null; - }; - 'following/requests/cancel': { - req: { - userId: User['id']; - }; - res: User; - }; - 'following/requests/list': { - req: NoParams; - res: FollowRequest[]; - }; - 'following/requests/reject': { - req: { - userId: User['id']; - }; - res: null; - }; - 'gallery/featured': { - req: TODO; - res: TODO; - }; - 'gallery/popular': { - req: TODO; - res: TODO; - }; - 'gallery/posts': { - req: TODO; - res: TODO; - }; - 'gallery/posts/create': { - req: TODO; - res: TODO; - }; - 'gallery/posts/delete': { - req: { - postId: GalleryPost['id']; - }; - res: null; - }; - 'gallery/posts/like': { - req: TODO; - res: TODO; - }; - 'gallery/posts/show': { - req: TODO; - res: TODO; - }; - 'gallery/posts/unlike': { - req: TODO; - res: TODO; - }; - 'gallery/posts/update': { - req: TODO; - res: TODO; - }; - 'games/reversi/games': { - req: TODO; - res: TODO; - }; - 'games/reversi/games/show': { - req: TODO; - res: TODO; - }; - 'games/reversi/games/surrender': { - req: TODO; - res: TODO; - }; - 'games/reversi/invitations': { - req: TODO; - res: TODO; - }; - 'games/reversi/match': { - req: TODO; - res: TODO; - }; - 'games/reversi/match/cancel': { - req: TODO; - res: TODO; - }; - 'get-online-users-count': { - req: NoParams; - res: { - count: number; - }; - }; - 'hashtags/list': { - req: TODO; - res: TODO; - }; - 'hashtags/search': { - req: TODO; - res: TODO; - }; - 'hashtags/show': { - req: TODO; - res: TODO; - }; - 'hashtags/trend': { - req: TODO; - res: TODO; - }; - 'hashtags/users': { - req: TODO; - res: TODO; - }; - 'i': { - req: NoParams; - res: User; - }; - 'i/apps': { - req: TODO; - res: TODO; - }; - 'i/authorized-apps': { - req: TODO; - res: TODO; - }; - 'i/change-password': { - req: TODO; - res: TODO; - }; - 'i/delete-account': { - req: { - password: string; - }; - res: null; - }; - 'i/export-blocking': { - req: TODO; - res: TODO; - }; - 'i/export-following': { - req: TODO; - res: TODO; - }; - 'i/export-mute': { - req: TODO; - res: TODO; - }; - 'i/export-notes': { - req: TODO; - res: TODO; - }; - 'i/export-user-lists': { - req: TODO; - res: TODO; - }; - 'i/favorites': { - req: { - limit?: number; - sinceId?: NoteFavorite['id']; - untilId?: NoteFavorite['id']; - }; - res: NoteFavorite[]; - }; - 'i/gallery/likes': { - req: TODO; - res: TODO; - }; - 'i/gallery/posts': { - req: TODO; - res: TODO; - }; - 'i/get-word-muted-notes-count': { - req: TODO; - res: TODO; - }; - 'i/import-following': { - req: TODO; - res: TODO; - }; - 'i/import-user-lists': { - req: TODO; - res: TODO; - }; - 'i/notifications': { - req: { - limit?: number; - sinceId?: Notification_2['id']; - untilId?: Notification_2['id']; - following?: boolean; - markAsRead?: boolean; - includeTypes?: Notification_2['type'][]; - excludeTypes?: Notification_2['type'][]; - }; - res: Notification_2[]; - }; - 'i/page-likes': { - req: TODO; - res: TODO; - }; - 'i/pages': { - req: TODO; - res: TODO; - }; - 'i/pin': { - req: { - noteId: Note['id']; - }; - res: MeDetailed; - }; - 'i/read-all-messaging-messages': { - req: TODO; - res: TODO; - }; - 'i/read-all-unread-notes': { - req: TODO; - res: TODO; - }; - 'i/read-announcement': { - req: TODO; - res: TODO; - }; - 'i/regenerate-token': { - req: { - password: string; - }; - res: null; - }; - 'i/registry/get-all': { - req: { - scope?: string[]; - }; - res: Record; - }; - 'i/registry/get-detail': { - req: { - key: string; - scope?: string[]; - }; - res: { - updatedAt: DateString; - value: any; - }; - }; - 'i/registry/get': { - req: { - key: string; - scope?: string[]; - }; - res: any; - }; - 'i/registry/keys-with-type': { - req: { - scope?: string[]; - }; - res: Record; - }; - 'i/registry/keys': { - req: { - scope?: string[]; - }; - res: string[]; - }; - 'i/registry/remove': { - req: { - key: string; - scope?: string[]; - }; - res: null; - }; - 'i/registry/scopes': { - req: NoParams; - res: string[][]; - }; - 'i/registry/set': { - req: { - key: string; - value: any; - scope?: string[]; - }; - res: null; - }; - 'i/revoke-token': { - req: TODO; - res: TODO; - }; - 'i/signin-history': { - req: { - limit?: number; - sinceId?: Signin['id']; - untilId?: Signin['id']; - }; - res: Signin[]; - }; - 'i/unpin': { - req: { - noteId: Note['id']; - }; - res: MeDetailed; - }; - 'i/update-email': { - req: { - password: string; - email?: string | null; - }; - res: MeDetailed; - }; - 'i/update': { - req: { - name?: string | null; - description?: string | null; - lang?: string | null; - location?: string | null; - birthday?: string | null; - avatarId?: DriveFile['id'] | null; - bannerId?: DriveFile['id'] | null; - fields?: { - name: string; - value: string; - }[]; - isLocked?: boolean; - isExplorable?: boolean; - hideOnlineStatus?: boolean; - carefulBot?: boolean; - autoAcceptFollowed?: boolean; - noCrawle?: boolean; - isBot?: boolean; - isCat?: boolean; - injectFeaturedNote?: boolean; - receiveAnnouncementEmail?: boolean; - alwaysMarkNsfw?: boolean; - mutedWords?: string[][]; - mutingNotificationTypes?: Notification_2['type'][]; - emailNotificationTypes?: string[]; - }; - res: MeDetailed; - }; - 'i/user-group-invites': { - req: TODO; - res: TODO; - }; - 'i/2fa/done': { - req: TODO; - res: TODO; - }; - 'i/2fa/key-done': { - req: TODO; - res: TODO; - }; - 'i/2fa/password-less': { - req: TODO; - res: TODO; - }; - 'i/2fa/register-key': { - req: TODO; - res: TODO; - }; - 'i/2fa/register': { - req: TODO; - res: TODO; - }; - 'i/2fa/remove-key': { - req: TODO; - res: TODO; - }; - 'i/2fa/unregister': { - req: TODO; - res: TODO; - }; - 'messaging/history': { - req: { - limit?: number; - group?: boolean; - }; - res: MessagingMessage[]; - }; - 'messaging/messages': { - req: { - userId?: User['id']; - groupId?: UserGroup['id']; - limit?: number; - sinceId?: MessagingMessage['id']; - untilId?: MessagingMessage['id']; - markAsRead?: boolean; - }; - res: MessagingMessage[]; - }; - 'messaging/messages/create': { - req: { - userId?: User['id']; - groupId?: UserGroup['id']; - text?: string; - fileId?: DriveFile['id']; - }; - res: MessagingMessage; - }; - 'messaging/messages/delete': { - req: { - messageId: MessagingMessage['id']; - }; - res: null; - }; - 'messaging/messages/read': { - req: { - messageId: MessagingMessage['id']; - }; - res: null; - }; - 'meta': { - req: { - detail?: boolean; - }; - res: { - $switch: { - $cases: [ - [ - { - detail: true; - }, - DetailedInstanceMetadata - ], - [ - { - detail: false; - }, - LiteInstanceMetadata - ], - [ - { - detail: boolean; - }, - LiteInstanceMetadata | DetailedInstanceMetadata - ] - ]; - $default: LiteInstanceMetadata; - }; - }; - }; - 'miauth/gen-token': { - req: TODO; - res: TODO; - }; - 'mute/create': { - req: TODO; - res: TODO; - }; - 'mute/delete': { - req: { - userId: User['id']; - }; - res: null; - }; - 'mute/list': { - req: TODO; - res: TODO; - }; - 'my/apps': { - req: TODO; - res: TODO; - }; - 'notes': { - req: { - limit?: number; - sinceId?: Note['id']; - untilId?: Note['id']; - }; - res: Note[]; - }; - 'notes/children': { - req: { - noteId: Note['id']; - limit?: number; - sinceId?: Note['id']; - untilId?: Note['id']; - }; - res: Note[]; - }; - 'notes/clips': { - req: TODO; - res: TODO; - }; - 'notes/conversation': { - req: TODO; - res: TODO; - }; - 'notes/create': { - req: { - visibility?: 'public' | 'home' | 'followers' | 'specified'; - visibleUserIds?: User['id'][]; - text?: null | string; - cw?: null | string; - viaMobile?: boolean; - localOnly?: boolean; - fileIds?: DriveFile['id'][]; - replyId?: null | Note['id']; - renoteId?: null | Note['id']; - channelId?: null | Channel['id']; - poll?: null | { - choices: string[]; - multiple?: boolean; - expiresAt?: null | number; - expiredAfter?: null | number; - }; - }; - res: { - createdNote: Note; - }; - }; - 'notes/delete': { - req: { - noteId: Note['id']; - }; - res: null; - }; - 'notes/favorites/create': { - req: { - noteId: Note['id']; - }; - res: null; - }; - 'notes/favorites/delete': { - req: { - noteId: Note['id']; - }; - res: null; - }; - 'notes/featured': { - req: TODO; - res: Note[]; - }; - 'notes/global-timeline': { - req: { - limit?: number; - sinceId?: Note['id']; - untilId?: Note['id']; - sinceDate?: number; - untilDate?: number; - }; - res: Note[]; - }; - 'notes/hybrid-timeline': { - req: { - limit?: number; - sinceId?: Note['id']; - untilId?: Note['id']; - sinceDate?: number; - untilDate?: number; - }; - res: Note[]; - }; - 'notes/local-timeline': { - req: { - limit?: number; - sinceId?: Note['id']; - untilId?: Note['id']; - sinceDate?: number; - untilDate?: number; - }; - res: Note[]; - }; - 'notes/mentions': { - req: { - following?: boolean; - limit?: number; - sinceId?: Note['id']; - untilId?: Note['id']; - }; - res: Note[]; - }; - 'notes/polls/recommendation': { - req: TODO; - res: TODO; - }; - 'notes/polls/vote': { - req: { - noteId: Note['id']; - choice: number; - }; - res: null; - }; - 'notes/reactions': { - req: { - noteId: Note['id']; - type?: string | null; - limit?: number; - }; - res: NoteReaction[]; - }; - 'notes/reactions/create': { - req: { - noteId: Note['id']; - reaction: string; - }; - res: null; - }; - 'notes/reactions/delete': { - req: { - noteId: Note['id']; - }; - res: null; - }; - 'notes/renotes': { - req: { - limit?: number; - sinceId?: Note['id']; - untilId?: Note['id']; - noteId: Note['id']; - }; - res: Note[]; - }; - 'notes/replies': { - req: { - limit?: number; - sinceId?: Note['id']; - untilId?: Note['id']; - noteId: Note['id']; - }; - res: Note[]; - }; - 'notes/search-by-tag': { - req: TODO; - res: TODO; - }; - 'notes/search': { - req: TODO; - res: TODO; - }; - 'notes/show': { - req: { - noteId: Note['id']; - }; - res: Note; - }; - 'notes/state': { - req: TODO; - res: TODO; - }; - 'notes/timeline': { - req: { - limit?: number; - sinceId?: Note['id']; - untilId?: Note['id']; - sinceDate?: number; - untilDate?: number; - }; - res: Note[]; - }; - 'notes/unrenote': { - req: { - noteId: Note['id']; - }; - res: null; - }; - 'notes/user-list-timeline': { - req: { - listId: UserList['id']; - limit?: number; - sinceId?: Note['id']; - untilId?: Note['id']; - sinceDate?: number; - untilDate?: number; - }; - res: Note[]; - }; - 'notes/watching/create': { - req: TODO; - res: TODO; - }; - 'notes/watching/delete': { - req: { - noteId: Note['id']; - }; - res: null; - }; - 'notifications/create': { - req: { - body: string; - header?: string | null; - icon?: string | null; - }; - res: null; - }; - 'notifications/mark-all-as-read': { - req: NoParams; - res: null; - }; - 'notifications/read': { - req: { - notificationId: Notification_2['id']; - }; - res: null; - }; - 'page-push': { - req: { - pageId: Page['id']; - event: string; - var?: any; - }; - res: null; - }; - 'pages/create': { - req: TODO; - res: Page; - }; - 'pages/delete': { - req: { - pageId: Page['id']; - }; - res: null; - }; - 'pages/featured': { - req: NoParams; - res: Page[]; - }; - 'pages/like': { - req: { - pageId: Page['id']; - }; - res: null; - }; - 'pages/show': { - req: { - pageId?: Page['id']; - name?: string; - username?: string; - }; - res: Page; - }; - 'pages/unlike': { - req: { - pageId: Page['id']; - }; - res: null; - }; - 'pages/update': { - req: TODO; - res: null; - }; - 'ping': { - req: NoParams; - res: { - pong: number; - }; - }; - 'pinned-users': { - req: TODO; - res: TODO; - }; - 'promo/read': { - req: TODO; - res: TODO; - }; - 'request-reset-password': { - req: { - username: string; - email: string; - }; - res: null; - }; - 'reset-password': { - req: { - token: string; - password: string; - }; - res: null; - }; - 'room/show': { - req: TODO; - res: TODO; - }; - 'room/update': { - req: TODO; - res: TODO; - }; - 'stats': { - req: NoParams; - res: Stats; - }; - 'server-info': { - req: NoParams; - res: ServerInfo; - }; - 'sw/register': { - req: TODO; - res: TODO; - }; - 'username/available': { - req: { - username: string; - }; - res: { - available: boolean; - }; - }; - 'users': { - req: { - limit?: number; - offset?: number; - sort?: UserSorting; - origin?: OriginType; - }; - res: User[]; - }; - 'users/clips': { - req: TODO; - res: TODO; - }; - 'users/followers': { - req: { - userId?: User['id']; - username?: User['username']; - host?: User['host'] | null; - limit?: number; - sinceId?: Following['id']; - untilId?: Following['id']; - }; - res: FollowingFollowerPopulated[]; - }; - 'users/following': { - req: { - userId?: User['id']; - username?: User['username']; - host?: User['host'] | null; - limit?: number; - sinceId?: Following['id']; - untilId?: Following['id']; - }; - res: FollowingFolloweePopulated[]; - }; - 'users/gallery/posts': { - req: TODO; - res: TODO; - }; - 'users/groups/create': { - req: TODO; - res: TODO; - }; - 'users/groups/delete': { - req: { - groupId: UserGroup['id']; - }; - res: null; - }; - 'users/groups/invitations/accept': { - req: TODO; - res: TODO; - }; - 'users/groups/invitations/reject': { - req: TODO; - res: TODO; - }; - 'users/groups/invite': { - req: TODO; - res: TODO; - }; - 'users/groups/joined': { - req: TODO; - res: TODO; - }; - 'users/groups/owned': { - req: TODO; - res: TODO; - }; - 'users/groups/pull': { - req: TODO; - res: TODO; - }; - 'users/groups/show': { - req: TODO; - res: TODO; - }; - 'users/groups/transfer': { - req: TODO; - res: TODO; - }; - 'users/groups/update': { - req: TODO; - res: TODO; - }; - 'users/lists/create': { - req: { - name: string; - }; - res: UserList; - }; - 'users/lists/delete': { - req: { - listId: UserList['id']; - }; - res: null; - }; - 'users/lists/list': { - req: NoParams; - res: UserList[]; - }; - 'users/lists/pull': { - req: { - listId: UserList['id']; - userId: User['id']; - }; - res: null; - }; - 'users/lists/push': { - req: { - listId: UserList['id']; - userId: User['id']; - }; - res: null; - }; - 'users/lists/show': { - req: { - listId: UserList['id']; - }; - res: UserList; - }; - 'users/lists/update': { - req: { - listId: UserList['id']; - name: string; - }; - res: UserList; - }; - 'users/notes': { - req: { - userId: User['id']; - limit?: number; - sinceId?: Note['id']; - untilId?: Note['id']; - sinceDate?: number; - untilDate?: number; - }; - res: Note[]; - }; - 'users/pages': { - req: TODO; - res: TODO; - }; - 'users/recommendation': { - req: TODO; - res: TODO; - }; - 'users/relation': { - req: TODO; - res: TODO; - }; - 'users/report-abuse': { - req: TODO; - res: TODO; - }; - 'users/search-by-username-and-host': { - req: TODO; - res: TODO; - }; - 'users/search': { - req: TODO; - res: TODO; - }; - 'users/show': { - req: ShowUserReq | { - userIds: User['id'][]; - }; - res: { - $switch: { - $cases: [ - [ - { - userIds: User['id'][]; - }, - UserDetailed[] - ] - ]; - $default: UserDetailed; - }; - }; - }; - 'users/stats': { - req: TODO; - res: TODO; - }; -}; - -declare namespace entities { - export { - ID, - DateString, - User, - UserLite, - UserDetailed, - UserGroup, - UserList, - MeDetailed, - DriveFile, - DriveFolder, - GalleryPost, - Note, - NoteReaction, - Notification_2 as Notification, - MessagingMessage, - CustomEmoji, - LiteInstanceMetadata, - DetailedInstanceMetadata, - InstanceMetadata, - ServerInfo, - Stats, - Page, - PageEvent, - Announcement, - Antenna, - App, - AuthSession, - Ad, - Clip, - NoteFavorite, - FollowRequest, - Channel, - Following, - FollowingFolloweePopulated, - FollowingFollowerPopulated, - Blocking, - Instance, - Signin, - UserSorting, - OriginType - } -} -export { entities } - -// @public (undocumented) -type FetchLike = (input: string, init?: { - method?: string; - body?: string; - credentials?: RequestCredentials; - cache?: RequestCache; -}) => Promise<{ - status: number; - json(): Promise; -}>; - -// @public (undocumented) -export const ffVisibility: readonly ["public", "followers", "private"]; - -// @public (undocumented) -type Following = { - id: ID; - createdAt: DateString; - followerId: User['id']; - followeeId: User['id']; -}; - -// @public (undocumented) -type FollowingFolloweePopulated = Following & { - followee: UserDetailed; -}; - -// @public (undocumented) -type FollowingFollowerPopulated = Following & { - follower: UserDetailed; -}; - -// @public (undocumented) -type FollowRequest = { - id: ID; - follower: User; - followee: User; -}; - -// @public (undocumented) -type GalleryPost = TODO_2; - -// @public (undocumented) -type ID = string; - -// @public (undocumented) -type Instance = { - id: ID; - caughtAt: DateString; - host: string; - usersCount: number; - notesCount: number; - followingCount: number; - followersCount: number; - driveUsage: number; - driveFiles: number; - latestRequestSentAt: DateString | null; - latestStatus: number | null; - latestRequestReceivedAt: DateString | null; - lastCommunicatedAt: DateString; - isNotResponding: boolean; - isSuspended: boolean; - softwareName: string | null; - softwareVersion: string | null; - openRegistrations: boolean | null; - name: string | null; - description: string | null; - maintainerName: string | null; - maintainerEmail: string | null; - iconUrl: string | null; - faviconUrl: string | null; - themeColor: string | null; - infoUpdatedAt: DateString | null; -}; - -// @public (undocumented) -type InstanceMetadata = LiteInstanceMetadata | DetailedInstanceMetadata; - -// @public (undocumented) -function isAPIError(reason: any): reason is APIError; - -// @public (undocumented) -type LiteInstanceMetadata = { - maintainerName: string | null; - maintainerEmail: string | null; - version: string; - name: string | null; - uri: string; - description: string | null; - tosUrl: string | null; - disableRegistration: boolean; - disableLocalTimeline: boolean; - disableGlobalTimeline: boolean; - driveCapacityPerLocalUserMb: number; - driveCapacityPerRemoteUserMb: number; - enableHcaptcha: boolean; - hcaptchaSiteKey: string | null; - enableRecaptcha: boolean; - recaptchaSiteKey: string | null; - swPublickey: string | null; - maxNoteTextLength: number; - enableEmail: boolean; - enableTwitterIntegration: boolean; - enableGithubIntegration: boolean; - enableDiscordIntegration: boolean; - enableServiceWorker: boolean; - emojis: CustomEmoji[]; - ads: { - id: ID; - ratio: number; - place: string; - url: string; - imageUrl: string; - }[]; -}; - -// @public (undocumented) -type MeDetailed = UserDetailed & { - avatarId: DriveFile['id']; - bannerId: DriveFile['id']; - autoAcceptFollowed: boolean; - alwaysMarkNsfw: boolean; - carefulBot: boolean; - emailNotificationTypes: string[]; - hasPendingReceivedFollowRequest: boolean; - hasUnreadAnnouncement: boolean; - hasUnreadAntenna: boolean; - hasUnreadChannel: boolean; - hasUnreadMentions: boolean; - hasUnreadMessagingMessage: boolean; - hasUnreadNotification: boolean; - hasUnreadSpecifiedNotes: boolean; - hideOnlineStatus: boolean; - injectFeaturedNote: boolean; - integrations: Record; - isDeleted: boolean; - isExplorable: boolean; - mutedWords: string[][]; - mutingNotificationTypes: string[]; - noCrawle: boolean; - receiveAnnouncementEmail: boolean; - usePasswordLessLogin: boolean; - [other: string]: any; -}; - -// @public (undocumented) -type MessagingMessage = { - id: ID; - createdAt: DateString; - file: DriveFile | null; - fileId: DriveFile['id'] | null; - isRead: boolean; - reads: User['id'][]; - text: string | null; - user: User; - userId: User['id']; - recipient?: User | null; - recipientId: User['id'] | null; - group?: UserGroup | null; - groupId: UserGroup['id'] | null; -}; - -// @public (undocumented) -export const mutedNoteReasons: readonly ["word", "manual", "spam", "other"]; - -// @public (undocumented) -type Note = { - id: ID; - createdAt: DateString; - text: string | null; - cw: string | null; - user: User; - userId: User['id']; - reply?: Note; - replyId: Note['id']; - renote?: Note; - renoteId: Note['id']; - files: DriveFile[]; - fileIds: DriveFile['id'][]; - visibility: 'public' | 'home' | 'followers' | 'specified'; - visibleUserIds?: User['id'][]; - localOnly?: boolean; - myReaction?: string; - reactions: Record; - renoteCount: number; - repliesCount: number; - poll?: { - expiresAt: DateString | null; - multiple: boolean; - choices: { - isVoted: boolean; - text: string; - votes: number; - }[]; - }; - emojis: { - name: string; - url: string; - }[]; - uri?: string; - url?: string; - isHidden?: boolean; -}; - -// @public (undocumented) -type NoteFavorite = { - id: ID; - createdAt: DateString; - noteId: Note['id']; - note: Note; -}; - -// @public (undocumented) -type NoteReaction = { - id: ID; - createdAt: DateString; - user: UserLite; - type: string; -}; - -// @public (undocumented) -export const noteVisibilities: readonly ["public", "home", "followers", "specified"]; - -// @public (undocumented) -type Notification_2 = { - id: ID; - createdAt: DateString; - isRead: boolean; -} & ({ - type: 'reaction'; - reaction: string; - user: User; - userId: User['id']; - note: Note; -} | { - type: 'reply'; - user: User; - userId: User['id']; - note: Note; -} | { - type: 'renote'; - user: User; - userId: User['id']; - note: Note; -} | { - type: 'quote'; - user: User; - userId: User['id']; - note: Note; -} | { - type: 'mention'; - user: User; - userId: User['id']; - note: Note; -} | { - type: 'pollVote'; - user: User; - userId: User['id']; - note: Note; -} | { - type: 'follow'; - user: User; - userId: User['id']; -} | { - type: 'followRequestAccepted'; - user: User; - userId: User['id']; -} | { - type: 'receiveFollowRequest'; - user: User; - userId: User['id']; -} | { - type: 'groupInvited'; - invitation: UserGroup; - user: User; - userId: User['id']; -} | { - type: 'app'; - header?: string | null; - body: string; - icon?: string | null; -}); - -// @public (undocumented) -export const notificationTypes: readonly ["follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app"]; - -// @public (undocumented) -type OriginType = 'combined' | 'local' | 'remote'; - -// @public (undocumented) -type Page = { - id: ID; - createdAt: DateString; - updatedAt: DateString; - userId: User['id']; - user: User; - content: Record[]; - variables: Record[]; - title: string; - name: string; - summary: string | null; - hideTitleWhenPinned: boolean; - alignCenter: boolean; - font: string; - script: string; - eyeCatchingImageId: DriveFile['id'] | null; - eyeCatchingImage: DriveFile | null; - attachedFiles: any; - likedCount: number; - isLiked?: boolean; -}; - -// @public (undocumented) -type PageEvent = { - pageId: Page['id']; - event: string; - var: any; - userId: User['id']; - user: User; -}; - -// @public (undocumented) -export const permissions: string[]; - -// @public (undocumented) -type ServerInfo = { - machine: string; - cpu: { - model: string; - cores: number; - }; - mem: { - total: number; - }; - fs: { - total: number; - used: number; - }; -}; - -// @public (undocumented) -type Signin = { - id: ID; - createdAt: DateString; - ip: string; - headers: Record; - success: boolean; -}; - -// @public (undocumented) -type Stats = { - notesCount: number; - originalNotesCount: number; - usersCount: number; - originalUsersCount: number; - instances: number; - driveUsageLocal: number; - driveUsageRemote: number; -}; - -// Warning: (ae-forgotten-export) The symbol "StreamEvents" needs to be exported by the entry point index.d.ts -// -// @public (undocumented) -export class Stream extends EventEmitter { - constructor(origin: string, user: { - token: string; - } | null, options?: { - WebSocket?: any; - }); - // (undocumented) - close(): void; - // Warning: (ae-forgotten-export) The symbol "NonSharedConnection" needs to be exported by the entry point index.d.ts - // - // (undocumented) - disconnectToChannel(connection: NonSharedConnection): void; - // Warning: (ae-forgotten-export) The symbol "SharedConnection" needs to be exported by the entry point index.d.ts - // - // (undocumented) - removeSharedConnection(connection: SharedConnection): void; - // Warning: (ae-forgotten-export) The symbol "Pool" needs to be exported by the entry point index.d.ts - // - // (undocumented) - removeSharedConnectionPool(pool: Pool): void; - // (undocumented) - send(typeOrPayload: any, payload?: any): void; - // (undocumented) - state: 'initializing' | 'reconnecting' | 'connected'; - // (undocumented) - useChannel(channel: C, params?: Channels[C]['params'], name?: string): ChannelConnection; -} - -// @public (undocumented) -type User = UserLite | UserDetailed; - -// @public (undocumented) -type UserDetailed = UserLite & { - bannerBlurhash: string | null; - bannerColor: string | null; - bannerUrl: string | null; - birthday: string | null; - createdAt: DateString; - description: string | null; - ffVisibility: 'public' | 'followers' | 'private'; - fields: { - name: string; - value: string; - }[]; - followersCount: number; - followingCount: number; - hasPendingFollowRequestFromYou: boolean; - hasPendingFollowRequestToYou: boolean; - isAdmin: boolean; - isBlocked: boolean; - isBlocking: boolean; - isBot: boolean; - isCat: boolean; - isFollowed: boolean; - isFollowing: boolean; - isLocked: boolean; - isModerator: boolean; - isMuted: boolean; - isSilenced: boolean; - isSuspended: boolean; - lang: string | null; - lastFetchedAt?: DateString; - location: string | null; - notesCount: number; - pinnedNoteIds: ID[]; - pinnedNotes: Note[]; - pinnedPage: Page | null; - pinnedPageId: string | null; - publicReactions: boolean; - securityKeys: boolean; - twoFactorEnabled: boolean; - updatedAt: DateString | null; - uri: string | null; - url: string | null; -}; - -// @public (undocumented) -type UserGroup = TODO_2; - -// @public (undocumented) -type UserList = { - id: ID; - createdAt: DateString; - name: string; - userIds: User['id'][]; -}; - -// @public (undocumented) -type UserLite = { - id: ID; - username: string; - host: string | null; - name: string; - onlineStatus: 'online' | 'active' | 'offline' | 'unknown'; - avatarUrl: string; - avatarBlurhash: string; - emojis: { - name: string; - url: string; - }[]; - instance?: { - name: Instance['name']; - softwareName: Instance['softwareName']; - softwareVersion: Instance['softwareVersion']; - iconUrl: Instance['iconUrl']; - faviconUrl: Instance['faviconUrl']; - themeColor: Instance['themeColor']; - }; -}; - -// @public (undocumented) -type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+updatedAt' | '-updatedAt'; - -// Warnings were encountered during analysis: -// -// src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts -// src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts -// src/api.types.ts:595:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts -// src/streaming.types.ts:35:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts - -// (No @packageDocumentation comment for this package) - -``` From de81fac33453b72e76ae5bdc3186da45fc0c55e6 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Fri, 26 May 2023 20:56:47 +0200 Subject: [PATCH 20/23] client: remove favorites --- packages/client/src/menu.ts | 6 --- packages/client/src/pages/favorites.vue | 50 -------------------- packages/client/src/router.ts | 4 -- packages/client/src/scripts/get-note-menu.ts | 15 ------ packages/client/src/store.ts | 1 - 5 files changed, 76 deletions(-) delete mode 100644 packages/client/src/pages/favorites.vue diff --git a/packages/client/src/menu.ts b/packages/client/src/menu.ts index 1de63b4dc..8fc98b33a 100644 --- a/packages/client/src/menu.ts +++ b/packages/client/src/menu.ts @@ -120,12 +120,6 @@ export const menuDef = reactive({ indicated: computed(() => $i != null && $i.hasUnreadSpecifiedNotes), to: '/my/notifications#directNotes', }, - favorites: { - title: 'favorites', - icon: 'fas fa-star', - show: computed(() => $i != null), - to: '/my/favorites', - }, pages: { title: 'pages', icon: 'fas fa-file-alt', diff --git a/packages/client/src/pages/favorites.vue b/packages/client/src/pages/favorites.vue deleted file mode 100644 index f9e0b552a..000000000 --- a/packages/client/src/pages/favorites.vue +++ /dev/null @@ -1,50 +0,0 @@ - - - - - diff --git a/packages/client/src/router.ts b/packages/client/src/router.ts index 6ba2c09ef..6b9d82b57 100644 --- a/packages/client/src/router.ts +++ b/packages/client/src/router.ts @@ -148,10 +148,6 @@ export const routes = [{ component: page(() => import('./pages/notifications.vue')), hash: 'initialTab', loginRequired: true, -}, { - path: '/my/favorites', - component: page(() => import('./pages/favorites.vue')), - loginRequired: true, }, { name: 'messaging', path: '/my/messaging', diff --git a/packages/client/src/scripts/get-note-menu.ts b/packages/client/src/scripts/get-note-menu.ts index 73b960cae..61fc4cc73 100644 --- a/packages/client/src/scripts/get-note-menu.ts +++ b/packages/client/src/scripts/get-note-menu.ts @@ -48,12 +48,6 @@ export function getNoteMenu(props: { }); } - function toggleFavorite(favorite: boolean): void { - os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', { - noteId: appearNote.id, - }); - } - function toggleWatch(watch: boolean): void { os.apiWithDialog(watch ? 'notes/watching/create' : 'notes/watching/delete', { noteId: appearNote.id, @@ -244,15 +238,6 @@ export function getNoteMenu(props: { action: translate, } : undefined, null, - statePromise.then(state => state.isFavorited ? { - icon: 'fas fa-star', - text: i18n.ts.unfavorite, - action: () => toggleFavorite(false), - } : { - icon: 'fas fa-star', - text: i18n.ts.favorite, - action: () => toggleFavorite(true), - }), { icon: 'fas fa-paperclip', text: i18n.ts.clip, diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts index 489cca84d..a267a3634 100644 --- a/packages/client/src/store.ts +++ b/packages/client/src/store.ts @@ -59,7 +59,6 @@ export const defaultStore = markRaw(new Storage('base', { where: 'deviceAccount', default: [ 'notifications', - 'favorites', 'drive', 'followRequests', '-', From aa7171e116f0f031d061aa1df0cdb9c83da5cadd Mon Sep 17 00:00:00 2001 From: Johann150 Date: Fri, 26 May 2023 20:52:31 +0200 Subject: [PATCH 21/23] server: remove favorites --- packages/backend/src/db/postgre.ts | 2 - packages/backend/src/misc/api-permissions.ts | 2 - packages/backend/src/misc/schema.ts | 2 - .../src/models/entities/note-favorite.ts | 35 -------------- packages/backend/src/models/index.ts | 2 - .../src/models/repositories/note-favorite.ts | 29 ----------- .../src/models/schema/note-favorite.ts | 26 ---------- packages/backend/src/server/api/endpoints.ts | 6 --- .../src/server/api/endpoints/i/favorites.ts | 44 ----------------- .../api/endpoints/notes/favorites/create.ts | 48 ------------------- .../api/endpoints/notes/favorites/delete.ts | 42 ---------------- .../src/server/api/endpoints/notes/state.ts | 16 +------ .../src/server/api/endpoints/users/stats.ts | 9 +--- packages/backend/src/server/api/error.ts | 8 ---- 14 files changed, 3 insertions(+), 268 deletions(-) delete mode 100644 packages/backend/src/models/entities/note-favorite.ts delete mode 100644 packages/backend/src/models/repositories/note-favorite.ts delete mode 100644 packages/backend/src/models/schema/note-favorite.ts delete mode 100644 packages/backend/src/server/api/endpoints/i/favorites.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/favorites/create.ts delete mode 100644 packages/backend/src/server/api/endpoints/notes/favorites/delete.ts diff --git a/packages/backend/src/db/postgre.ts b/packages/backend/src/db/postgre.ts index b59142597..d8c31ae96 100644 --- a/packages/backend/src/db/postgre.ts +++ b/packages/backend/src/db/postgre.ts @@ -33,7 +33,6 @@ import { UserGroup } from '@/models/entities/user-group.js'; import { UserGroupJoining } from '@/models/entities/user-group-joining.js'; import { UserGroupInvitation } from '@/models/entities/user-group-invitation.js'; import { Hashtag } from '@/models/entities/hashtag.js'; -import { NoteFavorite } from '@/models/entities/note-favorite.js'; import { AbuseUserReport } from '@/models/entities/abuse-user-report.js'; import { RegistrationTicket } from '@/models/entities/registration-tickets.js'; import { MessagingMessage } from '@/models/entities/messaging-message.js'; @@ -134,7 +133,6 @@ export const entities = [ RenoteMuting, Blocking, Note, - NoteFavorite, NoteReaction, NoteWatching, NoteThreadMuting, diff --git a/packages/backend/src/misc/api-permissions.ts b/packages/backend/src/misc/api-permissions.ts index d7c115a50..17ae0d99d 100644 --- a/packages/backend/src/misc/api-permissions.ts +++ b/packages/backend/src/misc/api-permissions.ts @@ -5,8 +5,6 @@ export const kinds = [ 'write:blocks', 'read:drive', 'write:drive', - 'read:favorites', - 'write:favorites', 'read:following', 'write:following', 'read:messaging', diff --git a/packages/backend/src/misc/schema.ts b/packages/backend/src/misc/schema.ts index 1bb07d14e..6b5fcf0f5 100644 --- a/packages/backend/src/misc/schema.ts +++ b/packages/backend/src/misc/schema.ts @@ -23,7 +23,6 @@ import { packedHashtagSchema } from '@/models/schema/hashtag.js'; import { packedPageSchema } from '@/models/schema/page.js'; import { packedUserGroupSchema } from '@/models/schema/user-group.js'; import { packedUserGroupInvitationSchema } from '@/models/schema/user-group-invitation.js'; -import { packedNoteFavoriteSchema } from '@/models/schema/note-favorite.js'; import { packedChannelSchema } from '@/models/schema/channel.js'; import { packedAntennaSchema } from '@/models/schema/antenna.js'; import { packedClipSchema } from '@/models/schema/clip.js'; @@ -47,7 +46,6 @@ export const refs = { MessagingMessage: packedMessagingMessageSchema, Note: packedNoteSchema, NoteReaction: packedNoteReactionSchema, - NoteFavorite: packedNoteFavoriteSchema, Notification: packedNotificationSchema, DriveFile: packedDriveFileSchema, DriveFolder: packedDriveFolderSchema, diff --git a/packages/backend/src/models/entities/note-favorite.ts b/packages/backend/src/models/entities/note-favorite.ts deleted file mode 100644 index 8b4449c3e..000000000 --- a/packages/backend/src/models/entities/note-favorite.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { id } from '../id.js'; -import { Note } from './note.js'; -import { User } from './user.js'; - -@Entity() -@Index(['userId', 'noteId'], { unique: true }) -export class NoteFavorite { - @PrimaryColumn(id()) - public id: string; - - @Column('timestamp with time zone', { - comment: 'The created date of the NoteFavorite.', - }) - public createdAt: Date; - - @Index() - @Column(id()) - public userId: User['id']; - - @ManyToOne(() => User, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public user: User | null; - - @Column(id()) - public noteId: Note['id']; - - @ManyToOne(() => Note, { - onDelete: 'CASCADE', - }) - @JoinColumn() - public note: Note | null; -} diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts index f4aa84d5b..7ba383c4a 100644 --- a/packages/backend/src/models/index.ts +++ b/packages/backend/src/models/index.ts @@ -29,7 +29,6 @@ import { RenoteMutingRepository } from './repositories/renote-muting.js'; import { BlockingRepository } from './repositories/blocking.js'; import { NoteReactionRepository } from './repositories/note-reaction.js'; import { NotificationRepository } from './repositories/notification.js'; -import { NoteFavoriteRepository } from './repositories/note-favorite.js'; import { UserPublickey } from './entities/user-publickey.js'; import { UserKeypair } from './entities/user-keypair.js'; import { AppRepository } from './repositories/app.js'; @@ -64,7 +63,6 @@ export const Announcements = db.getRepository(Announcement); export const AnnouncementReads = db.getRepository(AnnouncementRead); export const Apps = (AppRepository); export const Notes = (NoteRepository); -export const NoteFavorites = (NoteFavoriteRepository); export const NoteWatchings = db.getRepository(NoteWatching); export const NoteThreadMutings = db.getRepository(NoteThreadMuting); export const NoteReactions = (NoteReactionRepository); diff --git a/packages/backend/src/models/repositories/note-favorite.ts b/packages/backend/src/models/repositories/note-favorite.ts deleted file mode 100644 index 47d549455..000000000 --- a/packages/backend/src/models/repositories/note-favorite.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { db } from '@/db/postgre.js'; -import { NoteFavorite } from '@/models/entities/note-favorite.js'; -import { User } from '@/models/entities/user.js'; -import { Notes } from '../index.js'; - -export const NoteFavoriteRepository = db.getRepository(NoteFavorite).extend({ - async pack( - src: NoteFavorite['id'] | NoteFavorite, - me?: { id: User['id'] } | null | undefined, - ) { - const favorite = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); - - return { - id: favorite.id, - createdAt: favorite.createdAt.toISOString(), - noteId: favorite.noteId, - // may throw error - note: await Notes.pack(favorite.note || favorite.noteId, me), - }; - }, - - packMany( - favorites: any[], - me: { id: User['id'] }, - ) { - return Promise.allSettled(favorites.map(x => this.pack(x, me))) - .then(promises => promises.flatMap(result => result.status === 'fulfilled' ? [result.value] : [])); - }, -}); diff --git a/packages/backend/src/models/schema/note-favorite.ts b/packages/backend/src/models/schema/note-favorite.ts deleted file mode 100644 index d133f7367..000000000 --- a/packages/backend/src/models/schema/note-favorite.ts +++ /dev/null @@ -1,26 +0,0 @@ -export const packedNoteFavoriteSchema = { - type: 'object', - properties: { - id: { - type: 'string', - optional: false, nullable: false, - format: 'id', - example: 'xxxxxxxxxx', - }, - createdAt: { - type: 'string', - optional: false, nullable: false, - format: 'date-time', - }, - note: { - type: 'object', - optional: false, nullable: false, - ref: 'Note', - }, - noteId: { - type: 'string', - optional: false, nullable: false, - format: 'id', - }, - }, -} as const; diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index a7757c2b5..c5249191e 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -162,7 +162,6 @@ import * as ep___i_exportFollowing from './endpoints/i/export-following.js'; import * as ep___i_exportMute from './endpoints/i/export-mute.js'; import * as ep___i_exportNotes from './endpoints/i/export-notes.js'; import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js'; -import * as ep___i_favorites from './endpoints/i/favorites.js'; import * as ep___i_getWordMutedNotesCount from './endpoints/i/get-word-muted-notes-count.js'; import * as ep___i_importBlocking from './endpoints/i/import-blocking.js'; import * as ep___i_importFollowing from './endpoints/i/import-following.js'; @@ -215,8 +214,6 @@ import * as ep___notes_clips from './endpoints/notes/clips.js'; import * as ep___notes_conversation from './endpoints/notes/conversation.js'; import * as ep___notes_create from './endpoints/notes/create.js'; import * as ep___notes_delete from './endpoints/notes/delete.js'; -import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js'; -import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js'; import * as ep___notes_featured from './endpoints/notes/featured.js'; import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js'; import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js'; @@ -459,7 +456,6 @@ const eps = [ ['i/export-mute', ep___i_exportMute], ['i/export-notes', ep___i_exportNotes], ['i/export-user-lists', ep___i_exportUserLists], - ['i/favorites', ep___i_favorites], ['i/get-word-muted-notes-count', ep___i_getWordMutedNotesCount], ['i/import-blocking', ep___i_importBlocking], ['i/import-following', ep___i_importFollowing], @@ -512,8 +508,6 @@ const eps = [ ['notes/conversation', ep___notes_conversation], ['notes/create', ep___notes_create], ['notes/delete', ep___notes_delete], - ['notes/favorites/create', ep___notes_favorites_create], - ['notes/favorites/delete', ep___notes_favorites_delete], ['notes/featured', ep___notes_featured], ['notes/global-timeline', ep___notes_globalTimeline], ['notes/hybrid-timeline', ep___notes_hybridTimeline], diff --git a/packages/backend/src/server/api/endpoints/i/favorites.ts b/packages/backend/src/server/api/endpoints/i/favorites.ts deleted file mode 100644 index ddda42a6d..000000000 --- a/packages/backend/src/server/api/endpoints/i/favorites.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { NoteFavorites } from '@/models/index.js'; -import define from '@/server/api/define.js'; -import { makePaginationQuery } from '@/server/api/common/make-pagination-query.js'; - -export const meta = { - tags: ['account', 'notes', 'favorites'], - - requireCredential: true, - - kind: 'read:favorites', - - res: { - type: 'array', - optional: false, nullable: false, - items: { - type: 'object', - optional: false, nullable: false, - ref: 'NoteFavorite', - }, - }, -} as const; - -export const paramDef = { - type: 'object', - properties: { - limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, - sinceId: { type: 'string', format: 'misskey:id' }, - untilId: { type: 'string', format: 'misskey:id' }, - }, - required: [], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - const query = makePaginationQuery(NoteFavorites.createQueryBuilder('favorite'), ps.sinceId, ps.untilId) - .andWhere('favorite.userId = :meId', { meId: user.id }) - .leftJoinAndSelect('favorite.note', 'note'); - - const favorites = await query - .take(ps.limit) - .getMany(); - - return await NoteFavorites.packMany(favorites, user); -}); diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts b/packages/backend/src/server/api/endpoints/notes/favorites/create.ts deleted file mode 100644 index f9f769ace..000000000 --- a/packages/backend/src/server/api/endpoints/notes/favorites/create.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { NoteFavorites } from '@/models/index.js'; -import { genId } from '@/misc/gen-id.js'; -import define from '@/server/api/define.js'; -import { ApiError } from '@/server/api/error.js'; -import { getNote } from '@/server/api/common/getters.js'; - -export const meta = { - tags: ['notes', 'favorites'], - - requireCredential: true, - - kind: 'write:favorites', - - errors: ['NO_SUCH_NOTE', 'ALREADY_FAVORITED'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - noteId: { type: 'string', format: 'misskey:id' }, - }, - required: ['noteId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Get favoritee - const note = await getNote(ps.noteId, user).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE'); - throw err; - }); - - // if already favorited - const exist = await NoteFavorites.countBy({ - noteId: note.id, - userId: user.id, - }); - - if (exist) throw new ApiError('ALREADY_FAVORITED'); - - // Create favorite - await NoteFavorites.insert({ - id: genId(), - createdAt: new Date(), - noteId: note.id, - userId: user.id, - }); -}); diff --git a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts b/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts deleted file mode 100644 index 416c70062..000000000 --- a/packages/backend/src/server/api/endpoints/notes/favorites/delete.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { NoteFavorites } from '@/models/index.js'; -import define from '@/server/api/define.js'; -import { ApiError } from '@/server/api/error.js'; -import { getNote } from '@/server/api/common/getters.js'; - -export const meta = { - tags: ['notes', 'favorites'], - - requireCredential: true, - - kind: 'write:favorites', - - errors: ['NO_SUCH_NOTE', 'NOT_FAVORITED'], -} as const; - -export const paramDef = { - type: 'object', - properties: { - noteId: { type: 'string', format: 'misskey:id' }, - }, - required: ['noteId'], -} as const; - -// eslint-disable-next-line import/no-default-export -export default define(meta, paramDef, async (ps, user) => { - // Get favoritee - const note = await getNote(ps.noteId, user).catch(err => { - if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE'); - throw err; - }); - - // if already favorited - const exist = await NoteFavorites.findOneBy({ - noteId: note.id, - userId: user.id, - }); - - if (exist == null) throw new ApiError('NOT_FAVORITED'); - - // Delete favorite - await NoteFavorites.delete(exist.id); -}); diff --git a/packages/backend/src/server/api/endpoints/notes/state.ts b/packages/backend/src/server/api/endpoints/notes/state.ts index 85ae2aa23..bb7c937a8 100644 --- a/packages/backend/src/server/api/endpoints/notes/state.ts +++ b/packages/backend/src/server/api/endpoints/notes/state.ts @@ -1,4 +1,4 @@ -import { NoteFavorites, NoteThreadMutings, NoteWatchings } from '@/models/index.js'; +import { NoteThreadMutings, NoteWatchings } from '@/models/index.js'; import { ApiError } from '@/server/api/error.js'; import { getNote } from '@/server/api/common/getters.js'; import define from '@/server/api/define.js'; @@ -12,10 +12,6 @@ export const meta = { type: 'object', optional: false, nullable: false, properties: { - isFavorited: { - type: 'boolean', - optional: false, nullable: false, - }, isWatching: { type: 'boolean', optional: false, nullable: false, @@ -51,14 +47,7 @@ export default define(meta, paramDef, async (ps, user) => { throw err; }); - const [favorite, watching, threadMuting] = await Promise.all([ - NoteFavorites.count({ - where: { - userId: user.id, - noteId: note.id, - }, - take: 1, - }), + const [watching, threadMuting] = await Promise.all([ NoteWatchings.count({ where: { userId: user.id, @@ -76,7 +65,6 @@ export default define(meta, paramDef, async (ps, user) => { ]); return { - isFavorited: favorite !== 0, isWatching: watching !== 0, isMutedThread: threadMuting !== 0, }; diff --git a/packages/backend/src/server/api/endpoints/users/stats.ts b/packages/backend/src/server/api/endpoints/users/stats.ts index 7197219fb..3780b9eb0 100644 --- a/packages/backend/src/server/api/endpoints/users/stats.ts +++ b/packages/backend/src/server/api/endpoints/users/stats.ts @@ -1,4 +1,4 @@ -import { DriveFiles, Followings, NoteFavorites, NoteReactions, Notes, PageLikes, PollVotes, Users } from '@/models/index.js'; +import { DriveFiles, Followings, NoteReactions, Notes, PageLikes, PollVotes, Users } from '@/models/index.js'; import { awaitAll } from '@/prelude/await-all.js'; import define from '@/server/api/define.js'; import { ApiError } from '@/server/api/error.js'; @@ -76,10 +76,6 @@ export const meta = { type: 'integer', optional: false, nullable: false, }, - noteFavoritesCount: { - type: 'integer', - optional: false, nullable: false, - }, pageLikesCount: { type: 'integer', optional: false, nullable: false, @@ -148,9 +144,6 @@ export default define(meta, paramDef, async (ps, me) => { .innerJoin('reaction.note', 'note') .where('note.userId = :userId', { userId: user.id }) .getCount(), - noteFavoritesCount: NoteFavorites.createQueryBuilder('favorite') - .where('favorite.userId = :userId', { userId: user.id }) - .getCount(), pageLikesCount: PageLikes.createQueryBuilder('like') .where('like.userId = :userId', { userId: user.id }) .getCount(), diff --git a/packages/backend/src/server/api/error.ts b/packages/backend/src/server/api/error.ts index 201c234b7..d5ec24f71 100644 --- a/packages/backend/src/server/api/error.ts +++ b/packages/backend/src/server/api/error.ts @@ -68,10 +68,6 @@ export const errors: Record message: 'That note is already added to that clip.', httpStatusCode: 409, }, - ALREADY_FAVORITED: { - message: 'That note is already favorited.', - httpStatusCode: 409, - }, ALREADY_FOLLOWING: { message: 'You are already following that user.', httpStatusCode: 409, @@ -332,10 +328,6 @@ export const errors: Record message: 'That note is not added to that clip.', httpStatusCode: 409, }, - NOT_FAVORITED: { - message: 'You have not favorited that note.', - httpStatusCode: 409, - }, NOT_FOLLOWING: { message: 'You are not following that user.', httpStatusCode: 409, From 9859537b024898bbddee11811dde22b55b2305be Mon Sep 17 00:00:00 2001 From: Johann150 Date: Fri, 26 May 2023 21:13:42 +0200 Subject: [PATCH 22/23] BREAKING migrate note favorites to clips The following endpoints are removed: - `api/i/favorites` - `api/notes/favorites/create` - `api/notes/favorites/delete` The following endpoints are changed: - `api/notes/state` - `api/users/stats` closes https://akkoma.dev/FoundKeyGang/FoundKey/issues/374 Changelog: Removed --- .../1685126322423-remove-favourites.js | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 packages/backend/migration/1685126322423-remove-favourites.js diff --git a/packages/backend/migration/1685126322423-remove-favourites.js b/packages/backend/migration/1685126322423-remove-favourites.js new file mode 100644 index 000000000..fab024744 --- /dev/null +++ b/packages/backend/migration/1685126322423-remove-favourites.js @@ -0,0 +1,33 @@ +export class removeFavourites1685126322423 { + name = 'removeFavourites1685126322423'; + + async up(queryRunner) { + await queryRunner.query(` + WITH "new_clips" AS ( + INSERT INTO "clip" ("id", "createdAt", "userId", "name") + SELECT + RIGHT(GEN_RANDOM_UUID()::text, 10), + NOW(), + "userId", + '⭐' + FROM "note_favorite" + GROUP BY "userId" + RETURNING "id", "userId" + ) + INSERT INTO "clip_note" ("id", "noteId", "clipId") + SELECT + "note_favorite"."id", + "noteId", + "new_clips"."id" + FROM "note_favorite" + JOIN "new_clips" ON "note_favorite"."userId" = "new_clips"."userId" + `); + await queryRunner.query(`DROP TABLE "note_favorite"`); + } + + async down(queryRunner) { + // can't revert the migration to clips, can only recreate the database table + await queryRunner.query(`CREATE TABLE "note_favorite" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, CONSTRAINT "PK_af0da35a60b9fa4463a62082b36" PRIMARY KEY ("id"))`); + } +} + From f6381e3227d64485445ca8498b676a78d7d61970 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Sat, 27 May 2023 00:41:57 +0200 Subject: [PATCH 23/23] remove unused locale strings --- locales/en-US.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/locales/en-US.yml b/locales/en-US.yml index fe1dc137e..abf6e010c 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -32,9 +32,6 @@ signup: "Sign Up" save: "Save" users: "Users" addUser: "Add a user" -favorite: "Add to favorites" -favorites: "Favorites" -unfavorite: "Remove from favorites" pin: "Pin to profile" unpin: "Unpin from profile" copyContent: "Copy contents"