Merge remote-tracking branch 'refs/remotes/github-prismisskey/develop' into develop

# Conflicts:
#	locales/index.d.ts
#	packages/frontend/src/components/MkPostForm.vue
#	pnpm-lock.yaml
This commit is contained in:
mattyatea 2024-05-25 01:18:55 +09:00
commit 6445591350
99 changed files with 1734 additions and 417 deletions

View file

@ -15,6 +15,7 @@ import Logger from '@/logger.js';
import { envOption } from '../env.js';
import { masterMain } from './master.js';
import { workerMain } from './worker.js';
import { readyRef } from './ready.js';
import 'reflect-metadata';
@ -79,6 +80,8 @@ if (cluster.isWorker || envOption.disableClustering) {
await workerMain();
}
readyRef.value = true;
// ユニットテスト時にMisskeyが子プロセスで起動された時のため
// それ以外のときは process.send は使えないので弾く
if (process.send) {

View file

@ -0,0 +1,6 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export const readyRef = { value: false };

View file

@ -61,8 +61,8 @@ export class FanoutTimelineEndpointService {
// 呼び出し元と以下の処理をシンプルにするためにdbFallbackを置き換える
if (!ps.useDbFallback) ps.dbFallback = () => Promise.resolve([]);
const shouldPrepend = ps.sinceId && !ps.untilId;
const idCompare: (a: string, b: string) => number = shouldPrepend ? (a, b) => a < b ? -1 : 1 : (a, b) => a > b ? -1 : 1;
const ascending = ps.sinceId && !ps.untilId;
const idCompare: (a: string, b: string) => number = ascending ? (a, b) => a < b ? -1 : 1 : (a, b) => a > b ? -1 : 1;
const redisResult = await this.fanoutTimelineService.getMulti(ps.redisTimelines, ps.untilId, ps.sinceId);
@ -143,9 +143,7 @@ export class FanoutTimelineEndpointService {
if (ps.allowPartial ? redisTimeline.length !== 0 : redisTimeline.length >= ps.limit) {
// 十分Redisからとれた
const result = redisTimeline.slice(0, ps.limit);
if (shouldPrepend) result.reverse();
return result;
return redisTimeline.slice(0, ps.limit);
}
}
@ -153,8 +151,7 @@ export class FanoutTimelineEndpointService {
const remainingToRead = ps.limit - redisTimeline.length;
let dbUntil: string | null;
let dbSince: string | null;
if (shouldPrepend) {
redisTimeline.reverse();
if (ascending) {
dbUntil = ps.untilId;
dbSince = noteIds[noteIds.length - 1];
} else {
@ -162,7 +159,7 @@ export class FanoutTimelineEndpointService {
dbSince = ps.sinceId;
}
const gotFromDb = await ps.dbFallback(dbUntil, dbSince, remainingToRead);
return shouldPrepend ? [...gotFromDb, ...redisTimeline] : [...redisTimeline, ...gotFromDb];
return [...redisTimeline, ...gotFromDb];
}
return await ps.dbFallback(ps.untilId, ps.sinceId, ps.limit);

View file

@ -520,6 +520,7 @@ export class NoteCreateService implements OnApplicationShutdown {
noteVisibility: insert.visibility,
userId: user.id,
userHost: user.host,
channelId: insert.channelId,
});
await transactionalEntityManager.insert(MiPoll, poll);

View file

@ -39,7 +39,8 @@ export class InstanceEntityService {
followingCount: instance.followingCount,
followersCount: instance.followersCount,
isNotResponding: instance.isNotResponding,
isSuspended: instance.isSuspended,
isSuspended: instance.suspensionState !== 'none',
suspensionState: instance.suspensionState,
isBlocked: this.utilityService.isBlockedHost(meta.blockedHosts, instance.host),
softwareName: instance.softwareName,
softwareVersion: instance.softwareVersion,

View file

@ -230,7 +230,7 @@ export type SchemaTypeDef<p extends Schema> =
p['items']['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<NonNullable<p['items']['allOf']>>>[] :
never
) :
p['items'] extends NonNullable<Schema> ? SchemaTypeDef<p['items']>[] :
p['items'] extends NonNullable<Schema> ? SchemaType<p['items']>[] :
any[]
) :
p['anyOf'] extends ReadonlyArray<Schema> ? UnionSchemaType<p['anyOf']> & PartialIntersection<UnionSchemaType<p['anyOf']>> :

View file

@ -81,13 +81,22 @@ export class MiInstance {
public isNotResponding: boolean;
/**
*
*
*/
@Column('timestamp with time zone', {
nullable: true,
})
public notRespondingSince: Date | null;
/**
*
*/
@Index()
@Column('boolean', {
default: false,
@Column('enum', {
default: 'none',
enum: ['none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding'],
})
public isSuspended: boolean;
public suspensionState: 'none' | 'manuallySuspended' | 'goneSuspended' | 'autoSuspendedForNotResponding';
@Column('varchar', {
length: 64, nullable: true,

View file

@ -8,6 +8,7 @@ import { noteVisibilities } from '@/types.js';
import { id } from './util/id.js';
import { MiNote } from './Note.js';
import type { MiUser } from './User.js';
import type { MiChannel } from "@/models/Channel.js";
@Entity('poll')
export class MiPoll {
@ -58,6 +59,14 @@ export class MiPoll {
comment: '[Denormalized]',
})
public userHost: string | null;
@Index()
@Column({
...id(),
nullable: true,
comment: '[Denormalized]',
})
public channelId: MiChannel['id'] | null;
//#endregion
constructor(data: Partial<MiPoll>) {

View file

@ -45,6 +45,11 @@ export const packedFederationInstanceSchema = {
type: 'boolean',
optional: false, nullable: false,
},
suspensionState: {
type: 'string',
nullable: false, optional: false,
enum: ['none', 'manuallySuspended', 'goneSuspended', 'autoSuspendedForNotResponding'],
},
isBlocked: {
type: 'boolean',
optional: false, nullable: false,

View file

@ -5,6 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import * as Bull from 'bullmq';
import { Not } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { InstancesRepository } from '@/models/_.js';
import type Logger from '@/logger.js';
@ -62,7 +63,7 @@ export class DeliverProcessorService {
if (suspendedHosts == null) {
suspendedHosts = await this.instancesRepository.find({
where: {
isSuspended: true,
suspensionState: Not('none'),
},
});
this.suspendedHostsCache.set(suspendedHosts);
@ -79,6 +80,7 @@ export class DeliverProcessorService {
if (i.isNotResponding) {
this.federatedInstanceService.update(i.id, {
isNotResponding: false,
notRespondingSince: null,
});
}
@ -98,7 +100,15 @@ export class DeliverProcessorService {
if (!i.isNotResponding) {
this.federatedInstanceService.update(i.id, {
isNotResponding: true,
notRespondingSince: new Date(),
});
} else if (i.notRespondingSince) {
// 1週間以上不通ならサスペンド
if (i.suspensionState === 'none' && i.notRespondingSince.getTime() <= Date.now() - 1000 * 60 * 60 * 24 * 7) {
this.federatedInstanceService.update(i.id, {
suspensionState: 'autoSuspendedForNotResponding',
});
}
}
this.apRequestChart.deliverFail();
@ -116,7 +126,7 @@ export class DeliverProcessorService {
if (job.data.isSharedInbox && res.statusCode === 410) {
this.federatedInstanceService.fetch(host).then(i => {
this.federatedInstanceService.update(i.id, {
isSuspended: true,
suspensionState: 'goneSuspended',
});
});
throw new Bull.UnrecoverableError(`${host} is gone`);

View file

@ -188,6 +188,8 @@ export class InboxProcessorService {
this.federatedInstanceService.update(i.id, {
latestRequestReceivedAt: new Date(),
isNotResponding: false,
// もしサーバーが死んでるために配信が止まっていた場合には自動的に復活させてあげる
suspensionState: i.suspensionState === 'autoSuspendedForNotResponding' ? 'none' : undefined,
});
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);

View file

@ -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();
}
}

View file

@ -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';
@ -55,6 +56,7 @@ import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js
ClientServerService,
ClientLoggerService,
FeedService,
HealthServerService,
UrlPreviewService,
ActivityPubServerService,
FileServerService,

View file

@ -28,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 { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
@ -61,6 +62,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,
@ -108,6 +110,7 @@ export class ServerService implements OnApplicationShutdown {
fastify.register(this.wellKnownServerService.createServer);
fastify.register(this.oauth2ProviderService.createServer, { prefix: '/oauth' });
fastify.register(this.oauth2ProviderService.createTokenServer, { prefix: '/oauth/token' });
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;

View file

@ -137,7 +137,7 @@ export class ApiServerService {
const instances = await this.instancesRepository.find({
select: ['host'],
where: {
isSuspended: false,
suspensionState: 'none',
},
});

View file

@ -46,12 +46,19 @@ 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,
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,

View file

@ -16,7 +16,7 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'read:admin:show-users',
kind: 'read:admin:show-user',
res: {
type: 'array',

View file

@ -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

View file

@ -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)

View file

@ -110,9 +110,11 @@ 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, {

View file

@ -436,7 +436,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', {
@ -445,6 +445,7 @@ export class ClientServerService {
title: meta.name ?? 'Misskey',
desc: meta.description,
...await this.generateCommonPugData(meta),
...data,
});
};
@ -742,6 +743,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);

View file

@ -50,6 +50,9 @@ html
block title
= title || 'Misskey'
if noindex
meta(name='robots' content='noindex')
block desc
meta(name='description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨')