summaryrefslogtreecommitdiff
path: root/packages/backend/src/server
diff options
context:
space:
mode:
authordakkar <dakkar@thenautilus.net>2024-06-26 19:57:45 +0000
committerdakkar <dakkar@thenautilus.net>2024-06-26 19:57:45 +0000
commitdc4f6c8016ccece79f8f2b75a5378c643e884f0b (patch)
tree3e2c9e998a2f97244a37f3f8cb7f1831e4108ab7 /packages/backend/src/server
parentmerge: Release 2024.3.3 (!501) (diff)
parentmerge: parse `notRespondingSince` from redis instance cache (!560) (diff)
downloadsharkey-dc4f6c8016ccece79f8f2b75a5378c643e884f0b.tar.gz
sharkey-dc4f6c8016ccece79f8f2b75a5378c643e884f0b.tar.bz2
sharkey-dc4f6c8016ccece79f8f2b75a5378c643e884f0b.zip
merge: release 2024.5.0 (!556)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/556 Approved-by: Tess K <me@thvxl.se>
Diffstat (limited to 'packages/backend/src/server')
-rw-r--r--packages/backend/src/server/ActivityPubServerService.ts4
-rw-r--r--packages/backend/src/server/FileServerService.ts7
-rw-r--r--packages/backend/src/server/HealthServerService.ts54
-rw-r--r--packages/backend/src/server/NodeinfoServerService.ts13
-rw-r--r--packages/backend/src/server/ServerModule.ts2
-rw-r--r--packages/backend/src/server/ServerService.ts22
-rw-r--r--packages/backend/src/server/api/ApiCallService.ts77
-rw-r--r--packages/backend/src/server/api/ApiServerService.ts2
-rw-r--r--packages/backend/src/server/api/EndpointsModule.ts4
-rw-r--r--packages/backend/src/server/api/endpoints.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/copy.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/list.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts11
-rw-r--r--packages/backend/src/server/api/endpoints/admin/meta.ts39
-rw-r--r--packages/backend/src/server/api/endpoints/admin/nsfw-user.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/admin/roles/users.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/admin/show-users.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/silence-user.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/admin/unsilence-user.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/admin/update-meta.ts46
-rw-r--r--packages/backend/src/server/api/endpoints/announcements.ts11
-rw-r--r--packages/backend/src/server/api/endpoints/announcements/show.ts54
-rw-r--r--packages/backend/src/server/api/endpoints/antennas/create.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/antennas/update.ts16
-rw-r--r--packages/backend/src/server/api/endpoints/channels/timeline.ts32
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files/find.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/federation/instances.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/fetch-rss.ts179
-rw-r--r--packages/backend/src/server/api/endpoints/flash/create.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/following/requests/list.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/key-done.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/i/import-blocking.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/import-following.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/import-muting.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/import-notes.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/i/import-user-lists.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/notifications.ts60
-rw-r--r--packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/i/update-email.ts10
-rw-r--r--packages/backend/src/server/api/endpoints/i/update.ts36
-rw-r--r--packages/backend/src/server/api/endpoints/notes/create.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/notes/edit.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/notes/reactions.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/notes/translate.ts15
-rw-r--r--packages/backend/src/server/api/endpoints/roles/users.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/sponsors.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/users/following.ts7
-rw-r--r--packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts10
-rw-r--r--packages/backend/src/server/api/endpoints/users/relation.ts8
-rw-r--r--packages/backend/src/server/api/endpoints/users/show.ts10
-rw-r--r--packages/backend/src/server/api/mastodon/MastodonApiServerService.ts27
-rw-r--r--packages/backend/src/server/api/mastodon/converters.ts15
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints.ts5
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/account.ts9
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/auth.ts7
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/filter.ts5
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/meta.ts5
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/notifications.ts5
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/search.ts5
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/status.ts5
-rw-r--r--packages/backend/src/server/api/mastodon/endpoints/timeline.ts5
-rw-r--r--packages/backend/src/server/api/openapi/gen-spec.ts2
-rw-r--r--packages/backend/src/server/api/stream/channel.ts22
-rw-r--r--packages/backend/src/server/api/stream/channels/antenna.ts8
-rw-r--r--packages/backend/src/server/api/stream/channels/bubble-timeline.ts30
-rw-r--r--packages/backend/src/server/api/stream/channels/channel.ts17
-rw-r--r--packages/backend/src/server/api/stream/channels/global-timeline.ts28
-rw-r--r--packages/backend/src/server/api/stream/channels/hashtag.ts11
-rw-r--r--packages/backend/src/server/api/stream/channels/home-timeline.ts21
-rw-r--r--packages/backend/src/server/api/stream/channels/hybrid-timeline.ts19
-rw-r--r--packages/backend/src/server/api/stream/channels/local-timeline.ts17
-rw-r--r--packages/backend/src/server/api/stream/channels/role-timeline.ts9
-rw-r--r--packages/backend/src/server/api/stream/channels/user-list.ts17
-rw-r--r--packages/backend/src/server/web/ClientServerService.ts36
-rw-r--r--packages/backend/src/server/web/UrlPreviewService.ts67
-rw-r--r--packages/backend/src/server/web/boot.js4
-rw-r--r--packages/backend/src/server/web/views/base.pug7
-rw-r--r--packages/backend/src/server/web/views/note.pug4
-rw-r--r--packages/backend/src/server/web/views/page.pug2
-rw-r--r--packages/backend/src/server/web/views/user.pug2
82 files changed, 916 insertions, 341 deletions
diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts
index 64bce07a98..398ad54f6a 100644
--- a/packages/backend/src/server/ActivityPubServerService.ts
+++ b/packages/backend/src/server/ActivityPubServerService.ts
@@ -33,7 +33,7 @@ import { UtilityService } from '@/core/UtilityService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { IActivity } from '@/core/activitypub/type.js';
-import { isPureRenote } from '@/misc/is-pure-renote.js';
+import { isQuote, isRenote } from '@/misc/is-renote.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify';
import type { FindOptionsWhere } from 'typeorm';
import type Logger from '@/logger.js';
@@ -107,7 +107,7 @@ export class ActivityPubServerService {
*/
@bindThis
private async packActivity(note: MiNote): Promise<any> {
- if (isPureRenote(note)) {
+ if (isRenote(note) && !isQuote(note)) {
const renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId });
return this.apRendererService.renderAnnounce(renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`, note);
}
diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts
index 3cb353f2b3..e0b187f3cf 100644
--- a/packages/backend/src/server/FileServerService.ts
+++ b/packages/backend/src/server/FileServerService.ts
@@ -212,6 +212,8 @@ export class FileServerService {
}
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(image.type) ? image.type : 'application/octet-stream');
+ reply.header('Content-Length', file.file.size);
+ reply.header('Cache-Control', 'max-age=31536000, immutable');
reply.header('Content-Disposition',
contentDisposition(
'inline',
@@ -254,6 +256,7 @@ export class FileServerService {
return fs.createReadStream(file.path);
} else {
reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.file.type) ? file.file.type : 'application/octet-stream');
+ reply.header('Content-Length', file.file.size);
reply.header('Cache-Control', 'max-age=31536000, immutable');
reply.header('Content-Disposition', contentDisposition('inline', file.filename));
@@ -528,9 +531,7 @@ export class FileServerService {
if (!file.storedInternal) {
if (!(file.isLink && file.uri)) return '204';
const result = await this.downloadAndDetectTypeFromUrl(file.uri);
- if (!file.size) {
- file.size = (await fs.promises.stat(result.path)).size;
- }
+ file.size = (await fs.promises.stat(result.path)).size; // DB file.sizeは正確とは限らないので
return {
...result,
url: file.uri,
diff --git a/packages/backend/src/server/HealthServerService.ts b/packages/backend/src/server/HealthServerService.ts
new file mode 100644
index 0000000000..2c3ed85925
--- /dev/null
+++ b/packages/backend/src/server/HealthServerService.ts
@@ -0,0 +1,54 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import * as Redis from 'ioredis';
+import { DataSource } from 'typeorm';
+import { bindThis } from '@/decorators.js';
+import { DI } from '@/di-symbols.js';
+import { readyRef } from '@/boot/ready.js';
+import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
+import type { MeiliSearch } from 'meilisearch';
+
+@Injectable()
+export class HealthServerService {
+ constructor(
+ @Inject(DI.redis)
+ private redis: Redis.Redis,
+
+ @Inject(DI.redisForPub)
+ private redisForPub: Redis.Redis,
+
+ @Inject(DI.redisForSub)
+ private redisForSub: Redis.Redis,
+
+ @Inject(DI.redisForTimelines)
+ private redisForTimelines: Redis.Redis,
+
+ @Inject(DI.db)
+ private db: DataSource,
+
+ @Inject(DI.meilisearch)
+ private meilisearch: MeiliSearch | null,
+ ) {}
+
+ @bindThis
+ public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
+ fastify.get('/', async (request, reply) => {
+ reply.code(await Promise.all([
+ new Promise<void>((resolve, reject) => readyRef.value ? resolve() : reject()),
+ this.redis.ping(),
+ this.redisForPub.ping(),
+ this.redisForSub.ping(),
+ this.redisForTimelines.ping(),
+ this.db.query('SELECT 1'),
+ ...(this.meilisearch ? [this.meilisearch.health()] : []),
+ ]).then(() => 200, () => 503));
+ reply.header('Cache-Control', 'no-store');
+ });
+
+ done();
+ }
+}
diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts
index d4f4c8b752..716bb0944b 100644
--- a/packages/backend/src/server/NodeinfoServerService.ts
+++ b/packages/backend/src/server/NodeinfoServerService.ts
@@ -36,12 +36,12 @@ export class NodeinfoServerService {
@bindThis
public getLinks() {
return [{
- rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1',
- href: this.config.url + nodeinfo2_1path
- }, {
- rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0',
- href: this.config.url + nodeinfo2_0path,
- }];
+ rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1',
+ href: this.config.url + nodeinfo2_1path,
+ }, {
+ rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0',
+ href: this.config.url + nodeinfo2_0path,
+ }];
}
@bindThis
@@ -107,6 +107,7 @@ export class NodeinfoServerService {
langs: meta.langs,
tosUrl: meta.termsOfServiceUrl,
privacyPolicyUrl: meta.privacyPolicyUrl,
+ inquiryUrl: meta.inquiryUrl,
impressumUrl: meta.impressumUrl,
donationUrl: meta.donationUrl,
repositoryUrl: meta.repositoryUrl,
diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts
index e0c4768ffc..39c8f67b8e 100644
--- a/packages/backend/src/server/ServerModule.ts
+++ b/packages/backend/src/server/ServerModule.ts
@@ -8,6 +8,7 @@ import { EndpointsModule } from '@/server/api/EndpointsModule.js';
import { CoreModule } from '@/core/CoreModule.js';
import { ApiCallService } from './api/ApiCallService.js';
import { FileServerService } from './FileServerService.js';
+import { HealthServerService } from './HealthServerService.js';
import { NodeinfoServerService } from './NodeinfoServerService.js';
import { ServerService } from './ServerService.js';
import { WellKnownServerService } from './WellKnownServerService.js';
@@ -58,6 +59,7 @@ import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js
ClientServerService,
ClientLoggerService,
FeedService,
+ HealthServerService,
UrlPreviewService,
ActivityPubServerService,
FileServerService,
diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts
index 5a456e09ad..9eddf434f7 100644
--- a/packages/backend/src/server/ServerService.ts
+++ b/packages/backend/src/server/ServerService.ts
@@ -18,7 +18,6 @@ import { DI } from '@/di-symbols.js';
import type Logger from '@/logger.js';
import * as Acct from '@/misc/acct.js';
import { genIdenticon } from '@/misc/gen-identicon.js';
-import { createTemp } from '@/misc/create-temp.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
@@ -29,6 +28,7 @@ import { ApiServerService } from './api/ApiServerService.js';
import { StreamingApiServerService } from './api/StreamingApiServerService.js';
import { WellKnownServerService } from './WellKnownServerService.js';
import { FileServerService } from './FileServerService.js';
+import { HealthServerService } from './HealthServerService.js';
import { ClientServerService } from './web/ClientServerService.js';
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
import { MastodonApiServerService } from './api/mastodon/MastodonApiServerService.js';
@@ -64,6 +64,7 @@ export class ServerService implements OnApplicationShutdown {
private wellKnownServerService: WellKnownServerService,
private nodeinfoServerService: NodeinfoServerService,
private fileServerService: FileServerService,
+ private healthServerService: HealthServerService,
private clientServerService: ClientServerService,
private globalEventService: GlobalEventService,
private loggerService: LoggerService,
@@ -111,6 +112,7 @@ export class ServerService implements OnApplicationShutdown {
fastify.register(this.nodeinfoServerService.createServer);
fastify.register(this.wellKnownServerService.createServer);
fastify.register(this.oauth2ProviderService.createServer, { prefix: '/oauth' });
+ fastify.register(this.healthServerService.createServer, { prefix: '/healthz' });
fastify.get<{ Params: { path: string }; Querystring: { static?: any; badge?: any; }; }>('/emoji/:path(.*)', async (request, reply) => {
const path = request.params.path;
@@ -122,12 +124,20 @@ export class ServerService implements OnApplicationShutdown {
return;
}
- const name = path.split('@')[0].replace(/\.webp$/i, '');
- const host = path.split('@')[1]?.replace(/\.webp$/i, '');
+ const emojiPath = path.replace(/\.webp$/i, '');
+ const pathChunks = emojiPath.split('@');
+
+ if (pathChunks.length > 2) {
+ reply.code(400);
+ return;
+ }
+
+ const name = pathChunks.shift();
+ const host = pathChunks.pop();
const emoji = await this.emojisRepository.findOneBy({
// `@.` is the spec of ReactionService.decodeReaction
- host: (host == null || host === '.') ? IsNull() : host,
+ host: (host === undefined || host === '.') ? IsNull() : host,
name: name,
});
@@ -186,9 +196,7 @@ export class ServerService implements OnApplicationShutdown {
reply.header('Cache-Control', 'public, max-age=86400');
if ((await this.metaService.fetch()).enableIdenticonGeneration) {
- const [temp, cleanup] = await createTemp();
- await genIdenticon(request.params.x, fs.createWriteStream(temp));
- return fs.createReadStream(temp).on('close', () => cleanup());
+ return await genIdenticon(request.params.x);
} else {
return reply.redirect('/static-assets/avatar.png');
}
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index 9836689872..271ef80554 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -7,6 +7,7 @@ import { randomUUID } from 'node:crypto';
import * as fs from 'node:fs';
import * as stream from 'node:stream/promises';
import { Inject, Injectable } from '@nestjs/common';
+import * as Sentry from '@sentry/node';
import { DI } from '@/di-symbols.js';
import { getIpHash } from '@/misc/get-ip-hash.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
@@ -17,6 +18,7 @@ import { MetaService } from '@/core/MetaService.js';
import { createTemp } from '@/misc/create-temp.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
+import type { Config } from '@/config.js';
import { ApiError } from './error.js';
import { RateLimiterService } from './RateLimiterService.js';
import { ApiLoggerService } from './ApiLoggerService.js';
@@ -38,6 +40,9 @@ export class ApiCallService implements OnApplicationShutdown {
private userIpHistoriesClearIntervalId: NodeJS.Timeout;
constructor(
+ @Inject(DI.config)
+ private config: Config,
+
@Inject(DI.userIpsRepository)
private userIpsRepository: UserIpsRepository,
@@ -88,6 +93,48 @@ export class ApiCallService implements OnApplicationShutdown {
}
}
+ #onExecError(ep: IEndpoint, data: any, err: Error): void {
+ if (err instanceof ApiError || err instanceof AuthenticationError) {
+ throw err;
+ } else {
+ const errId = randomUUID();
+ this.logger.error(`Internal error occurred in ${ep.name}: ${err.message}`, {
+ ep: ep.name,
+ ps: data,
+ e: {
+ message: err.message,
+ code: err.name,
+ stack: err.stack,
+ id: errId,
+ },
+ });
+ console.error(err, errId);
+
+ if (this.config.sentryForBackend) {
+ Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, {
+ extra: {
+ ep: ep.name,
+ ps: data,
+ e: {
+ message: err.message,
+ code: err.name,
+ stack: err.stack,
+ id: errId,
+ },
+ },
+ });
+ }
+
+ throw new ApiError(null, {
+ e: {
+ message: err.message,
+ code: err.name,
+ id: errId,
+ },
+ });
+ }
+ }
+
@bindThis
public handleRequest(
endpoint: IEndpoint & { exec: any },
@@ -362,31 +409,11 @@ export class ApiCallService implements OnApplicationShutdown {
}
// API invoking
- return await ep.exec(data, user, token, file, request.ip, request.headers).catch((err: Error) => {
- if (err instanceof ApiError || err instanceof AuthenticationError) {
- throw err;
- } else {
- const errId = randomUUID();
- this.logger.error(`Internal error occurred in ${ep.name}: ${err.message}`, {
- ep: ep.name,
- ps: data,
- e: {
- message: err.message,
- code: err.name,
- stack: err.stack,
- id: errId,
- },
- });
- console.error(err, errId);
- throw new ApiError(null, {
- e: {
- message: err.message,
- code: err.name,
- id: errId,
- },
- });
- }
- });
+ if (this.config.sentryForBackend) {
+ return await Sentry.startSpan({ name: 'API: ' + ep.name }, () => ep.exec(data, user, token, file, request.ip, request.headers).catch((err: Error) => this.#onExecError(ep, data, err)));
+ } else {
+ return await ep.exec(data, user, token, file, request.ip, request.headers).catch((err: Error) => this.#onExecError(ep, data, err));
+ }
}
@bindThis
diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts
index e99244cdd0..4a5935f930 100644
--- a/packages/backend/src/server/api/ApiServerService.ts
+++ b/packages/backend/src/server/api/ApiServerService.ts
@@ -137,7 +137,7 @@ export class ApiServerService {
const instances = await this.instancesRepository.find({
select: ['host'],
where: {
- isSuspended: false,
+ suspensionState: 'none',
},
});
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index f2945f477c..f44635fba0 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -88,6 +88,7 @@ import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js'
import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js';
import * as ep___admin_roles_users from './endpoints/admin/roles/users.js';
import * as ep___announcements from './endpoints/announcements.js';
+import * as ep___announcements_show from './endpoints/announcements/show.js';
import * as ep___antennas_create from './endpoints/antennas/create.js';
import * as ep___antennas_delete from './endpoints/antennas/delete.js';
import * as ep___antennas_list from './endpoints/antennas/list.js';
@@ -473,6 +474,7 @@ const $admin_roles_unassign: Provider = { provide: 'ep:admin/roles/unassign', us
const $admin_roles_updateDefaultPolicies: Provider = { provide: 'ep:admin/roles/update-default-policies', useClass: ep___admin_roles_updateDefaultPolicies.default };
const $admin_roles_users: Provider = { provide: 'ep:admin/roles/users', useClass: ep___admin_roles_users.default };
const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default };
+const $announcements_show: Provider = { provide: 'ep:announcements/show', useClass: ep___announcements_show.default };
const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default };
const $antennas_delete: Provider = { provide: 'ep:antennas/delete', useClass: ep___antennas_delete.default };
const $antennas_list: Provider = { provide: 'ep:antennas/list', useClass: ep___antennas_list.default };
@@ -862,6 +864,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_roles_updateDefaultPolicies,
$admin_roles_users,
$announcements,
+ $announcements_show,
$antennas_create,
$antennas_delete,
$antennas_list,
@@ -1245,6 +1248,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_roles_updateDefaultPolicies,
$admin_roles_users,
$announcements,
+ $announcements_show,
$antennas_create,
$antennas_delete,
$antennas_list,
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index f83d2cacff..89f60933ef 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -88,6 +88,7 @@ import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js'
import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js';
import * as ep___admin_roles_users from './endpoints/admin/roles/users.js';
import * as ep___announcements from './endpoints/announcements.js';
+import * as ep___announcements_show from './endpoints/announcements/show.js';
import * as ep___antennas_create from './endpoints/antennas/create.js';
import * as ep___antennas_delete from './endpoints/antennas/delete.js';
import * as ep___antennas_list from './endpoints/antennas/list.js';
@@ -471,6 +472,7 @@ const eps = [
['admin/roles/update-default-policies', ep___admin_roles_updateDefaultPolicies],
['admin/roles/users', ep___admin_roles_users],
['announcements', ep___announcements],
+ ['announcements/show', ep___announcements_show],
['antennas/create', ep___antennas_create],
['antennas/delete', ep___antennas_delete],
['antennas/list', ep___antennas_list],
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
index f968813197..acd2494131 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts
@@ -90,7 +90,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const addedEmoji = await this.customEmojiService.add({
driveFile,
name: nameNfc,
- category: emoji.category?.normalize('NFC'),
+ category: emoji.category?.normalize('NFC') ?? null,
aliases: emoji.aliases?.map(a => a.normalize('NFC')),
host: null,
license: emoji.license,
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
index 5e21111f9f..f35a6667f4 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts
@@ -66,6 +66,7 @@ export const paramDef = {
properties: {
query: { type: 'string', nullable: true, default: null },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ offset: { type: 'integer', minimum: 1, nullable: true, default: null },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
},
@@ -91,7 +92,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
//q.andWhere('emoji.name ILIKE :q', { q: `%${ sqlLikeEscape(ps.query) }%` });
//const emojis = await q.limit(ps.limit).getMany();
- emojis = await q.orderBy('length(emoji.name)', 'ASC').getMany();
+ emojis = await q.orderBy('length(emoji.name)', 'ASC').addOrderBy('id', 'DESC').getMany();
const queryarry = ps.query.match(/:([\p{Letter}\p{Number}\p{Mark}_+-]*):/ug);
if (queryarry) {
@@ -105,9 +106,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
emoji.aliases.some(a => a.includes(queryNfc)) ||
emoji.category?.includes(queryNfc));
}
- emojis.splice(ps.limit + 1);
+ emojis = emojis.slice((ps.offset ?? 0), ((ps.offset ?? 0) + ps.limit));
} else {
- emojis = await q.limit(ps.limit).getMany();
+ emojis = await q.take(ps.limit).skip(ps.offset ?? 0).getMany();
}
return this.emojiEntityService.packDetailedMany(emojis);
diff --git a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts
index 4ababae9f2..8b142910a6 100644
--- a/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts
+++ b/packages/backend/src/server/api/endpoints/admin/federation/update-instance.ts
@@ -47,13 +47,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new Error('instance not found');
}
+ const isSuspendedBefore = instance.suspensionState !== 'none';
+ let suspensionState: undefined | 'manuallySuspended' | 'none';
+
+ if (ps.isSuspended != null && isSuspendedBefore !== ps.isSuspended) {
+ suspensionState = ps.isSuspended ? 'manuallySuspended' : 'none';
+ }
+
await this.federatedInstanceService.update(instance.id, {
- isSuspended: ps.isSuspended,
+ suspensionState,
isNSFW: ps.isNSFW,
moderationNote: ps.moderationNote,
});
- if (ps.isSuspended != null && instance.isSuspended !== ps.isSuspended) {
+ if (ps.isSuspended != null && isSuspendedBefore !== ps.isSuspended) {
if (ps.isSuspended) {
this.moderationLogService.log(me, 'suspendRemoteInstance', {
id: instance.id,
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index 34454c276e..ca4d63b834 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -458,6 +458,10 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
+ inquiryUrl: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
repositoryUrl: {
type: 'string',
optional: false, nullable: true,
@@ -465,6 +469,8 @@ export const meta = {
summalyProxy: {
type: 'string',
optional: false, nullable: true,
+ deprecated: true,
+ description: '[Deprecated] Use "urlPreviewSummaryProxyUrl" instead.',
},
themeColor: {
type: 'string',
@@ -482,6 +488,30 @@ export const meta = {
type: 'string',
optional: false, nullable: false,
},
+ urlPreviewEnabled: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ urlPreviewTimeout: {
+ type: 'number',
+ optional: false, nullable: false,
+ },
+ urlPreviewMaximumContentLength: {
+ type: 'number',
+ optional: false, nullable: false,
+ },
+ urlPreviewRequireContentLength: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ urlPreviewUserAgent: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
+ urlPreviewSummaryProxyUrl: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
},
},
} as const;
@@ -519,6 +549,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
impressumUrl: instance.impressumUrl,
donationUrl: instance.donationUrl,
privacyPolicyUrl: instance.privacyPolicyUrl,
+ inquiryUrl: instance.inquiryUrl,
disableRegistration: instance.disableRegistration,
emailRequiredForSignup: instance.emailRequiredForSignup,
approvalRequiredForSignup: instance.approvalRequiredForSignup,
@@ -569,7 +600,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos,
enableBotTrending: instance.enableBotTrending,
proxyAccountId: instance.proxyAccountId,
- summalyProxy: instance.summalyProxy,
email: instance.email,
smtpSecure: instance.smtpSecure,
smtpHost: instance.smtpHost,
@@ -616,6 +646,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax,
perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax,
notesPerOneAd: instance.notesPerOneAd,
+ summalyProxy: instance.urlPreviewSummaryProxyUrl,
+ urlPreviewEnabled: instance.urlPreviewEnabled,
+ urlPreviewTimeout: instance.urlPreviewTimeout,
+ urlPreviewMaximumContentLength: instance.urlPreviewMaximumContentLength,
+ urlPreviewRequireContentLength: instance.urlPreviewRequireContentLength,
+ urlPreviewUserAgent: instance.urlPreviewUserAgent,
+ urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl,
};
});
}
diff --git a/packages/backend/src/server/api/endpoints/admin/nsfw-user.ts b/packages/backend/src/server/api/endpoints/admin/nsfw-user.ts
index 2a47abe03c..d3fa4251dd 100644
--- a/packages/backend/src/server/api/endpoints/admin/nsfw-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/nsfw-user.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: marie and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/users.ts b/packages/backend/src/server/api/endpoints/admin/roles/users.ts
index 45758d4f50..198166bec2 100644
--- a/packages/backend/src/server/api/endpoints/admin/roles/users.ts
+++ b/packages/backend/src/server/api/endpoints/admin/roles/users.ts
@@ -89,10 +89,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.limit(ps.limit)
.getMany();
+ const _users = assigns.map(({ user, userId }) => user ?? userId);
+ const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' })
+ .then(users => new Map(users.map(u => [u.id, u])));
return await Promise.all(assigns.map(async assign => ({
id: assign.id,
createdAt: this.idService.parse(assign.id).date.toISOString(),
- user: await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
+ user: _userMap.get(assign.userId) ?? await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
expiresAt: assign.expiresAt?.toISOString() ?? null,
})));
});
diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts
index 685da928e3..5f16519403 100644
--- a/packages/backend/src/server/api/endpoints/admin/show-users.ts
+++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts
@@ -16,7 +16,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
- kind: 'read:admin:show-users',
+ kind: 'read:admin:show-user',
res: {
type: 'array',
diff --git a/packages/backend/src/server/api/endpoints/admin/silence-user.ts b/packages/backend/src/server/api/endpoints/admin/silence-user.ts
index 007bed5c03..7e6045049a 100644
--- a/packages/backend/src/server/api/endpoints/admin/silence-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/silence-user.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: marie and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository } from '@/models/_.js';
diff --git a/packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts b/packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts
index 013e7771ba..26588365e1 100644
--- a/packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/unnsfw-user.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: marie and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
diff --git a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts b/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts
index 5e514ccda6..f92be0d8e0 100644
--- a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: marie and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository } from '@/models/_.js';
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index 7fea7d969e..015a1e1f7c 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -93,7 +93,6 @@ export const paramDef = {
type: 'string',
},
},
- summalyProxy: { type: 'string', nullable: true },
deeplAuthKey: { type: 'string', nullable: true },
deeplIsPro: { type: 'boolean' },
deeplFreeMode: { type: 'boolean' },
@@ -114,6 +113,7 @@ export const paramDef = {
impressumUrl: { type: 'string', nullable: true },
donationUrl: { type: 'string', nullable: true },
privacyPolicyUrl: { type: 'string', nullable: true },
+ inquiryUrl: { type: 'string', nullable: true },
useObjectStorage: { type: 'boolean' },
objectStorageBaseUrl: { type: 'string', nullable: true },
objectStorageBucket: { type: 'string', nullable: true },
@@ -158,6 +158,16 @@ export const paramDef = {
type: 'string',
},
},
+ summalyProxy: {
+ type: 'string', nullable: true,
+ description: '[Deprecated] Use "urlPreviewSummaryProxyUrl" instead.',
+ },
+ urlPreviewEnabled: { type: 'boolean' },
+ urlPreviewTimeout: { type: 'integer' },
+ urlPreviewMaximumContentLength: { type: 'integer' },
+ urlPreviewRequireContentLength: { type: 'boolean' },
+ urlPreviewUserAgent: { type: 'string', nullable: true },
+ urlPreviewSummaryProxyUrl: { type: 'string', nullable: true },
},
required: [],
} as const;
@@ -357,10 +367,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.langs = ps.langs.filter(Boolean);
}
- if (ps.summalyProxy !== undefined) {
- set.summalyProxy = ps.summalyProxy;
- }
-
if (ps.enableEmail !== undefined) {
set.enableEmail = ps.enableEmail;
}
@@ -425,6 +431,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.privacyPolicyUrl = ps.privacyPolicyUrl;
}
+ if (ps.inquiryUrl !== undefined) {
+ set.inquiryUrl = ps.inquiryUrl;
+ }
+
if (ps.useObjectStorage !== undefined) {
set.useObjectStorage = ps.useObjectStorage;
}
@@ -609,6 +619,32 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.bannedEmailDomains = ps.bannedEmailDomains;
}
+ if (ps.urlPreviewEnabled !== undefined) {
+ set.urlPreviewEnabled = ps.urlPreviewEnabled;
+ }
+
+ if (ps.urlPreviewTimeout !== undefined) {
+ set.urlPreviewTimeout = ps.urlPreviewTimeout;
+ }
+
+ if (ps.urlPreviewMaximumContentLength !== undefined) {
+ set.urlPreviewMaximumContentLength = ps.urlPreviewMaximumContentLength;
+ }
+
+ if (ps.urlPreviewRequireContentLength !== undefined) {
+ set.urlPreviewRequireContentLength = ps.urlPreviewRequireContentLength;
+ }
+
+ if (ps.urlPreviewUserAgent !== undefined) {
+ const value = (ps.urlPreviewUserAgent ?? '').trim();
+ set.urlPreviewUserAgent = value === '' ? null : ps.urlPreviewUserAgent;
+ }
+
+ if (ps.summalyProxy !== undefined || ps.urlPreviewSummaryProxyUrl !== undefined) {
+ const value = ((ps.urlPreviewSummaryProxyUrl ?? ps.summalyProxy) ?? '').trim();
+ set.urlPreviewSummaryProxyUrl = value === '' ? null : value;
+ }
+
const before = await this.metaService.fetch(true);
await this.metaService.update(set);
diff --git a/packages/backend/src/server/api/endpoints/announcements.ts b/packages/backend/src/server/api/endpoints/announcements.ts
index 3b12f5b62c..ff8dd73605 100644
--- a/packages/backend/src/server/api/endpoints/announcements.ts
+++ b/packages/backend/src/server/api/endpoints/announcements.ts
@@ -7,9 +7,9 @@ import { Inject, Injectable } from '@nestjs/common';
import { Brackets } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
-import { AnnouncementService } from '@/core/AnnouncementService.js';
+import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js';
import { DI } from '@/di-symbols.js';
-import type { AnnouncementReadsRepository, AnnouncementsRepository } from '@/models/_.js';
+import type { AnnouncementsRepository } from '@/models/_.js';
export const meta = {
tags: ['meta'],
@@ -44,11 +44,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.announcementsRepository)
private announcementsRepository: AnnouncementsRepository,
- @Inject(DI.announcementReadsRepository)
- private announcementReadsRepository: AnnouncementReadsRepository,
-
private queryService: QueryService,
- private announcementService: AnnouncementService,
+ private announcementEntityService: AnnouncementEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId)
@@ -60,7 +57,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const announcements = await query.limit(ps.limit).getMany();
- return this.announcementService.packMany(announcements, me);
+ return this.announcementEntityService.packMany(announcements, me);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/announcements/show.ts b/packages/backend/src/server/api/endpoints/announcements/show.ts
new file mode 100644
index 0000000000..6312a0a54c
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/announcements/show.ts
@@ -0,0 +1,54 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { EntityNotFoundError } from 'typeorm';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { AnnouncementService } from '@/core/AnnouncementService.js';
+import { ApiError } from '../../error.js';
+
+export const meta = {
+ tags: ['meta'],
+
+ requireCredential: false,
+
+ res: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'Announcement',
+ },
+
+ errors: {
+ noSuchAnnouncement: {
+ message: 'No such announcement.',
+ code: 'NO_SUCH_ANNOUNCEMENT',
+ id: 'b57b5e1d-4f49-404a-9edb-46b00268f121',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ announcementId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['announcementId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ private announcementService: AnnouncementService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ try {
+ return await this.announcementService.getAnnouncement(ps.announcementId, me);
+ } catch (err) {
+ if (err instanceof EntityNotFoundError) throw new ApiError(meta.errors.noSuchAnnouncement);
+ throw err;
+ }
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts
index 191de8f833..6b7bacb054 100644
--- a/packages/backend/src/server/api/endpoints/antennas/create.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/create.ts
@@ -64,11 +64,11 @@ export const paramDef = {
} },
caseSensitive: { type: 'boolean' },
localOnly: { type: 'boolean' },
+ excludeBots: { type: 'boolean' },
withReplies: { type: 'boolean' },
withFile: { type: 'boolean' },
- notify: { type: 'boolean' },
},
- required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'],
+ required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile'],
} as const;
@Injectable()
@@ -124,9 +124,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
users: ps.users,
caseSensitive: ps.caseSensitive,
localOnly: ps.localOnly,
+ excludeBots: ps.excludeBots,
withReplies: ps.withReplies,
withFile: ps.withFile,
- notify: ps.notify,
}).then(x => this.antennasRepository.findOneByOrFail(x.identifiers[0]));
this.globalEventService.publishInternalEvent('antennaCreated', antenna);
diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts
index 459729f61f..0c30bca9e0 100644
--- a/packages/backend/src/server/api/endpoints/antennas/update.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/update.ts
@@ -63,11 +63,11 @@ export const paramDef = {
} },
caseSensitive: { type: 'boolean' },
localOnly: { type: 'boolean' },
+ excludeBots: { type: 'boolean' },
withReplies: { type: 'boolean' },
withFile: { type: 'boolean' },
- notify: { type: 'boolean' },
},
- required: ['antennaId', 'name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'],
+ required: ['antennaId'],
} as const;
@Injectable()
@@ -83,8 +83,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
- if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
- throw new Error('either keywords or excludeKeywords is required.');
+ if (ps.keywords && ps.excludeKeywords) {
+ if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
+ throw new Error('either keywords or excludeKeywords is required.');
+ }
}
// Fetch the antenna
const antenna = await this.antennasRepository.findOneBy({
@@ -98,7 +100,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
let userList;
- if (ps.src === 'list' && ps.userListId) {
+ if ((ps.src === 'list' || antenna.src === 'list') && ps.userListId) {
userList = await this.userListsRepository.findOneBy({
id: ps.userListId,
userId: me.id,
@@ -112,15 +114,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
await this.antennasRepository.update(antenna.id, {
name: ps.name,
src: ps.src,
- userListId: userList ? userList.id : null,
+ userListId: ps.userListId !== undefined ? userList ? userList.id : null : undefined,
keywords: ps.keywords,
excludeKeywords: ps.excludeKeywords,
users: ps.users,
caseSensitive: ps.caseSensitive,
localOnly: ps.localOnly,
+ excludeBots: ps.excludeBots,
withReplies: ps.withReplies,
withFile: ps.withFile,
- notify: ps.notify,
isActive: true,
lastUsedAt: new Date(),
});
diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts
index 8c55673590..9369481649 100644
--- a/packages/backend/src/server/api/endpoints/channels/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts
@@ -16,6 +16,7 @@ import { MetaService } from '@/core/MetaService.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import { MiLocalUser } from '@/models/User.js';
import { ApiError } from '../../error.js';
+import { Brackets } from 'typeorm';
export const meta = {
tags: ['notes', 'channels'],
@@ -51,6 +52,12 @@ export const paramDef = {
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
allowPartial: { type: 'boolean', default: false }, // true is recommended but for compatibility false by default
+ withRenotes: { type: 'boolean', default: true },
+ withFiles: {
+ type: 'boolean',
+ default: false,
+ description: 'Only show notes that have attached files.',
+ },
},
required: ['channelId'],
} as const;
@@ -89,7 +96,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (me) this.activeUsersChart.read(me);
if (!serverSettings.enableFanoutTimeline) {
- return await this.noteEntityService.packMany(await this.getFromDb({ untilId, sinceId, limit: ps.limit, channelId: channel.id }, me), me);
+ return await this.noteEntityService.packMany(await this.getFromDb({ untilId, sinceId, limit: ps.limit, channelId: channel.id, withFiles: ps.withFiles, withRenotes: ps.withRenotes }, me), me);
}
return await this.fanoutTimelineEndpointService.timeline({
@@ -100,9 +107,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
me,
useDbFallback: true,
redisTimelines: [`channelTimeline:${channel.id}`],
- excludePureRenotes: false,
+ excludePureRenotes: !ps.withRenotes,
+ excludeNoFiles: ps.withFiles,
dbFallback: async (untilId, sinceId, limit) => {
- return await this.getFromDb({ untilId, sinceId, limit, channelId: channel.id }, me);
+ return await this.getFromDb({ untilId, sinceId, limit, channelId: channel.id, withFiles: ps.withFiles, withRenotes: ps.withRenotes }, me);
},
});
});
@@ -112,7 +120,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
untilId: string | null,
sinceId: string | null,
limit: number,
- channelId: string
+ channelId: string,
+ withFiles: boolean,
+ withRenotes: boolean,
}, me: MiLocalUser | null) {
//#region fallback to database
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
@@ -128,6 +138,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.queryService.generateMutedUserQuery(query, me);
this.queryService.generateBlockedUserQuery(query, me);
}
+
+ if (ps.withRenotes === false) {
+ query.andWhere(new Brackets(qb => {
+ qb.orWhere('note.renoteId IS NULL');
+ qb.orWhere(new Brackets(qb => {
+ qb.orWhere('note.text IS NOT NULL');
+ qb.orWhere('note.fileIds != \'{}\'');
+ }));
+ }));
+ }
+
+ if (ps.withFiles) {
+ query.andWhere('note.fileIds != \'{}\'');
+ }
//#endregion
return await query.limit(ps.limit).getMany();
diff --git a/packages/backend/src/server/api/endpoints/drive/files/find.ts b/packages/backend/src/server/api/endpoints/drive/files/find.ts
index 595a6957b2..502d42f9e0 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/find.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/find.ts
@@ -54,7 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
folderId: ps.folderId ?? IsNull(),
});
- return await Promise.all(files.map(file => this.driveFileEntityService.pack(file, { self: true })));
+ return await this.driveFileEntityService.packMany(files, { self: true });
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/federation/instances.ts b/packages/backend/src/server/api/endpoints/federation/instances.ts
index 4064a415a4..c3f2247b69 100644
--- a/packages/backend/src/server/api/endpoints/federation/instances.ts
+++ b/packages/backend/src/server/api/endpoints/federation/instances.ts
@@ -125,9 +125,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (typeof ps.suspended === 'boolean') {
if (ps.suspended) {
- query.andWhere('instance.isSuspended = TRUE');
+ query.andWhere('instance.suspensionState != \'none\'');
} else {
- query.andWhere('instance.isSuspended = FALSE');
+ query.andWhere('instance.suspensionState = \'none\'');
}
}
diff --git a/packages/backend/src/server/api/endpoints/fetch-rss.ts b/packages/backend/src/server/api/endpoints/fetch-rss.ts
index 2085b06365..ba48b0119e 100644
--- a/packages/backend/src/server/api/endpoints/fetch-rss.ts
+++ b/packages/backend/src/server/api/endpoints/fetch-rss.ts
@@ -20,13 +20,188 @@ export const meta = {
res: {
type: 'object',
properties: {
+ image: {
+ type: 'object',
+ optional: true,
+ properties: {
+ link: {
+ type: 'string',
+ optional: true,
+ },
+ url: {
+ type: 'string',
+ optional: false,
+ },
+ title: {
+ type: 'string',
+ optional: true,
+ },
+ },
+ },
+ paginationLinks: {
+ type: 'object',
+ optional: true,
+ properties: {
+ self: {
+ type: 'string',
+ optional: true,
+ },
+ first: {
+ type: 'string',
+ optional: true,
+ },
+ next: {
+ type: 'string',
+ optional: true,
+ },
+ last: {
+ type: 'string',
+ optional: true,
+ },
+ prev: {
+ type: 'string',
+ optional: true,
+ },
+ },
+ },
+ link: {
+ type: 'string',
+ optional: true,
+ },
+ title: {
+ type: 'string',
+ optional: true,
+ },
items: {
type: 'array',
+ optional: false,
items: {
type: 'object',
+ properties: {
+ link: {
+ type: 'string',
+ optional: true,
+ },
+ guid: {
+ type: 'string',
+ optional: true,
+ },
+ title: {
+ type: 'string',
+ optional: true,
+ },
+ pubDate: {
+ type: 'string',
+ optional: true,
+ },
+ creator: {
+ type: 'string',
+ optional: true,
+ },
+ summary: {
+ type: 'string',
+ optional: true,
+ },
+ content: {
+ type: 'string',
+ optional: true,
+ },
+ isoDate: {
+ type: 'string',
+ optional: true,
+ },
+ categories: {
+ type: 'array',
+ optional: true,
+ items: {
+ type: 'string',
+ },
+ },
+ contentSnippet: {
+ type: 'string',
+ optional: true,
+ },
+ enclosure: {
+ type: 'object',
+ optional: true,
+ properties: {
+ url: {
+ type: 'string',
+ optional: false,
+ },
+ length: {
+ type: 'number',
+ optional: true,
+ },
+ type: {
+ type: 'string',
+ optional: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ feedUrl: {
+ type: 'string',
+ optional: true,
+ },
+ description: {
+ type: 'string',
+ optional: true,
+ },
+ itunes: {
+ type: 'object',
+ optional: true,
+ additionalProperties: true,
+ properties: {
+ image: {
+ type: 'string',
+ optional: true,
+ },
+ owner: {
+ type: 'object',
+ optional: true,
+ properties: {
+ name: {
+ type: 'string',
+ optional: true,
+ },
+ email: {
+ type: 'string',
+ optional: true,
+ },
+ },
+ },
+ author: {
+ type: 'string',
+ optional: true,
+ },
+ summary: {
+ type: 'string',
+ optional: true,
+ },
+ explicit: {
+ type: 'string',
+ optional: true,
+ },
+ categories: {
+ type: 'array',
+ optional: true,
+ items: {
+ type: 'string',
+ },
+ },
+ keywords: {
+ type: 'array',
+ optional: true,
+ items: {
+ type: 'string',
+ },
+ },
},
- }
- }
+ },
+ },
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/flash/create.ts b/packages/backend/src/server/api/endpoints/flash/create.ts
index 584d167a29..361496e17e 100644
--- a/packages/backend/src/server/api/endpoints/flash/create.ts
+++ b/packages/backend/src/server/api/endpoints/flash/create.ts
@@ -44,6 +44,7 @@ export const paramDef = {
permissions: { type: 'array', items: {
type: 'string',
} },
+ visibility: { type: 'string', enum: ['public', 'private'], default: 'public' },
},
required: ['title', 'summary', 'script', 'permissions'],
} as const;
@@ -66,6 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
summary: ps.summary,
script: ps.script,
permissions: ps.permissions,
+ visibility: ps.visibility,
}).then(x => this.flashsRepository.findOneByOrFail(x.identifiers[0]));
return await this.flashEntityService.pack(flash);
diff --git a/packages/backend/src/server/api/endpoints/following/requests/list.ts b/packages/backend/src/server/api/endpoints/following/requests/list.ts
index 88f559138b..fa59e38976 100644
--- a/packages/backend/src/server/api/endpoints/following/requests/list.ts
+++ b/packages/backend/src/server/api/endpoints/following/requests/list.ts
@@ -71,7 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.limit(ps.limit)
.getMany();
- return await Promise.all(requests.map(req => this.followRequestEntityService.pack(req)));
+ return await this.followRequestEntityService.packMany(requests, me);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
index 74ee90b3dd..a4e61a0e8f 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
@@ -97,10 +97,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
const keyInfo = await this.webAuthnService.verifyRegistration(me.id, ps.credential);
+ const keyId = keyInfo.credentialID;
- const credentialId = Buffer.from(keyInfo.credentialID).toString('base64url');
await this.userSecurityKeysRepository.insert({
- id: credentialId,
+ id: keyId,
userId: me.id,
name: ps.name,
publicKey: Buffer.from(keyInfo.credentialPublicKey).toString('base64url'),
@@ -117,7 +117,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}));
return {
- id: credentialId,
+ id: keyId,
name: ps.name,
};
});
diff --git a/packages/backend/src/server/api/endpoints/i/import-blocking.ts b/packages/backend/src/server/api/endpoints/i/import-blocking.ts
index 8ddbe5663e..2606108539 100644
--- a/packages/backend/src/server/api/endpoints/i/import-blocking.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-blocking.ts
@@ -75,7 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const checkMoving = await this.accountMoveService.validateAlsoKnownAs(
me,
- (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(),
+ (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > Date.now(),
true,
);
if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
diff --git a/packages/backend/src/server/api/endpoints/i/import-following.ts b/packages/backend/src/server/api/endpoints/i/import-following.ts
index 390dd9cd71..d5e824df27 100644
--- a/packages/backend/src/server/api/endpoints/i/import-following.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-following.ts
@@ -75,7 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const checkMoving = await this.accountMoveService.validateAlsoKnownAs(
me,
- (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(),
+ (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > Date.now(),
true,
);
if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
diff --git a/packages/backend/src/server/api/endpoints/i/import-muting.ts b/packages/backend/src/server/api/endpoints/i/import-muting.ts
index 51a9cdf5a5..0f5800404e 100644
--- a/packages/backend/src/server/api/endpoints/i/import-muting.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-muting.ts
@@ -75,7 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const checkMoving = await this.accountMoveService.validateAlsoKnownAs(
me,
- (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(),
+ (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > Date.now(),
true,
);
if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
diff --git a/packages/backend/src/server/api/endpoints/i/import-notes.ts b/packages/backend/src/server/api/endpoints/i/import-notes.ts
index 4e00163550..91ef12c3e3 100644
--- a/packages/backend/src/server/api/endpoints/i/import-notes.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-notes.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: marie and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
@@ -59,7 +64,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
if (file == null) throw new ApiError(meta.errors.noSuchFile);
-
+
if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
if ((await this.roleService.getUserPolicies(me.id)).canImportNotes === false) {
diff --git a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts
index a3b67301a7..bacdd5c88f 100644
--- a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts
@@ -74,7 +74,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const checkMoving = await this.accountMoveService.validateAlsoKnownAs(
me,
- (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(),
+ (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > Date.now(),
true,
);
if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile);
diff --git a/packages/backend/src/server/api/endpoints/i/notifications.ts b/packages/backend/src/server/api/endpoints/i/notifications.ts
index 594e8b95c8..5e97b90f99 100644
--- a/packages/backend/src/server/api/endpoints/i/notifications.ts
+++ b/packages/backend/src/server/api/endpoints/i/notifications.ts
@@ -7,7 +7,7 @@ import { In } from 'typeorm';
import * as Redis from 'ioredis';
import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository } from '@/models/_.js';
-import { obsoleteNotificationTypes, notificationTypes, FilterUnionByProperty } from '@/types.js';
+import { FilterUnionByProperty, notificationTypes, obsoleteNotificationTypes } from '@/types.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteReadService } from '@/core/NoteReadService.js';
import { NotificationEntityService } from '@/core/entities/NotificationEntityService.js';
@@ -84,27 +84,51 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const includeTypes = ps.includeTypes && ps.includeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
const excludeTypes = ps.excludeTypes && ps.excludeTypes.filter(type => !(obsoleteNotificationTypes).includes(type as any)) as typeof notificationTypes[number][];
- const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
- const notificationsRes = await this.redisClient.xrevrange(
- `notificationTimeline:${me.id}`,
- ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
- ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime() : '-',
- 'COUNT', limit);
+ let sinceTime = ps.sinceId ? this.idService.parse(ps.sinceId).date.getTime().toString() : null;
+ let untilTime = ps.untilId ? this.idService.parse(ps.untilId).date.getTime().toString() : null;
- if (notificationsRes.length === 0) {
- return [];
- }
+ let notifications: MiNotification[];
+ for (;;) {
+ let notificationsRes: [id: string, fields: string[]][];
- let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId && x !== ps.sinceId) as MiNotification[];
+ // sinceidのみの場合は古い順、そうでない場合は新しい順。 QueryService.makePaginationQueryも参照
+ if (sinceTime && !untilTime) {
+ notificationsRes = await this.redisClient.xrange(
+ `notificationTimeline:${me.id}`,
+ '(' + sinceTime,
+ '+',
+ 'COUNT', ps.limit);
+ } else {
+ notificationsRes = await this.redisClient.xrevrange(
+ `notificationTimeline:${me.id}`,
+ untilTime ? '(' + untilTime : '+',
+ sinceTime ? '(' + sinceTime : '-',
+ 'COUNT', ps.limit);
+ }
- if (includeTypes && includeTypes.length > 0) {
- notifications = notifications.filter(notification => includeTypes.includes(notification.type));
- } else if (excludeTypes && excludeTypes.length > 0) {
- notifications = notifications.filter(notification => !excludeTypes.includes(notification.type));
- }
+ if (notificationsRes.length === 0) {
+ return [];
+ }
- if (notifications.length === 0) {
- return [];
+ notifications = notificationsRes.map(x => JSON.parse(x[1][1])) as MiNotification[];
+
+ if (includeTypes && includeTypes.length > 0) {
+ notifications = notifications.filter(notification => includeTypes.includes(notification.type));
+ } else if (excludeTypes && excludeTypes.length > 0) {
+ notifications = notifications.filter(notification => !excludeTypes.includes(notification.type));
+ }
+
+ if (notifications.length !== 0) {
+ // 通知が1件以上ある場合は返す
+ break;
+ }
+
+ // フィルタしたことで通知が0件になった場合、次のページを取得する
+ if (ps.sinceId && !ps.untilId) {
+ sinceTime = notificationsRes[notificationsRes.length - 1][0];
+ } else {
+ untilTime = notificationsRes[notificationsRes.length - 1][0];
+ }
}
// Mark all as read
diff --git a/packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts b/packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts
index bb471284c9..3aa256077e 100644
--- a/packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts
+++ b/packages/backend/src/server/api/endpoints/i/registry/get-unsecure.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: marie and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { RegistryItemsRepository } from '@/models/_.js';
diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts
index 08a8301bd1..7332026d84 100644
--- a/packages/backend/src/server/api/endpoints/i/update-email.ts
+++ b/packages/backend/src/server/api/endpoints/i/update-email.ts
@@ -16,6 +16,7 @@ import { DI } from '@/di-symbols.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js';
import { UserAuthService } from '@/core/UserAuthService.js';
+import { MetaService } from '@/core/MetaService.js';
import { ApiError } from '../../error.js';
export const meta = {
@@ -40,6 +41,12 @@ export const meta = {
code: 'UNAVAILABLE',
id: 'a2defefb-f220-8849-0af6-17f816099323',
},
+
+ emailRequired: {
+ message: 'Email address is required.',
+ code: 'EMAIL_REQUIRED',
+ id: '324c7a88-59f2-492f-903f-89134f93e47e',
+ },
},
res: {
@@ -67,6 +74,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
+ private metaService: MetaService,
private userEntityService: UserEntityService,
private emailService: EmailService,
private userAuthService: UserAuthService,
@@ -98,6 +106,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (!res.available) {
throw new ApiError(meta.errors.unavailable);
}
+ } else if ((await this.metaService.fetch()).emailRequiredForSignup) {
+ throw new ApiError(meta.errors.emailRequired);
}
await this.userProfilesRepository.update(me.id, {
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 06edb28578..aa2f85845f 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -530,26 +530,32 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private async verifyLink(url: string, user: MiLocalUser) {
if (!safeForSql(url)) return;
- const html = await this.httpRequestService.getHtml(url);
+ try {
+ const html = await this.httpRequestService.getHtml(url);
- const { window } = new JSDOM(html);
- const doc = window.document;
+ const { window } = new JSDOM(html);
+ const doc = window.document;
- const myLink = `${this.config.url}/@${user.username}`;
+ const myLink = `${this.config.url}/@${user.username}`;
- const aEls = Array.from(doc.getElementsByTagName('a'));
- const linkEls = Array.from(doc.getElementsByTagName('link'));
+ const aEls = Array.from(doc.getElementsByTagName('a'));
+ const linkEls = Array.from(doc.getElementsByTagName('link'));
- const includesMyLink = aEls.some(a => a.href === myLink);
- const includesRelMeLinks = [...aEls, ...linkEls].some(link => link.rel === 'me' && link.href === myLink);
+ const includesMyLink = aEls.some(a => a.href === myLink);
+ const includesRelMeLinks = [...aEls, ...linkEls].some(link => link.rel === 'me' && link.href === myLink);
- if (includesMyLink || includesRelMeLinks) {
- await this.userProfilesRepository.createQueryBuilder('profile').update()
- .where('userId = :userId', { userId: user.id })
- .set({
- verifiedLinks: () => `array_append("verifiedLinks", '${url}')`, // ここでSQLインジェクションされそうなのでとりあえず safeForSql で弾いている
- })
- .execute();
+ if (includesMyLink || includesRelMeLinks) {
+ await this.userProfilesRepository.createQueryBuilder('profile').update()
+ .where('userId = :userId', { userId: user.id })
+ .set({
+ verifiedLinks: () => `array_append("verifiedLinks", '${url}')`, // ここでSQLインジェクションされそうなのでとりあえず safeForSql で弾いている
+ })
+ .execute();
+ }
+
+ window.close();
+ } catch (err) {
+ // なにもしない
}
}
}
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index 95ebda2f21..626f03b758 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.ts
@@ -16,7 +16,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteCreateService } from '@/core/NoteCreateService.js';
import { DI } from '@/di-symbols.js';
-import { isPureRenote } from '@/misc/is-pure-renote.js';
+import { isQuote, isRenote } from '@/misc/is-renote.js';
import { MetaService } from '@/core/MetaService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
@@ -32,6 +32,7 @@ export const meta = {
limit: {
duration: ms('1hour'),
max: 300,
+ minInterval: ms('1sec'),
},
kind: 'write:notes',
@@ -286,7 +287,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (renote == null) {
throw new ApiError(meta.errors.noSuchRenoteTarget);
- } else if (isPureRenote(renote)) {
+ } else if (isRenote(renote) && !isQuote(renote)) {
throw new ApiError(meta.errors.cannotReRenote);
}
@@ -332,7 +333,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (reply == null) {
throw new ApiError(meta.errors.noSuchReplyTarget);
- } else if (isPureRenote(reply)) {
+ } else if (isRenote(reply) && !isQuote(reply)) {
throw new ApiError(meta.errors.cannotReplyToPureRenote);
} else if (!await this.noteEntityService.isVisibleForMe(reply, me.id)) {
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
diff --git a/packages/backend/src/server/api/endpoints/notes/edit.ts b/packages/backend/src/server/api/endpoints/notes/edit.ts
index 3caeda288b..835cbc14fa 100644
--- a/packages/backend/src/server/api/endpoints/notes/edit.ts
+++ b/packages/backend/src/server/api/endpoints/notes/edit.ts
@@ -11,7 +11,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteEditService } from '@/core/NoteEditService.js';
import { DI } from '@/di-symbols.js';
-import { isPureRenote } from '@/misc/is-pure-renote.js';
+import { isQuote, isRenote } from '@/misc/is-renote.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { ApiError } from '../../error.js';
@@ -336,7 +336,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (renote == null) {
throw new ApiError(meta.errors.noSuchRenoteTarget);
- } else if (isPureRenote(renote)) {
+ } else if (isRenote(renote) && !isQuote(renote)) {
throw new ApiError(meta.errors.cannotReRenote);
}
@@ -386,7 +386,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (reply == null) {
throw new ApiError(meta.errors.noSuchReplyTarget);
- } else if (isPureRenote(reply)) {
+ } else if (isRenote(reply) && !isQuote(reply)) {
throw new ApiError(meta.errors.cannotReplyToPureRenote);
} else if (!await this.noteEntityService.isVisibleForMe(reply, me.id)) {
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
diff --git a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts
index ba38573065..4fd6f8682d 100644
--- a/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts
+++ b/packages/backend/src/server/api/endpoints/notes/polls/recommendation.ts
@@ -32,6 +32,7 @@ export const paramDef = {
properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
offset: { type: 'integer', default: 0 },
+ excludeChannels: { type: 'boolean', default: false },
},
required: [],
} as const;
@@ -86,6 +87,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
query.setParameters(mutingQuery.getParameters());
//#endregion
+ //#region exclude channels
+ if (ps.excludeChannels) {
+ query.andWhere('poll.channelId IS NULL');
+ }
+ //#endregion
+
const polls = await query
.orderBy('poll.noteId', 'DESC')
.limit(ps.limit)
diff --git a/packages/backend/src/server/api/endpoints/notes/reactions.ts b/packages/backend/src/server/api/endpoints/notes/reactions.ts
index 3beb5064ae..7e334df93e 100644
--- a/packages/backend/src/server/api/endpoints/notes/reactions.ts
+++ b/packages/backend/src/server/api/endpoints/notes/reactions.ts
@@ -79,7 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const reactions = await query.limit(ps.limit).getMany();
- return await Promise.all(reactions.map(reaction => this.noteReactionEntityService.pack(reaction, me)));
+ return await this.noteReactionEntityService.packMany(reactions, me);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts
index a935f761b7..d6ef655291 100644
--- a/packages/backend/src/server/api/endpoints/notes/translate.ts
+++ b/packages/backend/src/server/api/endpoints/notes/translate.ts
@@ -21,7 +21,7 @@ export const meta = {
res: {
type: 'object',
- optional: false, nullable: false,
+ optional: true, nullable: false,
properties: {
sourceLang: { type: 'string' },
text: { type: 'string' },
@@ -39,6 +39,11 @@ export const meta = {
code: 'NO_SUCH_NOTE',
id: 'bea9b03f-36e0-49c5-a4db-627a029f8971',
},
+ cannotTranslateInvisibleNote: {
+ message: 'Cannot translate invisible note.',
+ code: 'CANNOT_TRANSLATE_INVISIBLE_NOTE',
+ id: 'ea29f2ca-c368-43b3-aaf1-5ac3e74bbe5d',
+ },
},
} as const;
@@ -72,21 +77,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
if (!(await this.noteEntityService.isVisibleForMe(note, me.id))) {
- return 204; // TODO: 良い感じのエラー返す
+ throw new ApiError(meta.errors.cannotTranslateInvisibleNote);
}
if (note.text == null) {
- return 204;
+ return;
}
const instance = await this.metaService.fetch();
if (instance.deeplAuthKey == null && !instance.deeplFreeMode) {
- return 204; // TODO: 良い感じのエラー返す
+ throw new ApiError(meta.errors.unavailable);
}
if (instance.deeplFreeMode && !instance.deeplFreeInstance) {
- return 204;
+ throw new ApiError(meta.errors.unavailable);
}
let targetLang = ps.targetLang;
diff --git a/packages/backend/src/server/api/endpoints/roles/users.ts b/packages/backend/src/server/api/endpoints/roles/users.ts
index 85d100ce1c..48d350af59 100644
--- a/packages/backend/src/server/api/endpoints/roles/users.ts
+++ b/packages/backend/src/server/api/endpoints/roles/users.ts
@@ -92,9 +92,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.limit(ps.limit)
.getMany();
+ const _users = assigns.map(({ user, userId }) => user ?? userId);
+ const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' })
+ .then(users => new Map(users.map(u => [u.id, u])));
return await Promise.all(assigns.map(async assign => ({
id: assign.id,
- user: await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
+ user: _userMap.get(assign.userId) ?? await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
})));
});
}
diff --git a/packages/backend/src/server/api/endpoints/sponsors.ts b/packages/backend/src/server/api/endpoints/sponsors.ts
index 50e1c594f2..b6ccb9b2f9 100644
--- a/packages/backend/src/server/api/endpoints/sponsors.ts
+++ b/packages/backend/src/server/api/endpoints/sponsors.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: marie and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import { Endpoint } from '@/server/api/endpoint-base.js';
diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts
index 5d52ebba76..6b3389f0b2 100644
--- a/packages/backend/src/server/api/endpoints/users/following.ts
+++ b/packages/backend/src/server/api/endpoints/users/following.ts
@@ -6,6 +6,7 @@
import { IsNull } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { UsersRepository, FollowingsRepository, UserProfilesRepository } from '@/models/_.js';
+import { birthdaySchema } from '@/models/User.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js';
@@ -66,7 +67,7 @@ export const paramDef = {
description: 'The local host is represented with `null`.',
},
- birthday: { type: 'string', nullable: true },
+ birthday: { ...birthdaySchema, nullable: true },
},
anyOf: [
{ required: ['userId'] },
@@ -127,9 +128,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.birthday) {
try {
- const d = new Date(ps.birthday);
- d.setHours(0, 0, 0, 0);
- const birthday = `${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`;
+ const birthday = ps.birthday.substring(5, 10);
const birthdayUserQuery = this.userProfilesRepository.createQueryBuilder('user_profile');
birthdayUserQuery.select('user_profile.userId')
.where(`SUBSTR(user_profile.birthday, 6, 5) = '${birthday}'`);
diff --git a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts
index 02aa037466..9248a2fa68 100644
--- a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts
+++ b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts
@@ -118,12 +118,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]);
// Extract top replied users
- const topRepliedUsers = repliedUsersSorted.slice(0, ps.limit);
+ const topRepliedUserIds = repliedUsersSorted.slice(0, ps.limit);
// Make replies object (includes weights)
- const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({
- user: await this.userEntityService.pack(user, me, { schema: 'UserDetailed' }),
- weight: repliedUsers[user] / peak,
+ const _userMap = await this.userEntityService.packMany(topRepliedUserIds, me, { schema: 'UserDetailed' })
+ .then(users => new Map(users.map(u => [u.id, u])));
+ const repliesObj = await Promise.all(topRepliedUserIds.map(async (userId) => ({
+ user: _userMap.get(userId) ?? await this.userEntityService.pack(userId, me, { schema: 'UserDetailed' }),
+ weight: repliedUsers[userId] / peak,
})));
return repliesObj;
diff --git a/packages/backend/src/server/api/endpoints/users/relation.ts b/packages/backend/src/server/api/endpoints/users/relation.ts
index 6a5b2262fa..1d75437b81 100644
--- a/packages/backend/src/server/api/endpoints/users/relation.ts
+++ b/packages/backend/src/server/api/endpoints/users/relation.ts
@@ -132,11 +132,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private userEntityService: UserEntityService,
) {
super(meta, paramDef, async (ps, me) => {
- const ids = Array.isArray(ps.userId) ? ps.userId : [ps.userId];
-
- const relations = await Promise.all(ids.map(id => this.userEntityService.getRelation(me.id, id)));
-
- return Array.isArray(ps.userId) ? relations : relations[0];
+ return Array.isArray(ps.userId)
+ ? await this.userEntityService.getRelations(me.id, ps.userId).then(it => [...it.values()])
+ : await this.userEntityService.getRelation(me.id, ps.userId).then(it => [it]);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts
index bd81989cb9..062326e28d 100644
--- a/packages/backend/src/server/api/endpoints/users/show.ts
+++ b/packages/backend/src/server/api/endpoints/users/show.ts
@@ -110,14 +110,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
// リクエストされた通りに並べ替え
+ // 順番は保持されるけど数は減ってる可能性がある
const _users: MiUser[] = [];
for (const id of ps.userIds) {
- _users.push(users.find(x => x.id === id)!);
+ const user = users.find(x => x.id === id);
+ if (user != null) _users.push(user);
}
- return await Promise.all(_users.map(u => this.userEntityService.pack(u, me, {
- schema: 'UserDetailed',
- })));
+ const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' })
+ .then(users => new Map(users.map(u => [u.id, u])));
+ return _users.map(u => _userMap.get(u.id)!);
} else {
// Lookup user
if (typeof ps.host === 'string' && typeof ps.username === 'string') {
diff --git a/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts b/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts
index 5706a17786..8c9cca1730 100644
--- a/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts
+++ b/packages/backend/src/server/api/mastodon/MastodonApiServerService.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: marie and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import { Inject, Injectable } from '@nestjs/common';
import megalodon, { Entity, MegalodonInterface } from 'megalodon';
import querystring from 'querystring';
@@ -89,7 +94,7 @@ export class MastodonApiServerService {
reply.code(401).send(e.response.data);
}
});
-
+
fastify.get('/v1/instance', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
@@ -113,7 +118,7 @@ export class MastodonApiServerService {
reply.code(401).send(e.response.data);
}
});
-
+
fastify.get('/v1/announcements', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
@@ -126,7 +131,7 @@ export class MastodonApiServerService {
reply.code(401).send(e.response.data);
}
});
-
+
fastify.post<{ Body: { id: string } }>('/v1/announcements/:id/dismiss', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
@@ -177,8 +182,8 @@ export class MastodonApiServerService {
/* console.error(e); */
reply.code(401).send(e.response.data);
}
- });
-
+ });
+
fastify.get('/v1/filters', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
@@ -192,7 +197,7 @@ export class MastodonApiServerService {
reply.code(401).send(e.response.data);
}
});
-
+
fastify.get('/v1/trends', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
@@ -238,7 +243,7 @@ export class MastodonApiServerService {
reply.code(401).send(e.response.data);
}
});
-
+
fastify.get('/v1/preferences', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
@@ -291,18 +296,18 @@ export class MastodonApiServerService {
user: { id: tokeninfo.userId, host: null },
path: avatar.path,
name: avatar.originalname !== null && avatar.originalname !== 'file' ? avatar.originalname : undefined,
- sensitive: false,
+ sensitive: false,
});
if (upload.type.startsWith('image/')) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(_request.body as any).avatar = upload.id;
}
- } else if (tokeninfo && header) {
+ } else if (tokeninfo && header) {
const upload = await this.driveService.addFile({
user: { id: tokeninfo.userId, host: null },
path: header.path,
name: header.originalname !== null && header.originalname !== 'file' ? header.originalname : undefined,
- sensitive: false,
+ sensitive: false,
});
if (upload.type.startsWith('image/')) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -324,7 +329,7 @@ export class MastodonApiServerService {
};
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- (_request.body as any).fields_attributes = fields.filter((field: any) => field.name.trim().length > 0 && field.value.length > 0);
+ (_request.body as any).fields_attributes = fields.filter((field: any) => field.name.trim().length > 0 && field.value.length > 0);
}
const data = await client.updateCredentials(_request.body!);
diff --git a/packages/backend/src/server/api/mastodon/converters.ts b/packages/backend/src/server/api/mastodon/converters.ts
index 326d3a1d5c..405701e826 100644
--- a/packages/backend/src/server/api/mastodon/converters.ts
+++ b/packages/backend/src/server/api/mastodon/converters.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: marie and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import { Inject, Injectable } from '@nestjs/common';
import { Entity } from 'megalodon';
import mfm from '@transfem-org/sfm-js';
@@ -110,7 +115,7 @@ export class MastoConverters {
private async encodeField(f: Entity.Field): Promise<Entity.Field> {
return {
name: f.name,
- value: await this.mfmService.toMastoHtml(mfm.parse(f.value), [], true) ?? escapeMFM(f.value),
+ value: await this.mfmService.toMastoApiHtml(mfm.parse(f.value), [], true) ?? escapeMFM(f.value),
verified_at: null,
};
}
@@ -146,8 +151,8 @@ export class MastoConverters {
display_name: user.name ?? user.username,
locked: user.isLocked,
created_at: this.idService.parse(user.id).date.toISOString(),
- followers_count: user.followersCount,
- following_count: user.followingCount,
+ followers_count: profile?.followersVisibility === 'public' ? user.followersCount : 0,
+ following_count: profile?.followingVisibility === 'public' ? user.followingCount : 0,
statuses_count: user.notesCount,
note: profile?.description ?? '',
url: user.uri ?? acctUrl,
@@ -179,7 +184,7 @@ export class MastoConverters {
const files = this.driveFileEntityService.packManyByIds(edit.fileIds);
const item = {
account: noteUser,
- content: this.mfmService.toMastoHtml(mfm.parse(edit.newText ?? ''), JSON.parse(note.mentionedRemoteUsers)).then(p => p ?? ''),
+ content: this.mfmService.toMastoApiHtml(mfm.parse(edit.newText ?? ''), JSON.parse(note.mentionedRemoteUsers)).then(p => p ?? ''),
created_at: lastDate.toISOString(),
emojis: [],
sensitive: files.then(files => files.length > 0 ? files.some((f) => f.isSensitive) : false),
@@ -240,7 +245,7 @@ export class MastoConverters {
});
const content = note.text !== null
- ? quoteUri.then(quoteUri => this.mfmService.toMastoHtml(mfm.parse(note.text!), JSON.parse(note.mentionedRemoteUsers), false, quoteUri))
+ ? quoteUri.then(quoteUri => this.mfmService.toMastoApiHtml(mfm.parse(note.text!), JSON.parse(note.mentionedRemoteUsers), false, quoteUri))
.then(p => p ?? escapeMFM(note.text!))
: '';
diff --git a/packages/backend/src/server/api/mastodon/endpoints.ts b/packages/backend/src/server/api/mastodon/endpoints.ts
index 5a75823891..085314059b 100644
--- a/packages/backend/src/server/api/mastodon/endpoints.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: marie and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import { ApiAuthMastodon } from './endpoints/auth.js';
import { ApiAccountMastodon } from './endpoints/account.js';
import { ApiSearchMastodon } from './endpoints/search.js';
diff --git a/packages/backend/src/server/api/mastodon/endpoints/account.ts b/packages/backend/src/server/api/mastodon/endpoints/account.ts
index 07d9efb8c1..6fcfb0019c 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/account.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/account.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: marie and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import { MastoConverters, convertRelationship } from '../converters.js';
import { argsToBools, limitToInt } from './timeline.js';
import type { MegalodonInterface } from 'megalodon';
@@ -104,7 +109,7 @@ export class ApiAccountMastodon {
public async getFollowers() {
try {
const data = await this.client.getAccountFollowers(
- (this.request.params as any).id,
+ (this.request.params as any).id,
limitToInt(this.request.query as any),
);
return await Promise.all(data.data.map(async (account) => await this.mastoconverter.convertAccount(account)));
@@ -118,7 +123,7 @@ export class ApiAccountMastodon {
public async getFollowing() {
try {
const data = await this.client.getAccountFollowing(
- (this.request.params as any).id,
+ (this.request.params as any).id,
limitToInt(this.request.query as any),
);
return await Promise.all(data.data.map(async (account) => await this.mastoconverter.convertAccount(account)));
diff --git a/packages/backend/src/server/api/mastodon/endpoints/auth.ts b/packages/backend/src/server/api/mastodon/endpoints/auth.ts
index 80a5e89ca3..a447bdb1b7 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/auth.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/auth.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: marie and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import type { MegalodonInterface } from 'megalodon';
import type { FastifyRequest } from 'fastify';
@@ -65,7 +70,7 @@ export async function ApiAuthMastodon(request: FastifyRequest, client: Megalodon
client_id: Buffer.from(appData.url || '').toString('base64'),
client_secret: appData.clientSecret,
};
-
+
return returns;
} catch (e: any) {
console.error(e);
diff --git a/packages/backend/src/server/api/mastodon/endpoints/filter.ts b/packages/backend/src/server/api/mastodon/endpoints/filter.ts
index 212c79b251..ce6809d230 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/filter.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/filter.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: marie and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import { convertFilter } from '../converters.js';
import type { MegalodonInterface } from 'megalodon';
import type { FastifyRequest } from 'fastify';
diff --git a/packages/backend/src/server/api/mastodon/endpoints/meta.ts b/packages/backend/src/server/api/mastodon/endpoints/meta.ts
index efb39ef939..79d2c62a24 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/meta.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/meta.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: marie and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import { Entity } from 'megalodon';
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
import type { Config } from '@/config.js';
diff --git a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts
index c4628b58c4..0eefb5894c 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: marie and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import { convertNotification } from '../converters.js';
import type { MegalodonInterface, Entity } from 'megalodon';
import type { FastifyRequest } from 'fastify';
diff --git a/packages/backend/src/server/api/mastodon/endpoints/search.ts b/packages/backend/src/server/api/mastodon/endpoints/search.ts
index 500129c901..946e796e2a 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/search.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/search.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: marie and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import { MastoConverters } from '../converters.js';
import { limitToInt } from './timeline.js';
import type { MegalodonInterface } from 'megalodon';
diff --git a/packages/backend/src/server/api/mastodon/endpoints/status.ts b/packages/backend/src/server/api/mastodon/endpoints/status.ts
index fe77646af4..ddc99639fa 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/status.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/status.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: marie and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import querystring from 'querystring';
import { emojiRegexAtStartToEnd } from '@/misc/emoji-regex.js';
import { convertAttachment, convertPoll, convertStatusSource, MastoConverters } from '../converters.js';
diff --git a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts
index f81b63b9ac..3eb4898713 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts
@@ -1,3 +1,8 @@
+/*
+ * SPDX-FileCopyrightText: marie and other Sharkey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
import { ParsedUrlQuery } from 'querystring';
import { convertConversation, convertList, MastoConverters } from '../converters.js';
import { getClient } from '../MastodonApiServerService.js';
diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts
index b5f91ff542..436f9a44bb 100644
--- a/packages/backend/src/server/api/openapi/gen-spec.ts
+++ b/packages/backend/src/server/api/openapi/gen-spec.ts
@@ -93,7 +93,7 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) {
const hasBody = (schema.type === 'object' && schema.properties && Object.keys(schema.properties).length >= 1);
const info = {
- operationId: endpoint.name,
+ operationId: endpoint.name.replaceAll('/', '___'), // NOTE: スラッシュは使えない
summary: endpoint.name,
description: desc,
externalDocs: {
diff --git a/packages/backend/src/server/api/stream/channel.ts b/packages/backend/src/server/api/stream/channel.ts
index 44a143538b..49b0ae1d5b 100644
--- a/packages/backend/src/server/api/stream/channel.ts
+++ b/packages/backend/src/server/api/stream/channel.ts
@@ -4,6 +4,10 @@
*/
import { bindThis } from '@/decorators.js';
+import { isInstanceMuted } from '@/misc/is-instance-muted.js';
+import { isUserRelated } from '@/misc/is-user-related.js';
+import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
+import type { Packed } from '@/misc/json-schema.js';
import type Connection from './Connection.js';
/**
@@ -54,6 +58,24 @@ export default abstract class Channel {
return this.connection.subscriber;
}
+ /*
+ * ミュートとブロックされてるを処理する
+ */
+ protected isNoteMutedOrBlocked(note: Packed<'Note'>): boolean {
+ // 流れてきたNoteがインスタンスミュートしたインスタンスが関わる
+ if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? [])) && !this.following[note.userId]) return true;
+
+ // 流れてきたNoteがミュートしているユーザーが関わる
+ if (isUserRelated(note, this.userIdsWhoMeMuting)) return true;
+ // 流れてきたNoteがブロックされているユーザーが関わる
+ if (isUserRelated(note, this.userIdsWhoBlockingMe)) return true;
+
+ // 流れてきたNoteがリノートをミュートしてるユーザが行ったもの
+ if (isRenotePacked(note) && !isQuotePacked(note) && this.userIdsWhoMeMutingRenotes.has(note.user.id)) return true;
+
+ return false;
+ }
+
constructor(id: string, connection: Connection) {
this.id = id;
this.connection = connection;
diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts
index 135d162e63..4a1d2dd109 100644
--- a/packages/backend/src/server/api/stream/channels/antenna.ts
+++ b/packages/backend/src/server/api/stream/channels/antenna.ts
@@ -4,7 +4,6 @@
*/
import { Injectable } from '@nestjs/common';
-import { isUserRelated } from '@/misc/is-user-related.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
@@ -40,12 +39,7 @@ class AntennaChannel extends Channel {
if (data.type === 'note') {
const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true });
- // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
- // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
-
- if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
+ if (this.isNoteMutedOrBlocked(note)) return;
this.connection.cacheNote(note);
diff --git a/packages/backend/src/server/api/stream/channels/bubble-timeline.ts b/packages/backend/src/server/api/stream/channels/bubble-timeline.ts
index 2d85d65ba5..01be2d2089 100644
--- a/packages/backend/src/server/api/stream/channels/bubble-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/bubble-timeline.ts
@@ -4,15 +4,13 @@
*/
import { Injectable } from '@nestjs/common';
-import { checkWordMute } from '@/misc/check-word-mute.js';
-import { isInstanceMuted } from '@/misc/is-instance-muted.js';
-import { isUserRelated } from '@/misc/is-user-related.js';
import type { Packed } from '@/misc/json-schema.js';
import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import type { MiMeta } from '@/models/Meta.js';
+import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import Channel, { MiChannelService } from '../channel.js';
class BubbleTimelineChannel extends Channel {
@@ -54,36 +52,18 @@ class BubbleTimelineChannel extends Channel {
private async onNote(note: Packed<'Note'>) {
if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
if (!this.withBots && note.user.isBot) return;
-
+
if (!(note.user.host != null && this.instance.bubbleInstances.includes(note.user.host) && note.visibility === 'public' )) return;
if (note.channelId != null) return;
- // 関係ない返信は除外
- if (note.reply && !this.following[note.userId]?.withReplies) {
- const reply = note.reply;
- // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
- if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
- }
+ if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return;
if (note.user.isSilenced && !this.following[note.userId] && note.userId !== this.user!.id) return;
- if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
-
- // Ignore notes from instances the user has muted
- if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? [])) && !this.following[note.userId]) return;
-
- // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
- // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
-
- if (note.renote && !note.text && note.renote.mentions?.some(mention => this.userIdsWhoMeMuting.has(mention))) return;
- if (note.mentions?.some(mention => this.userIdsWhoMeMuting.has(mention))) return;
-
- if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
+ if (this.isNoteMutedOrBlocked(note)) return;
- if (this.user && note.renoteId && !note.text) {
+ if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts
index 90ee1ecda5..865e4fed19 100644
--- a/packages/backend/src/server/api/stream/channels/channel.ts
+++ b/packages/backend/src/server/api/stream/channels/channel.ts
@@ -4,10 +4,10 @@
*/
import { Injectable } from '@nestjs/common';
-import { isUserRelated } from '@/misc/is-user-related.js';
import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
+import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import Channel, { type MiChannelService } from '../channel.js';
class ChannelChannel extends Channel {
@@ -15,6 +15,8 @@ class ChannelChannel extends Channel {
public static shouldShare = false;
public static requireCredential = false as const;
private channelId: string;
+ private withFiles: boolean;
+ private withRenotes: boolean;
constructor(
private noteEntityService: NoteEntityService,
@@ -29,6 +31,8 @@ class ChannelChannel extends Channel {
@bindThis
public async init(params: any) {
this.channelId = params.channelId as string;
+ this.withFiles = params.withFiles ?? false;
+ this.withRenotes = params.withRenotes ?? true;
// Subscribe stream
this.subscriber.on('notesStream', this.onNote);
@@ -38,14 +42,13 @@ class ChannelChannel extends Channel {
private async onNote(note: Packed<'Note'>) {
if (note.channelId !== this.channelId) return;
- // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
- // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
+ if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
- if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
+ if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
- if (this.user && note.renoteId && !note.text) {
+ if (this.isNoteMutedOrBlocked(note)) return;
+
+ if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts
index fc25724782..0a894147a2 100644
--- a/packages/backend/src/server/api/stream/channels/global-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts
@@ -4,14 +4,12 @@
*/
import { Injectable } from '@nestjs/common';
-import { checkWordMute } from '@/misc/check-word-mute.js';
-import { isInstanceMuted } from '@/misc/is-instance-muted.js';
-import { isUserRelated } from '@/misc/is-user-related.js';
import type { Packed } from '@/misc/json-schema.js';
import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
+import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import Channel, { type MiChannelService } from '../channel.js';
class GlobalTimelineChannel extends Channel {
@@ -55,31 +53,13 @@ class GlobalTimelineChannel extends Channel {
if (note.visibility !== 'public') return;
if (note.channelId != null) return;
- // 関係ない返信は除外
- if (note.reply && !this.following[note.userId]?.withReplies) {
- const reply = note.reply;
- // 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
- if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
- }
+ if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return;
if (note.user.isSilenced && !this.following[note.userId] && note.userId !== this.user!.id) return;
- if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
-
- // Ignore notes from instances the user has muted
- if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? [])) && !this.following[note.userId]) return;
-
- // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
- // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
-
- if (note.renote && !note.text && note.renote.mentions?.some(mention => this.userIdsWhoMeMuting.has(mention))) return;
- if (note.mentions?.some(mention => this.userIdsWhoMeMuting.has(mention))) return;
-
- if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
+ if (this.isNoteMutedOrBlocked(note)) return;
- if (this.user && note.renoteId && !note.text) {
+ if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts
index 377b1a0162..57bada5d9c 100644
--- a/packages/backend/src/server/api/stream/channels/hashtag.ts
+++ b/packages/backend/src/server/api/stream/channels/hashtag.ts
@@ -5,10 +5,10 @@
import { Injectable } from '@nestjs/common';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
-import { isUserRelated } from '@/misc/is-user-related.js';
import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
+import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import Channel, { type MiChannelService } from '../channel.js';
class HashtagChannel extends Channel {
@@ -43,14 +43,9 @@ class HashtagChannel extends Channel {
const matched = this.q.some(tags => tags.every(tag => noteTags.includes(normalizeForSearch(tag))));
if (!matched) return;
- // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
- // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
+ if (this.isNoteMutedOrBlocked(note)) return;
- if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
-
- if (this.user && note.renoteId && !note.text) {
+ if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts
index 0a4852ee8d..84ff241469 100644
--- a/packages/backend/src/server/api/stream/channels/home-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts
@@ -4,12 +4,10 @@
*/
import { Injectable } from '@nestjs/common';
-import { checkWordMute } from '@/misc/check-word-mute.js';
-import { isUserRelated } from '@/misc/is-user-related.js';
-import { isInstanceMuted } from '@/misc/is-instance-muted.js';
import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
+import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import Channel, { type MiChannelService } from '../channel.js';
class HomeTimelineChannel extends Channel {
@@ -51,9 +49,6 @@ class HomeTimelineChannel extends Channel {
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
}
- // Ignore notes from instances the user has muted
- if (isInstanceMuted(note, new Set<string>(this.userProfile!.mutedInstances)) && !this.following[note.userId]) return;
-
if (note.visibility === 'followers') {
if (!isMe && !Object.hasOwn(this.following, note.userId)) return;
} else if (note.visibility === 'specified') {
@@ -74,7 +69,7 @@ class HomeTimelineChannel extends Channel {
if (note.user.isSilenced && !this.following[note.userId] && note.userId !== this.user!.id) return;
// 純粋なリノート(引用リノートでないリノート)の場合
- if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && note.poll == null) {
+ if (isRenotePacked(note) && !isQuotePacked(note) && note.renote) {
if (!this.withRenotes) return;
if (note.renote.reply) {
const reply = note.renote.reply;
@@ -83,17 +78,9 @@ class HomeTimelineChannel extends Channel {
}
}
- // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
- // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
-
- if (note.renote && !note.text && note.renote.mentions?.some(mention => this.userIdsWhoMeMuting.has(mention))) return;
- if (note.mentions?.some(mention => this.userIdsWhoMeMuting.has(mention))) return;
-
- if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
+ if (this.isNoteMutedOrBlocked(note)) return;
- if (this.user && note.renoteId && !note.text) {
+ if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
index 02786e9e16..b83d3ec817 100644
--- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
@@ -4,14 +4,12 @@
*/
import { Injectable } from '@nestjs/common';
-import { checkWordMute } from '@/misc/check-word-mute.js';
-import { isUserRelated } from '@/misc/is-user-related.js';
-import { isInstanceMuted } from '@/misc/is-instance-muted.js';
import type { Packed } from '@/misc/json-schema.js';
import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
+import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import Channel, { type MiChannelService } from '../channel.js';
class HybridTimelineChannel extends Channel {
@@ -74,8 +72,7 @@ class HybridTimelineChannel extends Channel {
if (!isMe && !note.visibleUserIds!.includes(this.user!.id)) return;
}
- // Ignore notes from instances the user has muted
- if (isInstanceMuted(note, new Set<string>(this.userProfile!.mutedInstances)) && !this.following[note.userId]) return;
+ if (this.isNoteMutedOrBlocked(note)) return;
if (note.reply) {
const reply = note.reply;
@@ -90,17 +87,7 @@ class HybridTimelineChannel extends Channel {
if (note.user.isSilenced && !this.following[note.userId] && note.userId !== this.user!.id) return;
- if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
-
- // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
- // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
-
- if (note.renote && !note.text && note.renote.mentions?.some(mention => this.userIdsWhoMeMuting.has(mention))) return;
- if (note.mentions?.some(mention => this.userIdsWhoMeMuting.has(mention))) return;
-
- if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
+ if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return;
if (this.user && note.renoteId && !note.text) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts
index 71b5675402..48cc76c497 100644
--- a/packages/backend/src/server/api/stream/channels/local-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts
@@ -4,13 +4,12 @@
*/
import { Injectable } from '@nestjs/common';
-import { checkWordMute } from '@/misc/check-word-mute.js';
-import { isUserRelated } from '@/misc/is-user-related.js';
import type { Packed } from '@/misc/json-schema.js';
import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
+import { isQuotePacked, isRenotePacked } from '@/misc/is-renote.js';
import Channel, { type MiChannelService } from '../channel.js';
class LocalTimelineChannel extends Channel {
@@ -66,19 +65,11 @@ class LocalTimelineChannel extends Channel {
if (note.user.isSilenced && !this.following[note.userId] && note.userId !== this.user!.id) return;
- if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
+ if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return;
- // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
- // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
+ if (this.isNoteMutedOrBlocked(note)) return;
- if (note.renote && !note.text && note.renote.mentions?.some(mention => this.userIdsWhoMeMuting.has(mention))) return;
- if (note.mentions?.some(mention => this.userIdsWhoMeMuting.has(mention))) return;
-
- if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
-
- if (this.user && note.renoteId && !note.text) {
+ if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
diff --git a/packages/backend/src/server/api/stream/channels/role-timeline.ts b/packages/backend/src/server/api/stream/channels/role-timeline.ts
index 80aab4b35e..6a4ad22460 100644
--- a/packages/backend/src/server/api/stream/channels/role-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/role-timeline.ts
@@ -4,8 +4,6 @@
*/
import { Injectable } from '@nestjs/common';
-import { isUserRelated } from '@/misc/is-user-related.js';
-import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
@@ -46,12 +44,7 @@ class RoleTimelineChannel extends Channel {
}
if (note.visibility !== 'public') return;
- // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
- // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
-
- if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
+ if (this.isNoteMutedOrBlocked(note)) return;
this.send('note', note);
} else {
diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts
index f7bb106c03..14b30a157c 100644
--- a/packages/backend/src/server/api/stream/channels/user-list.ts
+++ b/packages/backend/src/server/api/stream/channels/user-list.ts
@@ -5,12 +5,11 @@
import { Inject, Injectable } from '@nestjs/common';
import type { MiUserListMembership, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
-import { isUserRelated } from '@/misc/is-user-related.js';
import type { Packed } from '@/misc/json-schema.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
-import { isInstanceMuted } from '@/misc/is-instance-muted.js';
+import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js';
import Channel, { type MiChannelService } from '../channel.js';
class UserListChannel extends Channel {
@@ -106,25 +105,17 @@ class UserListChannel extends Channel {
}
}
- if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
+ if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return;
- // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
- // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
- if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
+ if (this.isNoteMutedOrBlocked(note)) return;
- if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
-
- if (this.user && note.renoteId && !note.text) {
+ if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myRenoteReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
note.renote.myReaction = myRenoteReaction;
}
}
- // 流れてきたNoteがミュートしているインスタンスに関わるものだったら無視する
- if (isInstanceMuted(note, this.userMutedInstances)) return;
-
this.connection.cacheNote(note);
this.send('note', note);
diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts
index cb41c4f338..702e306feb 100644
--- a/packages/backend/src/server/web/ClientServerService.ts
+++ b/packages/backend/src/server/web/ClientServerService.ts
@@ -202,9 +202,18 @@ export class ClientServerService {
// Authenticate
fastify.addHook('onRequest', async (request, reply) => {
+ if (request.routeOptions.url == null) {
+ reply.code(404).send('Not found');
+ return;
+ }
+
// %71ueueとかでリクエストされたら困るため
const url = decodeURI(request.routeOptions.url);
if (url === bullBoardPath || url.startsWith(bullBoardPath + '/')) {
+ if (!url.startsWith(bullBoardPath + '/static/')) {
+ reply.header('Cache-Control', 'private, max-age=0, must-revalidate');
+ }
+
const token = request.cookies.token;
if (token == null) {
reply.code(401).send('Login required');
@@ -447,7 +456,7 @@ export class ClientServerService {
//#endregion
- const renderBase = async (reply: FastifyReply) => {
+ const renderBase = async (reply: FastifyReply, data: { [key: string]: any } = {}) => {
const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=30');
return await reply.view('base', {
@@ -456,6 +465,7 @@ export class ClientServerService {
title: meta.name ?? 'Misskey',
desc: meta.description,
...await this.generateCommonPugData(meta),
+ ...data,
});
};
@@ -474,7 +484,9 @@ export class ClientServerService {
};
// Atom
- fastify.get<{ Params: { user: string; } }>('/@:user.atom', async (request, reply) => {
+ fastify.get<{ Params: { user?: string; } }>('/@:user.atom', async (request, reply) => {
+ if (request.params.user == null) return await renderBase(reply);
+
const feed = await getFeed(request.params.user);
if (feed) {
@@ -487,7 +499,9 @@ export class ClientServerService {
});
// RSS
- fastify.get<{ Params: { user: string; } }>('/@:user.rss', async (request, reply) => {
+ fastify.get<{ Params: { user?: string; } }>('/@:user.rss', async (request, reply) => {
+ if (request.params.user == null) return await renderBase(reply);
+
const feed = await getFeed(request.params.user);
if (feed) {
@@ -500,7 +514,9 @@ export class ClientServerService {
});
// JSON
- fastify.get<{ Params: { user: string; } }>('/@:user.json', async (request, reply) => {
+ fastify.get<{ Params: { user?: string; } }>('/@:user.json', async (request, reply) => {
+ if (request.params.user == null) return await renderBase(reply);
+
const feed = await getFeed(request.params.user);
if (feed) {
@@ -753,6 +769,18 @@ export class ClientServerService {
});
//#endregion
+ //region noindex pages
+ // Tags
+ fastify.get<{ Params: { clip: string; } }>('/tags/:tag', async (request, reply) => {
+ return await renderBase(reply, { noindex: true });
+ });
+
+ // User with Tags
+ fastify.get<{ Params: { clip: string; } }>('/user-tags/:tag', async (request, reply) => {
+ return await renderBase(reply, { noindex: true });
+ });
+ //endregion
+
fastify.get('/_info_card_', async (request, reply) => {
const meta = await this.metaService.fetch(true);
diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts
index c6a96e94cb..8f8f08a305 100644
--- a/packages/backend/src/server/web/UrlPreviewService.ts
+++ b/packages/backend/src/server/web/UrlPreviewService.ts
@@ -5,6 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { summaly } from '@misskey-dev/summaly';
+import { SummalyResult } from '@misskey-dev/summaly/built/summary.js';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { MetaService } from '@/core/MetaService.js';
@@ -14,6 +15,7 @@ import { query } from '@/misc/prelude/url.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import { ApiError } from '@/server/api/error.js';
+import { MiMeta } from '@/models/Meta.js';
import type { FastifyRequest, FastifyReply } from 'fastify';
@Injectable()
@@ -62,24 +64,25 @@ export class UrlPreviewService {
const meta = await this.metaService.fetch();
- this.logger.info(meta.summalyProxy
+ if (!meta.urlPreviewEnabled) {
+ reply.code(403);
+ return {
+ error: new ApiError({
+ message: 'URL preview is disabled',
+ code: 'URL_PREVIEW_DISABLED',
+ id: '58b36e13-d2f5-0323-b0c6-76aa9dabefb8',
+ }),
+ };
+ }
+
+ this.logger.info(meta.urlPreviewSummaryProxyUrl
? `(Proxy) Getting preview of ${url}@${lang} ...`
: `Getting preview of ${url}@${lang} ...`);
+
try {
- const summary = meta.summalyProxy ?
- await this.httpRequestService.getJson<ReturnType<typeof summaly>>(`${meta.summalyProxy}?${query({
- url: url,
- lang: lang ?? 'ja-JP',
- })}`)
- :
- await summaly(url, {
- followRedirects: false,
- lang: lang ?? 'ja-JP',
- agent: this.config.proxy ? {
- http: this.httpRequestService.httpAgent,
- https: this.httpRequestService.httpsAgent,
- } : undefined,
- });
+ const summary = meta.urlPreviewSummaryProxyUrl
+ ? await this.fetchSummaryFromProxy(url, meta, lang)
+ : await this.fetchSummary(url, meta, lang);
this.logger.succ(`Got preview of ${url}: ${summary.title}`);
@@ -100,6 +103,7 @@ export class UrlPreviewService {
return summary;
} catch (err) {
this.logger.warn(`Failed to get preview of ${url}: ${err}`);
+
reply.code(422);
reply.header('Cache-Control', 'max-age=86400, immutable');
return {
@@ -111,4 +115,37 @@ export class UrlPreviewService {
};
}
}
+
+ private fetchSummary(url: string, meta: MiMeta, lang?: string): Promise<SummalyResult> {
+ const agent = this.config.proxy
+ ? {
+ http: this.httpRequestService.httpAgent,
+ https: this.httpRequestService.httpsAgent,
+ }
+ : undefined;
+
+ return summaly(url, {
+ followRedirects: false,
+ lang: lang ?? 'ja-JP',
+ agent: agent,
+ userAgent: meta.urlPreviewUserAgent ?? undefined,
+ operationTimeout: meta.urlPreviewTimeout,
+ contentLengthLimit: meta.urlPreviewMaximumContentLength,
+ contentLengthRequired: meta.urlPreviewRequireContentLength,
+ });
+ }
+
+ private fetchSummaryFromProxy(url: string, meta: MiMeta, lang?: string): Promise<SummalyResult> {
+ const proxy = meta.urlPreviewSummaryProxyUrl!;
+ const queryStr = query({
+ url: url,
+ lang: lang ?? 'ja-JP',
+ userAgent: meta.urlPreviewUserAgent ?? undefined,
+ operationTimeout: meta.urlPreviewTimeout,
+ contentLengthLimit: meta.urlPreviewMaximumContentLength,
+ contentLengthRequired: meta.urlPreviewRequireContentLength,
+ });
+
+ return this.httpRequestService.getJson<SummalyResult>(`${proxy}?${queryStr}`);
+ }
}
diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js
index 6b9dc0a103..0543cc2b65 100644
--- a/packages/backend/src/server/web/boot.js
+++ b/packages/backend/src/server/web/boot.js
@@ -86,8 +86,8 @@
//#endregion
//#region Script
- function importAppScript() {
- import(`/vite/${CLIENT_ENTRY}`)
+ async function importAppScript() {
+ await import(`/vite/${CLIENT_ENTRY}`)
.catch(async e => {
console.error(e);
renderError('APP_IMPORT', e);
diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug
index 0f555e0637..4007632291 100644
--- a/packages/backend/src/server/web/views/base.pug
+++ b/packages/backend/src/server/web/views/base.pug
@@ -40,8 +40,8 @@ html
link(rel='prefetch' href=infoImageUrl)
link(rel='prefetch' href=notFoundImageUrl)
//- https://github.com/misskey-dev/misskey/issues/9842
- link(rel='stylesheet' href='/assets/phosphor-icons/bold/style.css')
- link(rel='stylesheet' href='/static-assets/fonts/sharkey-icons/style.css')
+ link(rel='stylesheet' href=`/assets/phosphor-icons/bold/style.css?version=${version}`)
+ link(rel='stylesheet' href=`/static-assets/fonts/sharkey-icons/style.css?version=${version}`)
link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
if !config.clientManifestExists
@@ -55,6 +55,9 @@ html
block title
= title || 'Sharkey'
+ if noindex
+ meta(name='robots' content='noindex')
+
block desc
meta(name='description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨')
diff --git a/packages/backend/src/server/web/views/note.pug b/packages/backend/src/server/web/views/note.pug
index 47c89ae2d7..a7cd0293cd 100644
--- a/packages/backend/src/server/web/views/note.pug
+++ b/packages/backend/src/server/web/views/note.pug
@@ -2,7 +2,7 @@ extends ./base
block vars
- const user = note.user;
- - const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`;
+ - const title = user.name ? `${user.name} (@${user.username}${user.host ? `@${user.host}` : ''})` : `@${user.username}${user.host ? `@${user.host}` : ''}`;
- const url = `${config.url}/notes/${note.id}`;
- const isRenote = note.renote && note.text == null && note.fileIds.length == 0 && note.poll == null;
- const images = note.cw ? [] : (note.files || []).filter(file => file.type.startsWith('image/') && !file.isSensitive)
@@ -28,7 +28,7 @@ block og
// FIXME: add embed player for Twitter
if images.length
meta(property='twitter:card' content='summary_large_image')
- each image in images
+ each image in images
meta(property='og:image' content= image.url)
else
meta(property='twitter:card' content='summary')
diff --git a/packages/backend/src/server/web/views/page.pug b/packages/backend/src/server/web/views/page.pug
index 08bb08ffe7..03c50eca8a 100644
--- a/packages/backend/src/server/web/views/page.pug
+++ b/packages/backend/src/server/web/views/page.pug
@@ -3,7 +3,7 @@ extends ./base
block vars
- const user = page.user;
- const title = page.title;
- - const url = `${config.url}/@${user.username}/${page.name}`;
+ - const url = `${config.url}/@${user.username}/pages/${page.name}`;
block title
= `${title} | ${instanceName}`
diff --git a/packages/backend/src/server/web/views/user.pug b/packages/backend/src/server/web/views/user.pug
index 83d57349a6..2b0a7bab5c 100644
--- a/packages/backend/src/server/web/views/user.pug
+++ b/packages/backend/src/server/web/views/user.pug
@@ -1,7 +1,7 @@
extends ./base
block vars
- - const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`;
+ - const title = user.name ? `${user.name} (@${user.username}${user.host ? `@${user.host}` : ''})` : `@${user.username}${user.host ? `@${user.host}` : ''}`;
- const url = `${config.url}/@${(user.host ? `${user.username}@${user.host}` : user.username)}`;
block title