Merge tag '13.13.2' into merge-upstream
This commit is contained in:
@@ -585,7 +585,7 @@ export class ActivityPubServerService {
|
||||
name: request.params.emoji,
|
||||
});
|
||||
|
||||
if (emoji == null) {
|
||||
if (emoji == null || emoji.localOnly) {
|
||||
reply.code(404);
|
||||
return;
|
||||
}
|
||||
|
@@ -201,7 +201,7 @@ export class ServerService implements OnApplicationShutdown {
|
||||
|
||||
fastify.register(this.clientServerService.createServer);
|
||||
|
||||
this.streamingApiServerService.attachStreamingApi(fastify.server);
|
||||
this.streamingApiServerService.attach(fastify.server);
|
||||
|
||||
fastify.server.on('error', err => {
|
||||
switch ((err as any).code) {
|
||||
@@ -229,7 +229,14 @@ export class ServerService implements OnApplicationShutdown {
|
||||
await fastify.ready();
|
||||
}
|
||||
|
||||
async onApplicationShutdown(signal: string): Promise<void> {
|
||||
@bindThis
|
||||
public async dispose(): Promise<void> {
|
||||
await this.streamingApiServerService.detach();
|
||||
await this.#fastify.close();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
async onApplicationShutdown(signal: string): Promise<void> {
|
||||
await this.dispose();
|
||||
}
|
||||
}
|
||||
|
@@ -359,7 +359,12 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined) {
|
||||
public dispose(): void {
|
||||
clearInterval(this.userIpHistoriesClearIntervalId);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { AccessTokensRepository, AppsRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { LocalUser } from '@/models/entities/User.js';
|
||||
@@ -17,7 +17,7 @@ export class AuthenticationError extends Error {
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuthenticateService {
|
||||
export class AuthenticateService implements OnApplicationShutdown {
|
||||
private appCache: MemoryKVCache<App>;
|
||||
|
||||
constructor(
|
||||
@@ -36,7 +36,7 @@ export class AuthenticateService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async authenticate(token: string | null | undefined): Promise<[LocalUser | null | undefined, AccessToken | null | undefined]> {
|
||||
public async authenticate(token: string | null | undefined): Promise<[LocalUser | null, AccessToken | null]> {
|
||||
if (token == null) {
|
||||
return [null, null];
|
||||
}
|
||||
@@ -85,4 +85,14 @@ export class AuthenticateService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.appCache.dispose();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
@@ -321,6 +321,9 @@ import * as ep___users_lists_pull from './endpoints/users/lists/pull.js';
|
||||
import * as ep___users_lists_push from './endpoints/users/lists/push.js';
|
||||
import * as ep___users_lists_show from './endpoints/users/lists/show.js';
|
||||
import * as ep___users_lists_update from './endpoints/users/lists/update.js';
|
||||
import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js';
|
||||
import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js';
|
||||
import * as ep___users_lists_create_from_public from './endpoints/users/lists/create-from-public.js';
|
||||
import * as ep___users_notes from './endpoints/users/notes.js';
|
||||
import * as ep___users_pages from './endpoints/users/pages.js';
|
||||
import * as ep___users_reactions from './endpoints/users/reactions.js';
|
||||
@@ -330,7 +333,6 @@ import * as ep___users_reportAbuse from './endpoints/users/report-abuse.js';
|
||||
import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by-username-and-host.js';
|
||||
import * as ep___users_search from './endpoints/users/search.js';
|
||||
import * as ep___users_show from './endpoints/users/show.js';
|
||||
import * as ep___users_stats from './endpoints/users/stats.js';
|
||||
import * as ep___users_achievements from './endpoints/users/achievements.js';
|
||||
import * as ep___users_updateMemo from './endpoints/users/update-memo.js';
|
||||
import * as ep___fetchRss from './endpoints/fetch-rss.js';
|
||||
@@ -659,6 +661,9 @@ const $users_lists_pull: Provider = { provide: 'ep:users/lists/pull', useClass:
|
||||
const $users_lists_push: Provider = { provide: 'ep:users/lists/push', useClass: ep___users_lists_push.default };
|
||||
const $users_lists_show: Provider = { provide: 'ep:users/lists/show', useClass: ep___users_lists_show.default };
|
||||
const $users_lists_update: Provider = { provide: 'ep:users/lists/update', useClass: ep___users_lists_update.default };
|
||||
const $users_lists_favorite: Provider = { provide: 'ep:users/lists/favorite', useClass: ep___users_lists_favorite.default };
|
||||
const $users_lists_unfavorite: Provider = { provide: 'ep:users/lists/unfavorite', useClass: ep___users_lists_unfavorite.default };
|
||||
const $users_lists_create_from_public: Provider = { provide: 'ep:users/lists/create-from-public', useClass: ep___users_lists_create_from_public.default };
|
||||
const $users_notes: Provider = { provide: 'ep:users/notes', useClass: ep___users_notes.default };
|
||||
const $users_pages: Provider = { provide: 'ep:users/pages', useClass: ep___users_pages.default };
|
||||
const $users_reactions: Provider = { provide: 'ep:users/reactions', useClass: ep___users_reactions.default };
|
||||
@@ -668,7 +673,6 @@ const $users_reportAbuse: Provider = { provide: 'ep:users/report-abuse', useClas
|
||||
const $users_searchByUsernameAndHost: Provider = { provide: 'ep:users/search-by-username-and-host', useClass: ep___users_searchByUsernameAndHost.default };
|
||||
const $users_search: Provider = { provide: 'ep:users/search', useClass: ep___users_search.default };
|
||||
const $users_show: Provider = { provide: 'ep:users/show', useClass: ep___users_show.default };
|
||||
const $users_stats: Provider = { provide: 'ep:users/stats', useClass: ep___users_stats.default };
|
||||
const $users_achievements: Provider = { provide: 'ep:users/achievements', useClass: ep___users_achievements.default };
|
||||
const $users_updateMemo: Provider = { provide: 'ep:users/update-memo', useClass: ep___users_updateMemo.default };
|
||||
const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default };
|
||||
@@ -1001,6 +1005,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$users_lists_push,
|
||||
$users_lists_show,
|
||||
$users_lists_update,
|
||||
$users_lists_favorite,
|
||||
$users_lists_unfavorite,
|
||||
$users_lists_create_from_public,
|
||||
$users_notes,
|
||||
$users_pages,
|
||||
$users_reactions,
|
||||
@@ -1010,7 +1017,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$users_searchByUsernameAndHost,
|
||||
$users_search,
|
||||
$users_show,
|
||||
$users_stats,
|
||||
$users_achievements,
|
||||
$users_updateMemo,
|
||||
$fetchRss,
|
||||
@@ -1335,6 +1341,9 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$users_lists_push,
|
||||
$users_lists_show,
|
||||
$users_lists_update,
|
||||
$users_lists_favorite,
|
||||
$users_lists_unfavorite,
|
||||
$users_lists_create_from_public,
|
||||
$users_notes,
|
||||
$users_pages,
|
||||
$users_reactions,
|
||||
@@ -1344,7 +1353,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$users_searchByUsernameAndHost,
|
||||
$users_search,
|
||||
$users_show,
|
||||
$users_stats,
|
||||
$users_achievements,
|
||||
$users_updateMemo,
|
||||
$fetchRss,
|
||||
|
@@ -1,23 +1,27 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import * as websocket from 'websocket';
|
||||
import * as WebSocket from 'ws';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { UsersRepository, BlockingsRepository, ChannelFollowingsRepository, FollowingsRepository, MutingsRepository, UserProfilesRepository, RenoteMutingsRepository } from '@/models/index.js';
|
||||
import type { UsersRepository, AccessToken } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { NoteReadService } from '@/core/NoteReadService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { NotificationService } from '@/core/NotificationService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { AuthenticateService } from './AuthenticateService.js';
|
||||
import { LocalUser } from '@/models/entities/User';
|
||||
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
|
||||
import MainStreamConnection from './stream/index.js';
|
||||
import { ChannelsService } from './stream/ChannelsService.js';
|
||||
import type { ParsedUrlQuery } from 'querystring';
|
||||
import type * as http from 'node:http';
|
||||
|
||||
@Injectable()
|
||||
export class StreamingApiServerService {
|
||||
#wss: WebSocket.WebSocketServer;
|
||||
#connections = new Map<WebSocket.WebSocket, number>();
|
||||
#cleanConnectionsIntervalId: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
@@ -28,24 +32,6 @@ export class StreamingApiServerService {
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
@Inject(DI.mutingsRepository)
|
||||
private mutingsRepository: MutingsRepository,
|
||||
|
||||
@Inject(DI.renoteMutingsRepository)
|
||||
private renoteMutingsRepository: RenoteMutingsRepository,
|
||||
|
||||
@Inject(DI.blockingsRepository)
|
||||
private blockingsRepository: BlockingsRepository,
|
||||
|
||||
@Inject(DI.channelFollowingsRepository)
|
||||
private channelFollowingsRepository: ChannelFollowingsRepository,
|
||||
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
private cacheService: CacheService,
|
||||
private noteReadService: NoteReadService,
|
||||
private authenticateService: AuthenticateService,
|
||||
@@ -55,25 +41,65 @@ export class StreamingApiServerService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public attachStreamingApi(server: http.Server) {
|
||||
// Init websocket server
|
||||
const ws = new websocket.server({
|
||||
httpServer: server,
|
||||
public attach(server: http.Server): void {
|
||||
this.#wss = new WebSocket.WebSocketServer({
|
||||
noServer: true,
|
||||
});
|
||||
|
||||
ws.on('request', async (request) => {
|
||||
const q = request.resourceURL.query as ParsedUrlQuery;
|
||||
|
||||
// TODO: トークンが間違ってるなどしてauthenticateに失敗したら
|
||||
// コネクション切断するなりエラーメッセージ返すなりする
|
||||
// (現状はエラーがキャッチされておらずサーバーのログに流れて邪魔なので)
|
||||
const [user, miapp] = await this.authenticateService.authenticate(q.i as string);
|
||||
|
||||
if (user?.isSuspended) {
|
||||
request.reject(400);
|
||||
server.on('upgrade', async (request, socket, head) => {
|
||||
if (request.url == null) {
|
||||
socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
const q = new URL(request.url, `http://${request.headers.host}`).searchParams;
|
||||
|
||||
let user: LocalUser | null = null;
|
||||
let app: AccessToken | null = null;
|
||||
|
||||
try {
|
||||
[user, app] = await this.authenticateService.authenticate(q.get('i'));
|
||||
} catch (e) {
|
||||
if (e instanceof AuthenticationError) {
|
||||
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
||||
} else {
|
||||
socket.write('HTTP/1.1 500 Internal Server Error\r\n\r\n');
|
||||
}
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
if (user?.isSuspended) {
|
||||
socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
const stream = new MainStreamConnection(
|
||||
this.channelsService,
|
||||
this.noteReadService,
|
||||
this.notificationService,
|
||||
this.cacheService,
|
||||
user, app,
|
||||
);
|
||||
|
||||
await stream.init();
|
||||
|
||||
this.#wss.handleUpgrade(request, socket, head, (ws) => {
|
||||
this.#wss.emit('connection', ws, request, {
|
||||
stream, user, app,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
this.#wss.on('connection', async (connection: WebSocket.WebSocket, request: http.IncomingMessage, ctx: {
|
||||
stream: MainStreamConnection,
|
||||
user: LocalUser | null;
|
||||
app: AccessToken | null
|
||||
}) => {
|
||||
const { stream, user, app } = ctx;
|
||||
|
||||
const ev = new EventEmitter();
|
||||
|
||||
async function onRedisMessage(_: string, data: string): Promise<void> {
|
||||
@@ -83,21 +109,11 @@ export class StreamingApiServerService {
|
||||
|
||||
this.redisForSub.on('message', onRedisMessage);
|
||||
|
||||
const main = new MainStreamConnection(
|
||||
this.channelsService,
|
||||
this.noteReadService,
|
||||
this.notificationService,
|
||||
this.cacheService,
|
||||
ev, user, miapp,
|
||||
);
|
||||
await stream.listen(ev, connection);
|
||||
|
||||
await main.init();
|
||||
this.#connections.set(connection, Date.now());
|
||||
|
||||
const connection = request.accept();
|
||||
|
||||
main.init2(connection);
|
||||
|
||||
const intervalId = user ? setInterval(() => {
|
||||
const userUpdateIntervalId = user ? setInterval(() => {
|
||||
this.usersRepository.update(user.id, {
|
||||
lastActiveDate: new Date(),
|
||||
});
|
||||
@@ -110,16 +126,39 @@ export class StreamingApiServerService {
|
||||
|
||||
connection.once('close', () => {
|
||||
ev.removeAllListeners();
|
||||
main.dispose();
|
||||
stream.dispose();
|
||||
this.redisForSub.off('message', onRedisMessage);
|
||||
if (intervalId) clearInterval(intervalId);
|
||||
this.#connections.delete(connection);
|
||||
if (userUpdateIntervalId) clearInterval(userUpdateIntervalId);
|
||||
});
|
||||
|
||||
connection.on('message', async (data) => {
|
||||
if (data.type === 'utf8' && data.utf8Data === 'ping') {
|
||||
connection.send('pong');
|
||||
}
|
||||
connection.on('pong', () => {
|
||||
this.#connections.set(connection, Date.now());
|
||||
});
|
||||
});
|
||||
|
||||
// 一定期間通信が無いコネクションは実際には切断されている可能性があるため定期的にterminateする
|
||||
this.#cleanConnectionsIntervalId = setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [connection, lastActive] of this.#connections.entries()) {
|
||||
if (now - lastActive > 1000 * 60 * 2) {
|
||||
connection.terminate();
|
||||
this.#connections.delete(connection);
|
||||
} else {
|
||||
connection.ping();
|
||||
}
|
||||
}
|
||||
}, 1000 * 60);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public detach(): Promise<void> {
|
||||
if (this.#cleanConnectionsIntervalId) {
|
||||
clearInterval(this.#cleanConnectionsIntervalId);
|
||||
this.#cleanConnectionsIntervalId = null;
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
this.#wss.close(() => resolve());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -320,6 +320,9 @@ import * as ep___users_lists_list from './endpoints/users/lists/list.js';
|
||||
import * as ep___users_lists_pull from './endpoints/users/lists/pull.js';
|
||||
import * as ep___users_lists_push from './endpoints/users/lists/push.js';
|
||||
import * as ep___users_lists_show from './endpoints/users/lists/show.js';
|
||||
import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js';
|
||||
import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js';
|
||||
import * as ep___users_lists_create_from_public from './endpoints/users/lists/create-from-public.js';
|
||||
import * as ep___users_lists_update from './endpoints/users/lists/update.js';
|
||||
import * as ep___users_notes from './endpoints/users/notes.js';
|
||||
import * as ep___users_pages from './endpoints/users/pages.js';
|
||||
@@ -330,7 +333,6 @@ import * as ep___users_reportAbuse from './endpoints/users/report-abuse.js';
|
||||
import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by-username-and-host.js';
|
||||
import * as ep___users_search from './endpoints/users/search.js';
|
||||
import * as ep___users_show from './endpoints/users/show.js';
|
||||
import * as ep___users_stats from './endpoints/users/stats.js';
|
||||
import * as ep___users_achievements from './endpoints/users/achievements.js';
|
||||
import * as ep___users_updateMemo from './endpoints/users/update-memo.js';
|
||||
import * as ep___fetchRss from './endpoints/fetch-rss.js';
|
||||
@@ -656,7 +658,10 @@ const eps = [
|
||||
['users/lists/pull', ep___users_lists_pull],
|
||||
['users/lists/push', ep___users_lists_push],
|
||||
['users/lists/show', ep___users_lists_show],
|
||||
['users/lists/favorite', ep___users_lists_favorite],
|
||||
['users/lists/unfavorite', ep___users_lists_unfavorite],
|
||||
['users/lists/update', ep___users_lists_update],
|
||||
['users/lists/create-from-public', ep___users_lists_create_from_public],
|
||||
['users/notes', ep___users_notes],
|
||||
['users/pages', ep___users_pages],
|
||||
['users/reactions', ep___users_reactions],
|
||||
@@ -666,7 +671,6 @@ const eps = [
|
||||
['users/search-by-username-and-host', ep___users_searchByUsernameAndHost],
|
||||
['users/search', ep___users_search],
|
||||
['users/show', ep___users_show],
|
||||
['users/stats', ep___users_stats],
|
||||
['users/achievements', ep___users_achievements],
|
||||
['users/update-memo', ep___users_updateMemo],
|
||||
['fetch-rss', ep___fetchRss],
|
||||
|
@@ -25,7 +25,7 @@ export const paramDef = {
|
||||
id: { type: 'string', format: 'misskey:id' },
|
||||
title: { type: 'string', minLength: 1 },
|
||||
text: { type: 'string', minLength: 1 },
|
||||
imageUrl: { type: 'string', nullable: true, minLength: 1 },
|
||||
imageUrl: { type: 'string', nullable: true, minLength: 0 },
|
||||
},
|
||||
required: ['id', 'title', 'text', 'imageUrl'],
|
||||
} as const;
|
||||
@@ -46,7 +46,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
updatedAt: new Date(),
|
||||
title: ps.title,
|
||||
text: ps.text,
|
||||
imageUrl: ps.imageUrl,
|
||||
/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- 空の文字列の場合、nullを渡すようにするため */
|
||||
imageUrl: ps.imageUrl || null,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@@ -25,9 +25,24 @@ export const meta = {
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' },
|
||||
fileId: { type: 'string', format: 'misskey:id' },
|
||||
category: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
description: 'Use `null` to reset the category.',
|
||||
},
|
||||
aliases: { type: 'array', items: {
|
||||
type: 'string',
|
||||
} },
|
||||
license: { type: 'string', nullable: true },
|
||||
isSensitive: { type: 'boolean' },
|
||||
localOnly: { type: 'boolean' },
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: {
|
||||
type: 'string',
|
||||
} },
|
||||
},
|
||||
required: ['fileId'],
|
||||
required: ['name', 'fileId'],
|
||||
} as const;
|
||||
|
||||
// TODO: ロジックをサービスに切り出す
|
||||
@@ -45,18 +60,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
||||
|
||||
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
|
||||
|
||||
const name = driveFile.name.split('.')[0].match(/^[a-z0-9_]+$/) ? driveFile.name.split('.')[0] : `_${rndstr('a-z0-9', 8)}_`;
|
||||
|
||||
const emoji = await this.customEmojiService.add({
|
||||
driveFile,
|
||||
name,
|
||||
category: null,
|
||||
aliases: [],
|
||||
name: ps.name,
|
||||
category: ps.category ?? null,
|
||||
aliases: ps.aliases ?? [],
|
||||
host: null,
|
||||
license: null,
|
||||
license: ps.license ?? null,
|
||||
isSensitive: ps.isSensitive ?? false,
|
||||
localOnly: ps.localOnly ?? false,
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [],
|
||||
});
|
||||
|
||||
this.moderationLogService.insertModerationLog(me, 'addEmoji', {
|
||||
|
@@ -1,6 +1,8 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import type { DriveFilesRepository } from '@/models/index.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -15,6 +17,11 @@ export const meta = {
|
||||
code: 'NO_SUCH_EMOJI',
|
||||
id: '684dec9d-a8c2-4364-9aa8-456c49cb1dc8',
|
||||
},
|
||||
noSuchFile: {
|
||||
message: 'No such file.',
|
||||
code: 'NO_SUCH_FILE',
|
||||
id: '14fb9fd9-0731-4e2f-aeb9-f09e4740333d',
|
||||
},
|
||||
sameNameEmojiExists: {
|
||||
message: 'Emoji that have same name already exists.',
|
||||
code: 'SAME_NAME_EMOJI_EXISTS',
|
||||
@@ -28,6 +35,7 @@ export const paramDef = {
|
||||
properties: {
|
||||
id: { type: 'string', format: 'misskey:id' },
|
||||
name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' },
|
||||
fileId: { type: 'string', format: 'misskey:id' },
|
||||
category: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
@@ -37,6 +45,11 @@ export const paramDef = {
|
||||
type: 'string',
|
||||
} },
|
||||
license: { type: 'string', nullable: true },
|
||||
isSensitive: { type: 'boolean' },
|
||||
localOnly: { type: 'boolean' },
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: {
|
||||
type: 'string',
|
||||
} },
|
||||
},
|
||||
required: ['id', 'name', 'aliases'],
|
||||
} as const;
|
||||
@@ -45,14 +58,28 @@ export const paramDef = {
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
private customEmojiService: CustomEmojiService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
let driveFile;
|
||||
|
||||
if (ps.fileId) {
|
||||
driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
||||
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
|
||||
}
|
||||
|
||||
await this.customEmojiService.update(ps.id, {
|
||||
driveFile,
|
||||
name: ps.name,
|
||||
category: ps.category ?? null,
|
||||
aliases: ps.aliases,
|
||||
license: ps.license ?? null,
|
||||
isSensitive: ps.isSensitive,
|
||||
localOnly: ps.localOnly,
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@@ -61,10 +61,17 @@ export const meta = {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
errorImageUrl: {
|
||||
serverErrorImageUrl: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
infoImageUrl: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
notFoundImageUrl: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
default: 'https://xn--931a.moe/aiart/yubitun.png',
|
||||
},
|
||||
iconUrl: {
|
||||
type: 'string',
|
||||
@@ -305,7 +312,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
themeColor: instance.themeColor,
|
||||
mascotImageUrl: instance.mascotImageUrl,
|
||||
bannerUrl: instance.bannerUrl,
|
||||
errorImageUrl: instance.errorImageUrl,
|
||||
serverErrorImageUrl: instance.serverErrorImageUrl,
|
||||
notFoundImageUrl: instance.notFoundImageUrl,
|
||||
infoImageUrl: instance.infoImageUrl,
|
||||
iconUrl: instance.iconUrl,
|
||||
backgroundImageUrl: instance.backgroundImageUrl,
|
||||
logoImageUrl: instance.logoImageUrl,
|
||||
|
@@ -62,7 +62,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
try {
|
||||
if (new URL(ps.inbox).protocol !== 'https:') throw 'https only';
|
||||
if (new URL(ps.inbox).protocol !== 'https:') throw new Error('https only');
|
||||
} catch {
|
||||
throw new ApiError(meta.errors.invalidUrl);
|
||||
}
|
||||
|
@@ -32,7 +32,9 @@ export const paramDef = {
|
||||
themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' },
|
||||
mascotImageUrl: { type: 'string', nullable: true },
|
||||
bannerUrl: { type: 'string', nullable: true },
|
||||
errorImageUrl: { type: 'string', nullable: true },
|
||||
serverErrorImageUrl: { type: 'string', nullable: true },
|
||||
infoImageUrl: { type: 'string', nullable: true },
|
||||
notFoundImageUrl: { type: 'string', nullable: true },
|
||||
iconUrl: { type: 'string', nullable: true },
|
||||
backgroundImageUrl: { type: 'string', nullable: true },
|
||||
logoImageUrl: { type: 'string', nullable: true },
|
||||
@@ -149,6 +151,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
set.iconUrl = ps.iconUrl;
|
||||
}
|
||||
|
||||
if (ps.serverErrorImageUrl !== undefined) {
|
||||
set.serverErrorImageUrl = ps.serverErrorImageUrl;
|
||||
}
|
||||
|
||||
if (ps.infoImageUrl !== undefined) {
|
||||
set.infoImageUrl = ps.infoImageUrl;
|
||||
}
|
||||
|
||||
if (ps.notFoundImageUrl !== undefined) {
|
||||
set.notFoundImageUrl = ps.notFoundImageUrl;
|
||||
}
|
||||
|
||||
if (ps.backgroundImageUrl !== undefined) {
|
||||
set.backgroundImageUrl = ps.backgroundImageUrl;
|
||||
}
|
||||
@@ -281,10 +295,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
set.smtpPass = ps.smtpPass;
|
||||
}
|
||||
|
||||
if (ps.errorImageUrl !== undefined) {
|
||||
set.errorImageUrl = ps.errorImageUrl;
|
||||
}
|
||||
|
||||
if (ps.enableServiceWorker !== undefined) {
|
||||
set.enableServiceWorker = ps.enableServiceWorker;
|
||||
}
|
||||
|
@@ -113,6 +113,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
}
|
||||
|
||||
this.antennasRepository.update(antenna.id, {
|
||||
isActive: true,
|
||||
lastUsedAt: new Date(),
|
||||
});
|
||||
|
||||
|
@@ -55,7 +55,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
throw new ApiError(meta.errors.noSuchSession);
|
||||
}
|
||||
|
||||
// Generate access token
|
||||
const accessToken = secureRndstr(32, true);
|
||||
|
||||
// Fetch exist access token
|
||||
@@ -65,7 +64,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
});
|
||||
|
||||
if (exist == null) {
|
||||
// Lookup app
|
||||
const app = await this.appsRepository.findOneByOrFail({ id: session.appId });
|
||||
|
||||
// Generate Hash
|
||||
@@ -75,7 +73,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
const now = new Date();
|
||||
|
||||
// Insert access token doc
|
||||
await this.accessTokensRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: now,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { promisify } from 'node:util';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import * as cbor from 'cbor';
|
||||
import cbor from 'cbor';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
|
@@ -26,7 +26,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.accessTokensRepository.createQueryBuilder('token')
|
||||
.where('token.userId = :userId', { userId: me.id });
|
||||
.where('token.userId = :userId', { userId: me.id })
|
||||
.leftJoinAndSelect('token.app', 'app');
|
||||
|
||||
switch (ps.sort) {
|
||||
case '+createdAt': query.orderBy('token.createdAt', 'DESC'); break;
|
||||
@@ -40,7 +41,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
return await Promise.all(tokens.map(token => ({
|
||||
id: token.id,
|
||||
name: token.name,
|
||||
name: token.name ?? token.app?.name,
|
||||
createdAt: token.createdAt,
|
||||
lastUsedAt: token.lastUsedAt,
|
||||
permission: token.permission,
|
||||
|
@@ -91,18 +91,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
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); // untilIdに指定したものも含まれるため+1
|
||||
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);
|
||||
|
||||
if (notificationsRes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId) as Notification[];
|
||||
let notifications = notificationsRes.map(x => JSON.parse(x[1][1])).filter(x => x.id !== ps.untilId && x !== ps.sinceId) as Notification[];
|
||||
|
||||
if (includeTypes && includeTypes.length > 0) {
|
||||
notifications = notifications.filter(notification => includeTypes.includes(notification.type));
|
||||
|
@@ -141,13 +141,12 @@ export const paramDef = {
|
||||
preventAiLearning: { type: 'boolean' },
|
||||
isBot: { type: 'boolean' },
|
||||
isCat: { type: 'boolean' },
|
||||
showTimelineReplies: { type: 'boolean' },
|
||||
injectFeaturedNote: { type: 'boolean' },
|
||||
receiveAnnouncementEmail: { type: 'boolean' },
|
||||
alwaysMarkNsfw: { type: 'boolean' },
|
||||
autoSensitive: { type: 'boolean' },
|
||||
ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
|
||||
pinnedPageId: { type: 'string', format: 'misskey:id' },
|
||||
pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
mutedWords: { type: 'array' },
|
||||
mutedInstances: { type: 'array', items: {
|
||||
type: 'string',
|
||||
@@ -239,7 +238,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus;
|
||||
if (typeof ps.publicReactions === 'boolean') profileUpdates.publicReactions = ps.publicReactions;
|
||||
if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot;
|
||||
if (typeof ps.showTimelineReplies === 'boolean') updates.showTimelineReplies = ps.showTimelineReplies;
|
||||
if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot;
|
||||
if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
|
||||
if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle;
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { IsNull, LessThanOrEqual, MoreThan } from 'typeorm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import JSON5 from 'json5';
|
||||
import type { AdsRepository, UsersRepository } from '@/models/index.js';
|
||||
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
@@ -123,10 +124,17 @@ export const meta = {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
errorImageUrl: {
|
||||
serverErrorImageUrl: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
default: 'https://xn--931a.moe/aiart/yubitun.png',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
infoImageUrl: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
notFoundImageUrl: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
iconUrl: {
|
||||
type: 'string',
|
||||
@@ -287,13 +295,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
themeColor: instance.themeColor,
|
||||
mascotImageUrl: instance.mascotImageUrl,
|
||||
bannerUrl: instance.bannerUrl,
|
||||
errorImageUrl: instance.errorImageUrl,
|
||||
infoImageUrl: instance.infoImageUrl,
|
||||
serverErrorImageUrl: instance.serverErrorImageUrl,
|
||||
notFoundImageUrl: instance.notFoundImageUrl,
|
||||
iconUrl: instance.iconUrl,
|
||||
backgroundImageUrl: instance.backgroundImageUrl,
|
||||
logoImageUrl: instance.logoImageUrl,
|
||||
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
|
||||
defaultLightTheme: instance.defaultLightTheme,
|
||||
defaultDarkTheme: instance.defaultDarkTheme,
|
||||
// クライアントの手間を減らすためあらかじめJSONに変換しておく
|
||||
defaultLightTheme: instance.defaultLightTheme ? JSON.stringify(JSON5.parse(instance.defaultLightTheme)) : null,
|
||||
defaultDarkTheme: instance.defaultDarkTheme ? JSON.stringify(JSON5.parse(instance.defaultDarkTheme)) : null,
|
||||
ads: ads.map(ad => ({
|
||||
id: ad.id,
|
||||
url: ad.url,
|
||||
|
@@ -99,7 +99,7 @@ export const paramDef = {
|
||||
} },
|
||||
cw: { type: 'string', nullable: true, maxLength: 100 },
|
||||
localOnly: { type: 'boolean', default: false },
|
||||
reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote'], default: null },
|
||||
reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null },
|
||||
noExtractMentions: { type: 'boolean', default: false },
|
||||
noExtractHashtags: { type: 'boolean', default: false },
|
||||
noExtractEmojis: { type: 'boolean', default: false },
|
||||
|
@@ -34,11 +34,8 @@ export const meta = {
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
withFiles: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Only show notes that have attached files.',
|
||||
},
|
||||
withFiles: { type: 'boolean', default: false },
|
||||
withReplies: { type: 'boolean', default: false },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
@@ -78,7 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateRepliesQuery(query, me);
|
||||
this.queryService.generateRepliesQuery(query, ps.withReplies, me);
|
||||
if (me) {
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateMutedNoteQuery(query, me);
|
||||
|
@@ -46,11 +46,8 @@ export const paramDef = {
|
||||
includeMyRenotes: { type: 'boolean', default: true },
|
||||
includeRenotedMyNotes: { type: 'boolean', default: true },
|
||||
includeLocalRenotes: { type: 'boolean', default: true },
|
||||
withFiles: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Only show notes that have attached files.',
|
||||
},
|
||||
withFiles: { type: 'boolean', default: false },
|
||||
withReplies: { type: 'boolean', default: false },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
@@ -98,7 +95,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
.setParameters(followingQuery.getParameters());
|
||||
|
||||
this.queryService.generateChannelQuery(query, me);
|
||||
this.queryService.generateRepliesQuery(query, me);
|
||||
this.queryService.generateRepliesQuery(query, ps.withReplies, me);
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateMutedNoteQuery(query, me);
|
||||
|
@@ -36,11 +36,8 @@ export const meta = {
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
withFiles: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Only show notes that have attached files.',
|
||||
},
|
||||
withFiles: { type: 'boolean', default: false },
|
||||
withReplies: { type: 'boolean', default: false },
|
||||
fileType: { type: 'array', items: {
|
||||
type: 'string',
|
||||
} },
|
||||
@@ -86,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||
|
||||
this.queryService.generateChannelQuery(query, me);
|
||||
this.queryService.generateRepliesQuery(query, me);
|
||||
this.queryService.generateRepliesQuery(query, ps.withReplies, me);
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
if (me) this.queryService.generateMutedUserQuery(query, me);
|
||||
if (me) this.queryService.generateMutedNoteQuery(query, me);
|
||||
|
@@ -82,14 +82,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
try {
|
||||
if (ps.tag) {
|
||||
if (!safeForSql(normalizeForSearch(ps.tag))) throw 'Injection';
|
||||
if (!safeForSql(normalizeForSearch(ps.tag))) throw new Error('Injection');
|
||||
query.andWhere(`'{"${normalizeForSearch(ps.tag)}"}' <@ note.tags`);
|
||||
} else {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
for (const tags of ps.query!) {
|
||||
qb.orWhere(new Brackets(qb => {
|
||||
for (const tag of tags) {
|
||||
if (!safeForSql(normalizeForSearch(tag))) throw 'Injection';
|
||||
if (!safeForSql(normalizeForSearch(tag))) throw new Error('Injection');
|
||||
qb.andWhere(`'{"${normalizeForSearch(tag)}"}' <@ note.tags`);
|
||||
}
|
||||
}));
|
||||
|
@@ -35,11 +35,8 @@ export const paramDef = {
|
||||
includeMyRenotes: { type: 'boolean', default: true },
|
||||
includeRenotedMyNotes: { type: 'boolean', default: true },
|
||||
includeLocalRenotes: { type: 'boolean', default: true },
|
||||
withFiles: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Only show notes that have attached files.',
|
||||
},
|
||||
withFiles: { type: 'boolean', default: false },
|
||||
withReplies: { type: 'boolean', default: false },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
@@ -84,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
}
|
||||
|
||||
this.queryService.generateChannelQuery(query, me);
|
||||
this.queryService.generateRepliesQuery(query, me);
|
||||
this.queryService.generateRepliesQuery(query, ps.withReplies, me);
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateMutedNoteQuery(query, me);
|
||||
|
@@ -34,7 +34,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
private redisClient: Redis.Redis,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test';
|
||||
if (process.env.NODE_ENV !== 'test') throw new Error('NODE_ENV is not a test');
|
||||
|
||||
await redisClient.flushdb();
|
||||
await resetDb(this.db);
|
||||
|
@@ -30,6 +30,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const roles = await this.rolesRepository.findBy({
|
||||
isPublic: true,
|
||||
isExplorable: true,
|
||||
});
|
||||
return await this.roleEntityService.packMany(roles, me);
|
||||
});
|
||||
|
@@ -93,6 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
const query = this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||
.andWhere('(note.visibility = \'public\')')
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
|
@@ -49,6 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
const role = await this.rolesRepository.findOneBy({
|
||||
id: ps.roleId,
|
||||
isPublic: true,
|
||||
isExplorable: true,
|
||||
});
|
||||
|
||||
if (role == null) {
|
||||
|
@@ -0,0 +1,148 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UserListsRepository, UserListJoiningsRepository, BlockingsRepository } from '@/models/index.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { UserList } from '@/models/entities/UserList.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
import { UserListEntityService } from '@/core/entities/UserListEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { UserListService } from '@/core/UserListService.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
prohibitMoved: true,
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'UserList',
|
||||
},
|
||||
|
||||
errors: {
|
||||
tooManyUserLists: {
|
||||
message: 'You cannot create user list any more.',
|
||||
code: 'TOO_MANY_USERLISTS',
|
||||
id: 'e9c105b2-c595-47de-97fb-7f7c2c33e92f',
|
||||
},
|
||||
noSuchList: {
|
||||
message: 'No such list.',
|
||||
code: 'NO_SUCH_LIST',
|
||||
id: '9292f798-6175-4f7d-93f4-b6742279667d',
|
||||
},
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: '13c457db-a8cb-4d88-b70a-211ceeeabb5f',
|
||||
},
|
||||
|
||||
alreadyAdded: {
|
||||
message: 'That user has already been added to that list.',
|
||||
code: 'ALREADY_ADDED',
|
||||
id: 'c3ad6fdb-692b-47ee-a455-7bd12c7af615',
|
||||
},
|
||||
|
||||
youHaveBeenBlocked: {
|
||||
message: 'You cannot push this user because you have been blocked by this user.',
|
||||
code: 'YOU_HAVE_BEEN_BLOCKED',
|
||||
id: 'a2497f2a-2389-439c-8626-5298540530f4',
|
||||
},
|
||||
|
||||
tooManyUsers: {
|
||||
message: 'You can not push users any more.',
|
||||
code: 'TOO_MANY_USERS',
|
||||
id: '1845ea77-38d1-426e-8e4e-8b83b24f5bd7',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', minLength: 1, maxLength: 100 },
|
||||
listId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['name', 'listId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.userListsRepository)
|
||||
private userListsRepository: UserListsRepository,
|
||||
|
||||
@Inject(DI.userListJoiningsRepository)
|
||||
private userListJoiningsRepository: UserListJoiningsRepository,
|
||||
|
||||
@Inject(DI.blockingsRepository)
|
||||
private blockingsRepository: BlockingsRepository,
|
||||
|
||||
private userListService: UserListService,
|
||||
private userListEntityService: UserListEntityService,
|
||||
private idService: IdService,
|
||||
private getterService: GetterService,
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const list = await this.userListsRepository.findOneBy({
|
||||
id: ps.listId,
|
||||
isPublic: true,
|
||||
});
|
||||
if (list === null) throw new ApiError(meta.errors.noSuchList);
|
||||
const currentCount = await this.userListsRepository.countBy({
|
||||
userId: me.id,
|
||||
});
|
||||
if (currentCount > (await this.roleService.getUserPolicies(me.id)).userListLimit) {
|
||||
throw new ApiError(meta.errors.tooManyUserLists);
|
||||
}
|
||||
|
||||
const userList = await this.userListsRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
userId: me.id,
|
||||
name: ps.name,
|
||||
} as UserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
const users = (await this.userListJoiningsRepository.findBy({
|
||||
userListId: ps.listId,
|
||||
})).map(x => x.userId);
|
||||
|
||||
for (const user of users) {
|
||||
const currentUser = await this.getterService.getUser(user).catch(err => {
|
||||
if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser);
|
||||
throw err;
|
||||
});
|
||||
|
||||
if (currentUser.id !== me.id) {
|
||||
const block = await this.blockingsRepository.findOneBy({
|
||||
blockerId: currentUser.id,
|
||||
blockeeId: me.id,
|
||||
});
|
||||
if (block) {
|
||||
throw new ApiError(meta.errors.youHaveBeenBlocked);
|
||||
}
|
||||
}
|
||||
|
||||
const exist = await this.userListJoiningsRepository.findOneBy({
|
||||
userListId: userList.id,
|
||||
userId: currentUser.id,
|
||||
});
|
||||
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyAdded);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.userListService.push(currentUser, userList, me);
|
||||
} catch (err) {
|
||||
if (err instanceof UserListService.TooManyUsersError) {
|
||||
throw new ApiError(meta.errors.tooManyUsers);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
return await this.userListEntityService.pack(userList);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,70 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { UserListFavoritesRepository, UserListsRepository } from '@/models/index.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
errors: {
|
||||
noSuchList: {
|
||||
message: 'No such user list.',
|
||||
code: 'NO_SUCH_USER_LIST',
|
||||
id: '7dbaf3cf-7b42-4b8f-b431-b3919e580dbe',
|
||||
},
|
||||
|
||||
alreadyFavorited: {
|
||||
message: 'The list has already been favorited.',
|
||||
code: 'ALREADY_FAVORITED',
|
||||
id: '6425bba0-985b-461e-af1b-518070e72081',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
listId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['listId'],
|
||||
} as const;
|
||||
|
||||
@Injectable() // eslint-disable-next-line import/no-default-export
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor (
|
||||
@Inject(DI.userListsRepository)
|
||||
private userListsRepository: UserListsRepository,
|
||||
|
||||
@Inject(DI.userListFavoritesRepository)
|
||||
private userListFavoritesRepository: UserListFavoritesRepository,
|
||||
private idService: IdService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const userList = await this.userListsRepository.findOneBy({
|
||||
id: ps.listId,
|
||||
isPublic: true,
|
||||
});
|
||||
|
||||
if (userList === null) {
|
||||
throw new ApiError(meta.errors.noSuchList);
|
||||
}
|
||||
|
||||
const exist = await this.userListFavoritesRepository.findOneBy({
|
||||
userId: me.id,
|
||||
userListId: ps.listId,
|
||||
});
|
||||
|
||||
if (exist !== null) {
|
||||
throw new ApiError(meta.errors.alreadyFavorited);
|
||||
}
|
||||
|
||||
await this.userListFavoritesRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
userId: me.id,
|
||||
userListId: ps.listId,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,13 +1,14 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UserListsRepository } from '@/models/index.js';
|
||||
import type { UserListsRepository, UsersRepository } from '@/models/index.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { UserListEntityService } from '@/core/entities/UserListEntityService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['lists', 'account'],
|
||||
|
||||
requireCredential: true,
|
||||
requireCredential: false,
|
||||
|
||||
kind: 'read:account',
|
||||
|
||||
@@ -22,26 +23,58 @@ export const meta = {
|
||||
ref: 'UserList',
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: 'a8af4a82-0980-4cc4-a6af-8b0ffd54465e',
|
||||
},
|
||||
remoteUser: {
|
||||
message: 'Not allowed to load the remote user\'s list',
|
||||
code: 'REMOTE_USER_NOT_ALLOWED',
|
||||
id: '53858f1b-3315-4a01-81b7-db9b48d4b79a',
|
||||
},
|
||||
invalidParam: {
|
||||
message: 'Invalid param.',
|
||||
code: 'INVALID_PARAM',
|
||||
id: 'ab36de0e-29e9-48cb-9732-d82f1281620d',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
@Injectable() // eslint-disable-next-line import/no-default-export
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.userListsRepository)
|
||||
private userListsRepository: UserListsRepository,
|
||||
|
||||
private userListEntityService: UserListEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const userLists = await this.userListsRepository.findBy({
|
||||
if (typeof ps.userId !== 'undefined') {
|
||||
const user = await this.usersRepository.findOneBy({ id: ps.userId });
|
||||
if (user === null) throw new ApiError(meta.errors.noSuchUser);
|
||||
if (user.host !== null) throw new ApiError(meta.errors.remoteUser);
|
||||
} else if (me === null) {
|
||||
throw new ApiError(meta.errors.invalidParam);
|
||||
}
|
||||
|
||||
const userLists = await this.userListsRepository.findBy(typeof ps.userId === 'undefined' && me !== null ? {
|
||||
userId: me.id,
|
||||
} : {
|
||||
userId: ps.userId,
|
||||
isPublic: true,
|
||||
});
|
||||
|
||||
return await Promise.all(userLists.map(x => this.userListEntityService.pack(x)));
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UserListsRepository } from '@/models/index.js';
|
||||
import type { UserListsRepository, UserListFavoritesRepository } from '@/models/index.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { UserListEntityService } from '@/core/entities/UserListEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
@@ -8,7 +8,7 @@ import { ApiError } from '../../../error.js';
|
||||
export const meta = {
|
||||
tags: ['lists', 'account'],
|
||||
|
||||
requireCredential: true,
|
||||
requireCredential: false,
|
||||
|
||||
kind: 'read:account',
|
||||
|
||||
@@ -33,31 +33,54 @@ export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
listId: { type: 'string', format: 'misskey:id' },
|
||||
forPublic: { type: 'boolean', default: false },
|
||||
},
|
||||
required: ['listId'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
@Injectable() // eslint-disable-next-line import/no-default-export
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.userListsRepository)
|
||||
private userListsRepository: UserListsRepository,
|
||||
|
||||
@Inject(DI.userListFavoritesRepository)
|
||||
private userListFavoritesRepository: UserListFavoritesRepository,
|
||||
|
||||
private userListEntityService: UserListEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const additionalProperties: Partial<{ likedCount: number, isLiked: boolean }> = {};
|
||||
// Fetch the list
|
||||
const userList = await this.userListsRepository.findOneBy({
|
||||
const userList = await this.userListsRepository.findOneBy(!ps.forPublic && me !== null ? {
|
||||
id: ps.listId,
|
||||
userId: me.id,
|
||||
} : {
|
||||
id: ps.listId,
|
||||
isPublic: true,
|
||||
});
|
||||
|
||||
if (userList == null) {
|
||||
throw new ApiError(meta.errors.noSuchList);
|
||||
}
|
||||
|
||||
return await this.userListEntityService.pack(userList);
|
||||
if (ps.forPublic && userList.isPublic) {
|
||||
additionalProperties.likedCount = await this.userListFavoritesRepository.countBy({
|
||||
userListId: ps.listId,
|
||||
});
|
||||
if (me !== null) {
|
||||
additionalProperties.isLiked = (await this.userListFavoritesRepository.findOneBy({
|
||||
userId: me.id,
|
||||
userListId: ps.listId,
|
||||
}) !== null);
|
||||
} else {
|
||||
additionalProperties.isLiked = false;
|
||||
}
|
||||
}
|
||||
return {
|
||||
...await this.userListEntityService.pack(userList),
|
||||
...additionalProperties,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,63 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { UserListFavoritesRepository, UserListsRepository } from '@/models/index.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
errors: {
|
||||
noSuchList: {
|
||||
message: 'No such user list.',
|
||||
code: 'NO_SUCH_USER_LIST',
|
||||
id: 'baedb33e-76b8-4b0c-86a8-9375c0a7b94b',
|
||||
},
|
||||
|
||||
notFavorited: {
|
||||
message: 'You have not favorited the list.',
|
||||
code: 'ALREADY_FAVORITED',
|
||||
id: '835c4b27-463d-4cfa-969b-a9058678d465',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
listId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['listId'],
|
||||
} as const;
|
||||
|
||||
@Injectable() // eslint-disable-next-line import/no-default-export
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor (
|
||||
@Inject(DI.userListsRepository)
|
||||
private userListsRepository: UserListsRepository,
|
||||
|
||||
@Inject(DI.userListFavoritesRepository)
|
||||
private userListFavoritesRepository: UserListFavoritesRepository,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const userList = await this.userListsRepository.findOneBy({
|
||||
id: ps.listId,
|
||||
isPublic: true,
|
||||
});
|
||||
|
||||
if (userList === null) {
|
||||
throw new ApiError(meta.errors.noSuchList);
|
||||
}
|
||||
|
||||
const exist = await this.userListFavoritesRepository.findOneBy({
|
||||
userListId: ps.listId,
|
||||
userId: me.id,
|
||||
});
|
||||
|
||||
if (exist === null) {
|
||||
throw new ApiError(meta.errors.notFavorited);
|
||||
}
|
||||
|
||||
await this.userListFavoritesRepository.delete({ id: exist.id });
|
||||
});
|
||||
}
|
||||
}
|
@@ -34,8 +34,9 @@ export const paramDef = {
|
||||
properties: {
|
||||
listId: { type: 'string', format: 'misskey:id' },
|
||||
name: { type: 'string', minLength: 1, maxLength: 100 },
|
||||
isPublic: { type: 'boolean' },
|
||||
},
|
||||
required: ['listId', 'name'],
|
||||
required: ['listId'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@@ -48,7 +49,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
private userListEntityService: UserListEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
// Fetch the list
|
||||
const userList = await this.userListsRepository.findOneBy({
|
||||
id: ps.listId,
|
||||
userId: me.id,
|
||||
@@ -60,6 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
await this.userListsRepository.update(userList.id, {
|
||||
name: ps.name,
|
||||
isPublic: ps.isPublic,
|
||||
});
|
||||
|
||||
return await this.userListEntityService.pack(userList.id);
|
||||
|
@@ -1,228 +0,0 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { UsersRepository, NotesRepository, FollowingsRepository, DriveFilesRepository, NoteReactionsRepository, PageLikesRepository, NoteFavoritesRepository, PollVotesRepository } from '@/models/index.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users'],
|
||||
|
||||
requireCredential: false,
|
||||
|
||||
description: 'Show statistics about a user.',
|
||||
|
||||
errors: {
|
||||
noSuchUser: {
|
||||
message: 'No such user.',
|
||||
code: 'NO_SUCH_USER',
|
||||
id: '9e638e45-3b25-4ef7-8f95-07e8498f1819',
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
notesCount: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
repliesCount: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
renotesCount: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
repliedCount: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
renotedCount: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
pollVotesCount: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
pollVotedCount: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
localFollowingCount: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
remoteFollowingCount: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
localFollowersCount: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
remoteFollowersCount: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
followingCount: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
followersCount: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
sentReactionsCount: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
receivedReactionsCount: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
noteFavoritesCount: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
pageLikesCount: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
pageLikedCount: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
driveFilesCount: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
driveUsage: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
description: 'Drive usage in bytes',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['userId'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
@Inject(DI.followingsRepository)
|
||||
private followingsRepository: FollowingsRepository,
|
||||
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
@Inject(DI.noteReactionsRepository)
|
||||
private noteReactionsRepository: NoteReactionsRepository,
|
||||
|
||||
@Inject(DI.pageLikesRepository)
|
||||
private pageLikesRepository: PageLikesRepository,
|
||||
|
||||
@Inject(DI.noteFavoritesRepository)
|
||||
private noteFavoritesRepository: NoteFavoritesRepository,
|
||||
|
||||
@Inject(DI.pollVotesRepository)
|
||||
private pollVotesRepository: PollVotesRepository,
|
||||
|
||||
private driveFileEntityService: DriveFileEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const user = await this.usersRepository.findOneBy({ id: ps.userId });
|
||||
if (user == null) {
|
||||
throw new ApiError(meta.errors.noSuchUser);
|
||||
}
|
||||
|
||||
const result = await awaitAll({
|
||||
notesCount: this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.userId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
repliesCount: this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.userId = :userId', { userId: user.id })
|
||||
.andWhere('note.replyId IS NOT NULL')
|
||||
.getCount(),
|
||||
renotesCount: this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.userId = :userId', { userId: user.id })
|
||||
.andWhere('note.renoteId IS NOT NULL')
|
||||
.getCount(),
|
||||
repliedCount: this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.replyUserId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
renotedCount: this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.renoteUserId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
pollVotesCount: this.pollVotesRepository.createQueryBuilder('vote')
|
||||
.where('vote.userId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
pollVotedCount: this.pollVotesRepository.createQueryBuilder('vote')
|
||||
.innerJoin('vote.note', 'note')
|
||||
.where('note.userId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
localFollowingCount: this.followingsRepository.createQueryBuilder('following')
|
||||
.where('following.followerId = :userId', { userId: user.id })
|
||||
.andWhere('following.followeeHost IS NULL')
|
||||
.getCount(),
|
||||
remoteFollowingCount: this.followingsRepository.createQueryBuilder('following')
|
||||
.where('following.followerId = :userId', { userId: user.id })
|
||||
.andWhere('following.followeeHost IS NOT NULL')
|
||||
.getCount(),
|
||||
localFollowersCount: this.followingsRepository.createQueryBuilder('following')
|
||||
.where('following.followeeId = :userId', { userId: user.id })
|
||||
.andWhere('following.followerHost IS NULL')
|
||||
.getCount(),
|
||||
remoteFollowersCount: this.followingsRepository.createQueryBuilder('following')
|
||||
.where('following.followeeId = :userId', { userId: user.id })
|
||||
.andWhere('following.followerHost IS NOT NULL')
|
||||
.getCount(),
|
||||
sentReactionsCount: this.noteReactionsRepository.createQueryBuilder('reaction')
|
||||
.where('reaction.userId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
receivedReactionsCount: this.noteReactionsRepository.createQueryBuilder('reaction')
|
||||
.innerJoin('reaction.note', 'note')
|
||||
.where('note.userId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
noteFavoritesCount: this.noteFavoritesRepository.createQueryBuilder('favorite')
|
||||
.where('favorite.userId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
pageLikesCount: this.pageLikesRepository.createQueryBuilder('like')
|
||||
.where('like.userId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
pageLikedCount: this.pageLikesRepository.createQueryBuilder('like')
|
||||
.innerJoin('like.page', 'page')
|
||||
.where('page.userId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
driveFilesCount: this.driveFilesRepository.createQueryBuilder('file')
|
||||
.where('file.userId = :userId', { userId: user.id })
|
||||
.getCount(),
|
||||
driveUsage: this.driveFileEntityService.calcDriveUsageOf(user),
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
followingCount: result.localFollowingCount + result.remoteFollowingCount,
|
||||
followersCount: result.localFollowersCount + result.remoteFollowersCount,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
@@ -13,6 +13,7 @@ class GlobalTimelineChannel extends Channel {
|
||||
public readonly chName = 'globalTimeline';
|
||||
public static shouldShare = true;
|
||||
public static requireCredential = false;
|
||||
private withReplies: boolean;
|
||||
|
||||
constructor(
|
||||
private metaService: MetaService,
|
||||
@@ -31,6 +32,8 @@ class GlobalTimelineChannel extends Channel {
|
||||
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
|
||||
if (!policies.gtlAvailable) return;
|
||||
|
||||
this.withReplies = params.withReplies as boolean;
|
||||
|
||||
// Subscribe events
|
||||
this.subscriber.on('notesStream', this.onNote);
|
||||
}
|
||||
@@ -54,7 +57,7 @@ class GlobalTimelineChannel extends Channel {
|
||||
}
|
||||
|
||||
// 関係ない返信は除外
|
||||
if (note.reply && !this.user!.showTimelineReplies) {
|
||||
if (note.reply && !this.withReplies) {
|
||||
const reply = note.reply;
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
|
||||
|
@@ -11,6 +11,7 @@ class HomeTimelineChannel extends Channel {
|
||||
public readonly chName = 'homeTimeline';
|
||||
public static shouldShare = true;
|
||||
public static requireCredential = true;
|
||||
private withReplies: boolean;
|
||||
|
||||
constructor(
|
||||
private noteEntityService: NoteEntityService,
|
||||
@@ -24,6 +25,8 @@ class HomeTimelineChannel extends Channel {
|
||||
|
||||
@bindThis
|
||||
public async init(params: any) {
|
||||
this.withReplies = params.withReplies as boolean;
|
||||
|
||||
this.subscriber.on('notesStream', this.onNote);
|
||||
}
|
||||
|
||||
@@ -63,7 +66,7 @@ class HomeTimelineChannel extends Channel {
|
||||
}
|
||||
|
||||
// 関係ない返信は除外
|
||||
if (note.reply && !this.user!.showTimelineReplies) {
|
||||
if (note.reply && !this.withReplies) {
|
||||
const reply = note.reply;
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
|
||||
|
@@ -13,6 +13,7 @@ class HybridTimelineChannel extends Channel {
|
||||
public readonly chName = 'hybridTimeline';
|
||||
public static shouldShare = true;
|
||||
public static requireCredential = true;
|
||||
private withReplies: boolean;
|
||||
|
||||
constructor(
|
||||
private metaService: MetaService,
|
||||
@@ -31,6 +32,8 @@ class HybridTimelineChannel extends Channel {
|
||||
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
|
||||
if (!policies.ltlAvailable) return;
|
||||
|
||||
this.withReplies = params.withReplies as boolean;
|
||||
|
||||
// Subscribe events
|
||||
this.subscriber.on('notesStream', this.onNote);
|
||||
}
|
||||
@@ -75,7 +78,7 @@ class HybridTimelineChannel extends Channel {
|
||||
if (isInstanceMuted(note, new Set<string>(this.userProfile!.mutedInstances ?? []))) return;
|
||||
|
||||
// 関係ない返信は除外
|
||||
if (note.reply && !this.user!.showTimelineReplies) {
|
||||
if (note.reply && !this.withReplies) {
|
||||
const reply = note.reply;
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
|
||||
|
@@ -12,6 +12,7 @@ class LocalTimelineChannel extends Channel {
|
||||
public readonly chName = 'localTimeline';
|
||||
public static shouldShare = true;
|
||||
public static requireCredential = false;
|
||||
private withReplies: boolean;
|
||||
|
||||
constructor(
|
||||
private metaService: MetaService,
|
||||
@@ -30,6 +31,8 @@ class LocalTimelineChannel extends Channel {
|
||||
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
|
||||
if (!policies.ltlAvailable) return;
|
||||
|
||||
this.withReplies = params.withReplies as boolean;
|
||||
|
||||
// Subscribe events
|
||||
this.subscriber.on('notesStream', this.onNote);
|
||||
}
|
||||
@@ -54,7 +57,7 @@ class LocalTimelineChannel extends Channel {
|
||||
}
|
||||
|
||||
// 関係ない返信は除外
|
||||
if (note.reply && this.user && !this.user.showTimelineReplies) {
|
||||
if (note.reply && this.user && !this.withReplies) {
|
||||
const reply = note.reply;
|
||||
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
|
||||
if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return;
|
||||
|
@@ -5,15 +5,17 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import Channel from '../channel.js';
|
||||
import { StreamMessages } from '../types.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
|
||||
class RoleTimelineChannel extends Channel {
|
||||
public readonly chName = 'roleTimeline';
|
||||
public static shouldShare = false;
|
||||
public static requireCredential = false;
|
||||
private roleId: string;
|
||||
|
||||
|
||||
constructor(
|
||||
private noteEntityService: NoteEntityService,
|
||||
private roleservice: RoleService,
|
||||
|
||||
id: string,
|
||||
connection: Channel['connection'],
|
||||
@@ -34,6 +36,11 @@ class RoleTimelineChannel extends Channel {
|
||||
if (data.type === 'note') {
|
||||
const note = data.body;
|
||||
|
||||
if (!(await this.roleservice.isExplorable({ id: this.roleId }))) {
|
||||
return;
|
||||
}
|
||||
if (note.visibility !== 'public') return;
|
||||
|
||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
|
||||
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
|
||||
@@ -61,6 +68,7 @@ export class RoleTimelineChannelService {
|
||||
|
||||
constructor(
|
||||
private noteEntityService: NoteEntityService,
|
||||
private roleservice: RoleService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -68,6 +76,7 @@ export class RoleTimelineChannelService {
|
||||
public create(id: string, connection: Channel['connection']): RoleTimelineChannel {
|
||||
return new RoleTimelineChannel(
|
||||
this.noteEntityService,
|
||||
this.roleservice,
|
||||
id,
|
||||
connection,
|
||||
);
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import * as WebSocket from 'ws';
|
||||
import type { User } from '@/models/entities/User.js';
|
||||
import type { AccessToken } from '@/models/entities/AccessToken.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
@@ -7,7 +8,6 @@ import { bindThis } from '@/decorators.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { UserProfile } from '@/models/index.js';
|
||||
import type { ChannelsService } from './ChannelsService.js';
|
||||
import type * as websocket from 'websocket';
|
||||
import type { EventEmitter } from 'events';
|
||||
import type Channel from './channel.js';
|
||||
import type { StreamEventEmitter, StreamMessages } from './types.js';
|
||||
@@ -18,7 +18,7 @@ import type { StreamEventEmitter, StreamMessages } from './types.js';
|
||||
export default class Connection {
|
||||
public user?: User;
|
||||
public token?: AccessToken;
|
||||
private wsConnection: websocket.connection;
|
||||
private wsConnection: WebSocket.WebSocket;
|
||||
public subscriber: StreamEventEmitter;
|
||||
private channels: Channel[] = [];
|
||||
private subscribingNotes: any = {};
|
||||
@@ -37,11 +37,9 @@ export default class Connection {
|
||||
private notificationService: NotificationService,
|
||||
private cacheService: CacheService,
|
||||
|
||||
subscriber: EventEmitter,
|
||||
user: User | null | undefined,
|
||||
token: AccessToken | null | undefined,
|
||||
) {
|
||||
this.subscriber = subscriber;
|
||||
if (user) this.user = user;
|
||||
if (token) this.token = token;
|
||||
}
|
||||
@@ -70,12 +68,16 @@ export default class Connection {
|
||||
if (this.user != null) {
|
||||
await this.fetch();
|
||||
|
||||
this.fetchIntervalId = setInterval(this.fetch, 1000 * 10);
|
||||
if (!this.fetchIntervalId) {
|
||||
this.fetchIntervalId = setInterval(this.fetch, 1000 * 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async init2(wsConnection: websocket.connection) {
|
||||
public async listen(subscriber: EventEmitter, wsConnection: WebSocket.WebSocket) {
|
||||
this.subscriber = subscriber;
|
||||
|
||||
this.wsConnection = wsConnection;
|
||||
this.wsConnection.on('message', this.onWsConnectionMessage);
|
||||
|
||||
@@ -88,14 +90,11 @@ export default class Connection {
|
||||
* クライアントからメッセージ受信時
|
||||
*/
|
||||
@bindThis
|
||||
private async onWsConnectionMessage(data: websocket.Message) {
|
||||
if (data.type !== 'utf8') return;
|
||||
if (data.utf8Data == null) return;
|
||||
|
||||
private async onWsConnectionMessage(data: WebSocket.RawData) {
|
||||
let obj: Record<string, any>;
|
||||
|
||||
try {
|
||||
obj = JSON.parse(data.utf8Data);
|
||||
obj = JSON.parse(data.toString());
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
@@ -246,7 +245,7 @@ export default class Connection {
|
||||
|
||||
const ch: Channel = channelService.create(id, this);
|
||||
this.channels.push(ch);
|
||||
ch.init(params);
|
||||
ch.init(params ?? {});
|
||||
|
||||
if (pong) {
|
||||
this.sendMessageToWs('connected', {
|
||||
|
@@ -27,7 +27,7 @@ import { PageEntityService } from '@/core/entities/PageEntityService.js';
|
||||
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
|
||||
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
|
||||
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
|
||||
import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, NotesRepository, PagesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, Meta, NotesRepository, PagesRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { deepClone } from '@/misc/clone.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
@@ -118,6 +118,18 @@ export class ClientServerService {
|
||||
return (res);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private generateCommonPugData(meta: Meta) {
|
||||
return {
|
||||
instanceName: meta.name ?? 'Misskey',
|
||||
icon: meta.iconUrl,
|
||||
themeColor: meta.themeColor,
|
||||
serverErrorImageUrl: meta.serverErrorImageUrl ?? 'https://xn--931a.moe/assets/error.jpg',
|
||||
infoImageUrl: meta.infoImageUrl ?? 'https://xn--931a.moe/assets/info.jpg',
|
||||
notFoundImageUrl: meta.notFoundImageUrl ?? 'https://xn--931a.moe/assets/not-found.jpg',
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
|
||||
fastify.register(fastifyCookie, {});
|
||||
@@ -355,12 +367,10 @@ export class ClientServerService {
|
||||
reply.header('Cache-Control', 'public, max-age=30');
|
||||
return await reply.view('base', {
|
||||
img: meta.bannerUrl,
|
||||
title: meta.name ?? 'Misskey',
|
||||
instanceName: meta.name ?? 'Misskey',
|
||||
url: this.config.url,
|
||||
title: meta.name ?? 'Misskey',
|
||||
desc: meta.description,
|
||||
icon: meta.iconUrl,
|
||||
themeColor: meta.themeColor,
|
||||
...this.generateCommonPugData(meta),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -445,9 +455,7 @@ export class ClientServerService {
|
||||
user, profile, me,
|
||||
avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user),
|
||||
sub: request.params.sub,
|
||||
instanceName: meta.name ?? 'Misskey',
|
||||
icon: meta.iconUrl,
|
||||
themeColor: meta.themeColor,
|
||||
...this.generateCommonPugData(meta),
|
||||
});
|
||||
} else {
|
||||
// リモートユーザーなので
|
||||
@@ -495,9 +503,7 @@ export class ClientServerService {
|
||||
avatarUrl: _note.user.avatarUrl,
|
||||
// TODO: Let locale changeable by instance setting
|
||||
summary: getNoteSummary(_note),
|
||||
instanceName: meta.name ?? 'Misskey',
|
||||
icon: meta.iconUrl,
|
||||
themeColor: meta.themeColor,
|
||||
...this.generateCommonPugData(meta),
|
||||
});
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
@@ -536,9 +542,7 @@ export class ClientServerService {
|
||||
page: _page,
|
||||
profile,
|
||||
avatarUrl: _page.user.avatarUrl,
|
||||
instanceName: meta.name ?? 'Misskey',
|
||||
icon: meta.iconUrl,
|
||||
themeColor: meta.themeColor,
|
||||
...this.generateCommonPugData(meta),
|
||||
});
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
@@ -564,9 +568,7 @@ export class ClientServerService {
|
||||
flash: _flash,
|
||||
profile,
|
||||
avatarUrl: _flash.user.avatarUrl,
|
||||
instanceName: meta.name ?? 'Misskey',
|
||||
icon: meta.iconUrl,
|
||||
themeColor: meta.themeColor,
|
||||
...this.generateCommonPugData(meta),
|
||||
});
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
@@ -592,9 +594,7 @@ export class ClientServerService {
|
||||
clip: _clip,
|
||||
profile,
|
||||
avatarUrl: _clip.user.avatarUrl,
|
||||
instanceName: meta.name ?? 'Misskey',
|
||||
icon: meta.iconUrl,
|
||||
themeColor: meta.themeColor,
|
||||
...this.generateCommonPugData(meta),
|
||||
});
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
@@ -618,9 +618,7 @@ export class ClientServerService {
|
||||
post: _post,
|
||||
profile,
|
||||
avatarUrl: _post.user.avatarUrl,
|
||||
instanceName: meta.name ?? 'Misskey',
|
||||
icon: meta.iconUrl,
|
||||
themeColor: meta.themeColor,
|
||||
...this.generateCommonPugData(meta),
|
||||
});
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
@@ -639,9 +637,7 @@ export class ClientServerService {
|
||||
reply.header('Cache-Control', 'public, max-age=15');
|
||||
return await reply.view('channel', {
|
||||
channel: _channel,
|
||||
instanceName: meta.name ?? 'Misskey',
|
||||
icon: meta.iconUrl,
|
||||
themeColor: meta.themeColor,
|
||||
...this.generateCommonPugData(meta),
|
||||
});
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
|
@@ -116,9 +116,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
const colorSchema = localStorage.getItem('colorSchema');
|
||||
if (colorSchema) {
|
||||
document.documentElement.style.setProperty('color-schema', colorSchema);
|
||||
const colorScheme = localStorage.getItem('colorScheme');
|
||||
if (colorScheme) {
|
||||
document.documentElement.style.setProperty('color-scheme', colorScheme);
|
||||
}
|
||||
//#endregion
|
||||
|
||||
@@ -160,31 +160,34 @@
|
||||
<path d="M12 9v2m0 4v.01"></path>
|
||||
<path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path>
|
||||
</svg>
|
||||
<h1>An error has occurred!</h1>
|
||||
<h1>Failed to load<br>読み込みに失敗しました</h1>
|
||||
<button class="button-big" id="reload">
|
||||
<span class="button-label-big">Refresh</span>
|
||||
<span class="button-label-big">Reload / リロード</span>
|
||||
</button>
|
||||
<p class="dont-worry">Don't worry, it's (probably) not your fault.</p>
|
||||
<p>If the problem persists after refreshing, please contact your instance's administrator.<br>You may also try the following options:</p>
|
||||
<p>Update your os and browser.</p>
|
||||
<p>Disable an adblocker.</p>
|
||||
<a href="/flush">
|
||||
<button class="button-small">
|
||||
<span class="button-label-small">Clear preferences and cache</span>
|
||||
</button>
|
||||
</a>
|
||||
<br>
|
||||
<a href="/cli">
|
||||
<button class="button-small">
|
||||
<span class="button-label-small">Start the simple client</span>
|
||||
</button>
|
||||
</a>
|
||||
<br>
|
||||
<a href="/bios">
|
||||
<button class="button-small">
|
||||
<span class="button-label-small">Start the repair tool</span>
|
||||
</button>
|
||||
</a>
|
||||
<p><b>The following actions may solve the problem. / 以下を行うと解決する可能性があります。</b></p>
|
||||
<p>Clear the browser cache / ブラウザのキャッシュをクリアする</p>
|
||||
<p>Update your os and browser / ブラウザおよびOSを最新バージョンに更新する</p>
|
||||
<p>Disable an adblocker / アドブロッカーを無効にする</p>
|
||||
<details style="color: #86b300;">
|
||||
<summary>Other options / その他のオプション</summary>
|
||||
<a href="/flush">
|
||||
<button class="button-small">
|
||||
<span class="button-label-small">Clear preferences and cache</span>
|
||||
</button>
|
||||
</a>
|
||||
<br>
|
||||
<a href="/cli">
|
||||
<button class="button-small">
|
||||
<span class="button-label-small">Start the simple client</span>
|
||||
</button>
|
||||
</a>
|
||||
<br>
|
||||
<a href="/bios">
|
||||
<button class="button-small">
|
||||
<span class="button-label-small">Start the repair tool</span>
|
||||
</button>
|
||||
</a>
|
||||
</details>
|
||||
<br>
|
||||
<div id="errors"></div>
|
||||
`;
|
||||
@@ -194,6 +197,7 @@
|
||||
errorsElement = document.getElementById('errors');
|
||||
}
|
||||
const detailsElement = document.createElement('details');
|
||||
detailsElement.id = 'errorInfo';
|
||||
detailsElement.innerHTML = `
|
||||
<br>
|
||||
<summary>
|
||||
@@ -250,7 +254,7 @@
|
||||
.button-label-big {
|
||||
color: #222;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
font-size: 1.2em;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
@@ -270,11 +274,6 @@
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.dont-worry,
|
||||
#msg {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.icon-warning {
|
||||
color: #dec340;
|
||||
height: 4rem;
|
||||
@@ -282,14 +281,15 @@
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 32px;
|
||||
font-size: 1.5em;
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: Fira, FiraCode, monospace;
|
||||
}
|
||||
|
||||
details {
|
||||
#errorInfo {
|
||||
background: #333;
|
||||
margin-bottom: 2rem;
|
||||
padding: 0.5rem 1rem;
|
||||
@@ -299,16 +299,16 @@
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
summary {
|
||||
#errorInfo summary {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
summary > * {
|
||||
#errorInfo summary > * {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
details {
|
||||
#errorInfo {
|
||||
width: 50%;
|
||||
}
|
||||
`)
|
||||
|
@@ -25,18 +25,17 @@ html
|
||||
meta(name='referrer' content='origin')
|
||||
meta(name='theme-color' content= themeColor || '#86b300')
|
||||
meta(name='theme-color-orig' content= themeColor || '#86b300')
|
||||
meta(property='twitter:card' content='summary')
|
||||
meta(property='og:site_name' content= instanceName || 'Misskey')
|
||||
meta(name='viewport' content='width=device-width, initial-scale=1')
|
||||
link(rel='icon' href= icon || '/favicon.ico')
|
||||
link(rel='apple-touch-icon' href= icon || '/apple-touch-icon.png')
|
||||
link(rel='manifest' href='/manifest.json')
|
||||
link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${url}/opensearch.xml`)
|
||||
link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg')
|
||||
link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg')
|
||||
link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg')
|
||||
link(rel='prefetch' href=serverErrorImageUrl)
|
||||
link(rel='prefetch' href=infoImageUrl)
|
||||
link(rel='prefetch' href=notFoundImageUrl)
|
||||
//- https://github.com/misskey-dev/misskey/issues/9842
|
||||
link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.17.0')
|
||||
link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.21.0')
|
||||
link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
|
||||
|
||||
if !config.clientManifestExists
|
||||
@@ -59,6 +58,7 @@ html
|
||||
meta(property='og:title' content= title || 'Misskey')
|
||||
meta(property='og:description' content= desc || '✨🌎✨ A interplanetary communication platform ✨🚀✨')
|
||||
meta(property='og:image' content= img)
|
||||
meta(property='twitter:card' content='summary')
|
||||
|
||||
style
|
||||
include ../style.css
|
||||
|
@@ -16,3 +16,4 @@ block og
|
||||
meta(property='og:description' content= channel.description)
|
||||
meta(property='og:url' content= url)
|
||||
meta(property='og:image' content= channel.bannerUrl)
|
||||
meta(property='twitter:card' content='summary')
|
||||
|
@@ -17,6 +17,7 @@ block og
|
||||
meta(property='og:description' content= clip.description)
|
||||
meta(property='og:url' content= url)
|
||||
meta(property='og:image' content= avatarUrl)
|
||||
meta(property='twitter:card' content='summary')
|
||||
|
||||
block meta
|
||||
if profile.noCrawle
|
||||
|
@@ -17,6 +17,7 @@ block og
|
||||
meta(property='og:description' content= flash.summary)
|
||||
meta(property='og:url' content= url)
|
||||
meta(property='og:image' content= avatarUrl)
|
||||
meta(property='twitter:card' content='summary')
|
||||
|
||||
block meta
|
||||
if profile.noCrawle
|
||||
|
@@ -17,6 +17,7 @@ block og
|
||||
meta(property='og:description' content= post.description)
|
||||
meta(property='og:url' content= url)
|
||||
meta(property='og:image' content= post.files[0].thumbnailUrl)
|
||||
meta(property='twitter:card' content='summary_large_image')
|
||||
|
||||
block meta
|
||||
if user.host || profile.noCrawle
|
||||
|
@@ -5,6 +5,8 @@ block vars
|
||||
- const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`;
|
||||
- const url = `${config.url}/notes/${note.id}`;
|
||||
- const isRenote = note.renote && note.text == null && note.fileIds.length == 0 && note.poll == null;
|
||||
- const image = (note.files || []).find(file => file.type.startsWith('image/') && !file.isSensitive)
|
||||
- const video = (note.files || []).find(file => file.type.startsWith('video/') && !file.isSensitive)
|
||||
|
||||
block title
|
||||
= `${title} | ${instanceName}`
|
||||
@@ -17,7 +19,19 @@ block og
|
||||
meta(property='og:title' content= title)
|
||||
meta(property='og:description' content= summary)
|
||||
meta(property='og:url' content= url)
|
||||
meta(property='og:image' content= avatarUrl)
|
||||
if video
|
||||
meta(property='og:video:url' content= video.url)
|
||||
meta(property='og:video:secure_url' content= video.url)
|
||||
meta(property='og:video:type' content= video.type)
|
||||
// FIXME: add width and height
|
||||
// FIXME: add embed player for Twitter
|
||||
if image
|
||||
meta(property='twitter:card' content='summary_large_image')
|
||||
meta(property='og:image' content= image.url)
|
||||
else
|
||||
meta(property='twitter:card' content='summary')
|
||||
meta(property='og:image' content= avatarUrl)
|
||||
|
||||
|
||||
block meta
|
||||
if user.host || isRenote || profile.noCrawle
|
||||
|
@@ -17,6 +17,7 @@ block og
|
||||
meta(property='og:description' content= page.summary)
|
||||
meta(property='og:url' content= url)
|
||||
meta(property='og:image' content= page.eyeCatchingImage ? page.eyeCatchingImage.thumbnailUrl : avatarUrl)
|
||||
meta(property='twitter:card' content= page.eyeCatchingImage ? 'summary_large_image' : 'summary')
|
||||
|
||||
block meta
|
||||
if profile.noCrawle
|
||||
|
@@ -16,6 +16,7 @@ block og
|
||||
meta(property='og:description' content= profile.description)
|
||||
meta(property='og:url' content= url)
|
||||
meta(property='og:image' content= avatarUrl)
|
||||
meta(property='twitter:card' content='summary')
|
||||
|
||||
block meta
|
||||
if user.host || profile.noCrawle
|
||||
|
Reference in New Issue
Block a user