merge: upstream
This commit is contained in:
commit
42bf8e5e76
86 changed files with 3938 additions and 2258 deletions
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class EnableFanoutTimelineDbFallback1700096812223 {
|
||||
name = 'EnableFanoutTimelineDbFallback1700096812223'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "enableFanoutTimelineDbFallback" boolean NOT NULL DEFAULT true`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableFanoutTimelineDbFallback"`);
|
||||
}
|
||||
}
|
|
@ -57,16 +57,15 @@
|
|||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.412.0",
|
||||
"@aws-sdk/lib-storage": "3.412.0",
|
||||
"@smithy/node-http-handler": "2.1.5",
|
||||
"@bull-board/api": "5.9.1",
|
||||
"@bull-board/fastify": "5.9.1",
|
||||
"@bull-board/ui": "5.9.1",
|
||||
"@discordapp/twemoji": "14.1.2",
|
||||
"@fastify/accepts": "4.2.0",
|
||||
"@fastify/cookie": "9.1.0",
|
||||
"@fastify/cookie": "9.2.0",
|
||||
"@fastify/cors": "8.4.1",
|
||||
"@fastify/express": "2.3.0",
|
||||
"@fastify/http-proxy": "9.2.1",
|
||||
"@fastify/http-proxy": "9.3.0",
|
||||
"@fastify/multipart": "8.0.0",
|
||||
"@fastify/static": "6.12.0",
|
||||
"@fastify/view": "8.2.0",
|
||||
|
@ -76,8 +75,9 @@
|
|||
"@peertube/http-signature": "1.7.0",
|
||||
"@simplewebauthn/server": "8.3.5",
|
||||
"@sinonjs/fake-timers": "11.2.2",
|
||||
"@swc/cli": "0.1.62",
|
||||
"@swc/core": "1.3.95",
|
||||
"@smithy/node-http-handler": "2.1.5",
|
||||
"@swc/cli": "0.1.63",
|
||||
"@swc/core": "1.3.96",
|
||||
"accepts": "1.3.8",
|
||||
"ajv": "8.12.0",
|
||||
"archiver": "6.0.1",
|
||||
|
@ -86,7 +86,7 @@
|
|||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "2.0.5",
|
||||
"body-parser": "1.20.2",
|
||||
"bullmq": "4.12.8",
|
||||
"bullmq": "4.13.3",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"cbor": "9.0.1",
|
||||
"chalk": "5.3.0",
|
||||
|
@ -99,8 +99,9 @@
|
|||
"deep-email-validator": "0.1.21",
|
||||
"fastify-multer": "^2.0.3",
|
||||
"fastify": "4.24.3",
|
||||
"fastify-raw-body": "4.3.0",
|
||||
"feed": "4.2.2",
|
||||
"file-type": "18.6.0",
|
||||
"file-type": "18.7.0",
|
||||
"fluent-ffmpeg": "2.1.2",
|
||||
"form-data": "4.0.0",
|
||||
"got": "13.0.0",
|
||||
|
@ -123,7 +124,7 @@
|
|||
"mime-types": "2.1.35",
|
||||
"misskey-js": "workspace:*",
|
||||
"ms": "3.0.0-canary.1",
|
||||
"nanoid": "5.0.2",
|
||||
"nanoid": "5.0.3",
|
||||
"nested-property": "4.0.0",
|
||||
"node-fetch": "3.3.2",
|
||||
"nodemailer": "6.9.7",
|
||||
|
@ -131,7 +132,7 @@
|
|||
"oauth2orize": "1.12.0",
|
||||
"oauth2orize-pkce": "0.1.2",
|
||||
"os-utils": "0.0.14",
|
||||
"otpauth": "9.1.5",
|
||||
"otpauth": "9.2.0",
|
||||
"parse5": "7.1.2",
|
||||
"pg": "8.11.3",
|
||||
"pkce-challenge": "4.0.1",
|
||||
|
@ -143,20 +144,21 @@
|
|||
"qrcode": "1.5.3",
|
||||
"random-seed": "0.3.0",
|
||||
"ratelimiter": "3.4.1",
|
||||
"re2": "1.20.5",
|
||||
"re2": "1.20.8",
|
||||
"redis-lock": "0.1.4",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rename": "1.0.4",
|
||||
"rss-parser": "3.13.0",
|
||||
"rxjs": "7.8.1",
|
||||
"sanitize-html": "2.11.0",
|
||||
"secure-json-parse": "2.7.0",
|
||||
"sharp": "0.32.6",
|
||||
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
|
||||
"slacc": "0.0.10",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"summaly": "github:misskey-dev/summaly",
|
||||
"systeminformation": "5.21.15",
|
||||
"systeminformation": "5.21.17",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.1",
|
||||
"tsc-alias": "1.8.8",
|
||||
|
@ -175,51 +177,51 @@
|
|||
"@jest/globals": "29.7.0",
|
||||
"@simplewebauthn/typescript-types": "8.3.4",
|
||||
"@swc/jest": "0.2.29",
|
||||
"@types/accepts": "1.3.6",
|
||||
"@types/archiver": "6.0.0",
|
||||
"@types/bcryptjs": "2.4.5",
|
||||
"@types/body-parser": "1.19.4",
|
||||
"@types/accepts": "1.3.7",
|
||||
"@types/archiver": "6.0.1",
|
||||
"@types/bcryptjs": "2.4.6",
|
||||
"@types/body-parser": "1.19.5",
|
||||
"@types/cbor": "6.0.0",
|
||||
"@types/color-convert": "2.0.2",
|
||||
"@types/content-disposition": "0.5.7",
|
||||
"@types/fluent-ffmpeg": "2.1.23",
|
||||
"@types/http-link-header": "1.0.4",
|
||||
"@types/jest": "29.5.7",
|
||||
"@types/js-yaml": "4.0.8",
|
||||
"@types/jsdom": "21.1.4",
|
||||
"@types/jsonld": "1.5.11",
|
||||
"@types/jsrsasign": "10.5.11",
|
||||
"@types/mime-types": "2.1.3",
|
||||
"@types/ms": "0.7.33",
|
||||
"@types/node": "20.8.10",
|
||||
"@types/color-convert": "2.0.3",
|
||||
"@types/content-disposition": "0.5.8",
|
||||
"@types/fluent-ffmpeg": "2.1.24",
|
||||
"@types/http-link-header": "1.0.5",
|
||||
"@types/jest": "29.5.8",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/jsdom": "21.1.5",
|
||||
"@types/jsonld": "1.5.12",
|
||||
"@types/jsrsasign": "10.5.12",
|
||||
"@types/mime-types": "2.1.4",
|
||||
"@types/ms": "0.7.34",
|
||||
"@types/node": "20.9.1",
|
||||
"@types/node-fetch": "3.0.3",
|
||||
"@types/nodemailer": "6.4.13",
|
||||
"@types/oauth": "0.9.3",
|
||||
"@types/oauth2orize": "1.11.2",
|
||||
"@types/oauth2orize-pkce": "0.1.1",
|
||||
"@types/pg": "8.10.7",
|
||||
"@types/pug": "2.0.8",
|
||||
"@types/punycode": "2.1.1",
|
||||
"@types/qrcode": "1.5.4",
|
||||
"@types/random-seed": "0.3.4",
|
||||
"@types/ratelimiter": "3.4.5",
|
||||
"@types/rename": "1.0.6",
|
||||
"@types/sanitize-html": "2.9.3",
|
||||
"@types/semver": "7.5.4",
|
||||
"@types/nodemailer": "6.4.14",
|
||||
"@types/oauth": "0.9.4",
|
||||
"@types/oauth2orize": "1.11.3",
|
||||
"@types/oauth2orize-pkce": "0.1.2",
|
||||
"@types/pg": "8.10.9",
|
||||
"@types/pug": "2.0.9",
|
||||
"@types/punycode": "2.1.2",
|
||||
"@types/qrcode": "1.5.5",
|
||||
"@types/random-seed": "0.3.5",
|
||||
"@types/ratelimiter": "3.4.6",
|
||||
"@types/rename": "1.0.7",
|
||||
"@types/sanitize-html": "2.9.4",
|
||||
"@types/semver": "7.5.5",
|
||||
"@types/sharp": "0.32.0",
|
||||
"@types/simple-oauth2": "5.0.6",
|
||||
"@types/sinonjs__fake-timers": "8.1.4",
|
||||
"@types/tinycolor2": "1.4.5",
|
||||
"@types/tmp": "0.2.5",
|
||||
"@types/vary": "1.1.2",
|
||||
"@types/web-push": "3.6.2",
|
||||
"@types/simple-oauth2": "5.0.7",
|
||||
"@types/sinonjs__fake-timers": "8.1.5",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/tmp": "0.2.6",
|
||||
"@types/vary": "1.1.3",
|
||||
"@types/web-push": "3.6.3",
|
||||
"@types/ws": "8.5.9",
|
||||
"@types/uuid": "^9.0.4",
|
||||
"@types/ws": "8.5.8",
|
||||
"@typescript-eslint/eslint-plugin": "6.9.1",
|
||||
"@typescript-eslint/parser": "6.9.1",
|
||||
"@typescript-eslint/eslint-plugin": "6.11.0",
|
||||
"@typescript-eslint/parser": "6.11.0",
|
||||
"aws-sdk-client-mock": "3.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.52.0",
|
||||
"eslint": "8.53.0",
|
||||
"eslint-plugin-import": "2.29.0",
|
||||
"execa": "8.0.1",
|
||||
"jest": "29.7.0",
|
||||
|
|
|
@ -276,9 +276,18 @@ export class MfmService {
|
|||
},
|
||||
|
||||
fn: (node) => {
|
||||
const el = doc.createElement('i');
|
||||
appendChildren(node.children, el);
|
||||
return el;
|
||||
if (node.props.name === 'unixtime') {
|
||||
const text = node.children[0]!.type === 'text' ? node.children[0].props.text : '';
|
||||
const date = new Date(parseInt(text, 10) * 1000);
|
||||
const el = doc.createElement('time');
|
||||
el.setAttribute('datetime', date.toISOString());
|
||||
el.textContent = date.toISOString();
|
||||
return el;
|
||||
} else {
|
||||
const el = doc.createElement('i');
|
||||
appendChildren(node.children, el);
|
||||
return el;
|
||||
}
|
||||
},
|
||||
|
||||
blockCode: (node) => {
|
||||
|
|
|
@ -696,11 +696,13 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
followeeId: user.id,
|
||||
notify: 'normal',
|
||||
}).then(followings => {
|
||||
for (const following of followings) {
|
||||
// TODO: ワードミュート考慮
|
||||
this.notificationService.createNotification(following.followerId, 'note', {
|
||||
noteId: note.id,
|
||||
}, user.id);
|
||||
if (note.visibility !== 'specified') {
|
||||
for (const following of followings) {
|
||||
// TODO: ワードミュート考慮
|
||||
this.notificationService.createNotification(following.followerId, 'note', {
|
||||
noteId: note.id,
|
||||
}, user.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -485,7 +485,7 @@ export class ApRendererService {
|
|||
const attachment = profile.fields.map(field => ({
|
||||
type: 'PropertyValue',
|
||||
name: field.name,
|
||||
value: /^https?:/.test(field.value)
|
||||
value: (field.value.startsWith('http://') || field.value.startsWith('https://'))
|
||||
? `<a href="${new URL(field.value).href}" rel="me nofollow noopener" target="_blank">${new URL(field.value).href}</a>`
|
||||
: field.value,
|
||||
}));
|
||||
|
|
|
@ -509,6 +509,11 @@ export class MiMeta {
|
|||
})
|
||||
public enableFanoutTimeline: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: true,
|
||||
})
|
||||
public enableFanoutTimelineDbFallback: boolean;
|
||||
|
||||
@Column('integer', {
|
||||
default: 300,
|
||||
})
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as crypto from 'node:crypto';
|
||||
import { IncomingMessage } from 'node:http';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import fastifyAccepts from '@fastify/accepts';
|
||||
|
@ -10,6 +11,7 @@ import httpSignature from '@peertube/http-signature';
|
|||
import { Brackets, In, IsNull, LessThan, Not } from 'typeorm';
|
||||
import accepts from 'accepts';
|
||||
import vary from 'vary';
|
||||
import secureJson from 'secure-json-parse';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { FollowingsRepository, NotesRepository, EmojisRepository, NoteReactionsRepository, UserProfilesRepository, UserNotePiningsRepository, UsersRepository, FollowRequestsRepository } from '@/models/_.js';
|
||||
import * as url from '@/misc/prelude/url.js';
|
||||
|
@ -27,7 +29,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { IActivity } from '@/core/activitypub/type.js';
|
||||
import { isPureRenote } from '@/misc/is-pure-renote.js';
|
||||
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
|
||||
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify';
|
||||
import type { FindOptionsWhere } from 'typeorm';
|
||||
|
||||
const ACTIVITY_JSON = 'application/activity+json; charset=utf-8';
|
||||
|
@ -108,7 +110,58 @@ export class ActivityPubServerService {
|
|||
return;
|
||||
}
|
||||
|
||||
// TODO: request.bodyのバリデーション?
|
||||
if (signature.params.headers.indexOf('host') === -1
|
||||
|| request.headers.host !== this.config.host) {
|
||||
// Host not specified or not match.
|
||||
reply.code(401);
|
||||
return;
|
||||
}
|
||||
|
||||
if (signature.params.headers.indexOf('digest') === -1) {
|
||||
// Digest not found.
|
||||
reply.code(401);
|
||||
} else {
|
||||
const digest = request.headers.digest;
|
||||
|
||||
if (typeof digest !== 'string') {
|
||||
// Huh?
|
||||
reply.code(401);
|
||||
return;
|
||||
}
|
||||
|
||||
const re = /^([a-zA-Z0-9\-]+)=(.+)$/;
|
||||
const match = digest.match(re);
|
||||
|
||||
if (match == null) {
|
||||
// Invalid digest
|
||||
reply.code(401);
|
||||
return;
|
||||
}
|
||||
|
||||
const algo = match[1];
|
||||
const digestValue = match[2];
|
||||
|
||||
if (algo !== 'SHA-256') {
|
||||
// Unsupported digest algorithm
|
||||
reply.code(401);
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.rawBody == null) {
|
||||
// Bad request
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
|
||||
const hash = crypto.createHash('sha256').update(request.rawBody).digest('base64');
|
||||
|
||||
if (hash !== digestValue) {
|
||||
// Invalid digest
|
||||
reply.code(401);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.queueService.inbox(request.body as IActivity, signature);
|
||||
|
||||
reply.code(202);
|
||||
|
@ -460,9 +513,28 @@ export class ActivityPubServerService {
|
|||
},
|
||||
});
|
||||
|
||||
const almostDefaultJsonParser: FastifyBodyParser<Buffer> = function (request, rawBody, done) {
|
||||
if (rawBody.length === 0) {
|
||||
const err = new Error('Body cannot be empty!') as any;
|
||||
err.statusCode = 400;
|
||||
return done(err);
|
||||
}
|
||||
|
||||
try {
|
||||
const json = secureJson.parse(rawBody.toString('utf8'), null, {
|
||||
protoAction: 'ignore',
|
||||
constructorAction: 'ignore',
|
||||
});
|
||||
done(null, json);
|
||||
} catch (err: any) {
|
||||
err.statusCode = 400;
|
||||
return done(err);
|
||||
}
|
||||
};
|
||||
|
||||
fastify.register(fastifyAccepts);
|
||||
fastify.addContentTypeParser('application/activity+json', { parseAs: 'string' }, fastify.getDefaultJsonParser('ignore', 'ignore'));
|
||||
fastify.addContentTypeParser('application/ld+json', { parseAs: 'string' }, fastify.getDefaultJsonParser('ignore', 'ignore'));
|
||||
fastify.addContentTypeParser('application/activity+json', { parseAs: 'buffer' }, almostDefaultJsonParser);
|
||||
fastify.addContentTypeParser('application/ld+json', { parseAs: 'buffer' }, almostDefaultJsonParser);
|
||||
|
||||
fastify.addHook('onRequest', (request, reply, done) => {
|
||||
reply.header('Access-Control-Allow-Headers', 'Accept');
|
||||
|
@ -474,8 +546,8 @@ export class ActivityPubServerService {
|
|||
|
||||
//#region Routing
|
||||
// inbox (limit: 64kb)
|
||||
fastify.post('/inbox', { bodyLimit: 1024 * 64 }, async (request, reply) => await this.inbox(request, reply));
|
||||
fastify.post('/users/:user/inbox', { bodyLimit: 1024 * 64 }, async (request, reply) => await this.inbox(request, reply));
|
||||
fastify.post('/inbox', { config: { rawBody: true }, bodyLimit: 1024 * 64 }, async (request, reply) => await this.inbox(request, reply));
|
||||
fastify.post('/users/:user/inbox', { config: { rawBody: true }, bodyLimit: 1024 * 64 }, async (request, reply) => await this.inbox(request, reply));
|
||||
|
||||
// note
|
||||
fastify.get<{ Params: { note: string; } }>('/notes/:note', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
|
||||
|
|
|
@ -9,6 +9,7 @@ import { fileURLToPath } from 'node:url';
|
|||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import Fastify, { FastifyInstance } from 'fastify';
|
||||
import fastifyStatic from '@fastify/static';
|
||||
import fastifyRawBody from 'fastify-raw-body';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
|
@ -88,6 +89,13 @@ export class ServerService implements OnApplicationShutdown {
|
|||
});
|
||||
}
|
||||
|
||||
// Register raw-body parser for ActivityPub HTTP signature validation.
|
||||
await fastify.register(fastifyRawBody, {
|
||||
global: false,
|
||||
encoding: null,
|
||||
runFirst: true,
|
||||
});
|
||||
|
||||
// Register non-serving static server so that the child services can use reply.sendFile.
|
||||
// `root` here is just a placeholder and each call must use its own `rootPath`.
|
||||
fastify.register(fastifyStatic, {
|
||||
|
|
|
@ -10,6 +10,7 @@ import * as ep___admin_meta from './endpoints/admin/meta.js';
|
|||
import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js';
|
||||
import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js';
|
||||
import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js';
|
||||
import * as ep___admin_accounts_findByEmail from './endpoints/admin/accounts/find-by-email.js';
|
||||
import * as ep___admin_ad_create from './endpoints/admin/ad/create.js';
|
||||
import * as ep___admin_ad_delete from './endpoints/admin/ad/delete.js';
|
||||
import * as ep___admin_ad_list from './endpoints/admin/ad/list.js';
|
||||
|
@ -380,6 +381,7 @@ const $admin_meta: Provider = { provide: 'ep:admin/meta', useClass: ep___admin_m
|
|||
const $admin_abuseUserReports: Provider = { provide: 'ep:admin/abuse-user-reports', useClass: ep___admin_abuseUserReports.default };
|
||||
const $admin_accounts_create: Provider = { provide: 'ep:admin/accounts/create', useClass: ep___admin_accounts_create.default };
|
||||
const $admin_accounts_delete: Provider = { provide: 'ep:admin/accounts/delete', useClass: ep___admin_accounts_delete.default };
|
||||
const $admin_accounts_findByEmail: Provider = { provide: 'ep:admin/accounts/find-by-email', useClass: ep___admin_accounts_findByEmail.default };
|
||||
const $admin_ad_create: Provider = { provide: 'ep:admin/ad/create', useClass: ep___admin_ad_create.default };
|
||||
const $admin_ad_delete: Provider = { provide: 'ep:admin/ad/delete', useClass: ep___admin_ad_delete.default };
|
||||
const $admin_ad_list: Provider = { provide: 'ep:admin/ad/list', useClass: ep___admin_ad_list.default };
|
||||
|
@ -754,6 +756,7 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de
|
|||
$admin_abuseUserReports,
|
||||
$admin_accounts_create,
|
||||
$admin_accounts_delete,
|
||||
$admin_accounts_findByEmail,
|
||||
$admin_ad_create,
|
||||
$admin_ad_delete,
|
||||
$admin_ad_list,
|
||||
|
@ -1122,6 +1125,7 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de
|
|||
$admin_abuseUserReports,
|
||||
$admin_accounts_create,
|
||||
$admin_accounts_delete,
|
||||
$admin_accounts_findByEmail,
|
||||
$admin_ad_create,
|
||||
$admin_ad_delete,
|
||||
$admin_ad_list,
|
||||
|
|
|
@ -10,6 +10,7 @@ import * as ep___admin_meta from './endpoints/admin/meta.js';
|
|||
import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js';
|
||||
import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js';
|
||||
import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js';
|
||||
import * as ep___admin_accounts_findByEmail from './endpoints/admin/accounts/find-by-email.js';
|
||||
import * as ep___admin_ad_create from './endpoints/admin/ad/create.js';
|
||||
import * as ep___admin_ad_delete from './endpoints/admin/ad/delete.js';
|
||||
import * as ep___admin_ad_list from './endpoints/admin/ad/list.js';
|
||||
|
@ -378,6 +379,7 @@ const eps = [
|
|||
['admin/abuse-user-reports', ep___admin_abuseUserReports],
|
||||
['admin/accounts/create', ep___admin_accounts_create],
|
||||
['admin/accounts/delete', ep___admin_accounts_delete],
|
||||
['admin/accounts/find-by-email', ep___admin_accounts_findByEmail],
|
||||
['admin/ad/create', ep___admin_ad_create],
|
||||
['admin/ad/delete', ep___admin_ad_delete],
|
||||
['admin/ad/list', ep___admin_ad_list],
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { UserProfilesRepository } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ApiError } from '@/server/api/error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireAdmin: true,
|
||||
|
||||
errors: {
|
||||
userNotFound: {
|
||||
message: 'No such user who has the email address.',
|
||||
code: 'USER_NOT_FOUND',
|
||||
id: 'cb865949-8af5-4062-a88c-ef55e8786d1d',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
email: { type: 'string' },
|
||||
},
|
||||
required: ['email'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const profile = await this.userProfilesRepository.findOne({
|
||||
where: { email: ps.email },
|
||||
relations: ['user'],
|
||||
});
|
||||
|
||||
if (profile == null) {
|
||||
throw new ApiError(meta.errors.userNotFound);
|
||||
}
|
||||
|
||||
const res = await this.userEntityService.pack(profile.user!, null, {
|
||||
detail: true,
|
||||
});
|
||||
|
||||
return res;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -307,6 +307,10 @@ export const meta = {
|
|||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
enableFanoutTimelineDbFallback: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
perLocalUserUserTimelineCacheMax: {
|
||||
type: 'number',
|
||||
optional: false, nullable: false,
|
||||
|
@ -440,6 +444,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
policies: { ...DEFAULT_POLICIES, ...instance.policies },
|
||||
manifestJsonOverride: instance.manifestJsonOverride,
|
||||
enableFanoutTimeline: instance.enableFanoutTimeline,
|
||||
enableFanoutTimelineDbFallback: instance.enableFanoutTimelineDbFallback,
|
||||
perLocalUserUserTimelineCacheMax: instance.perLocalUserUserTimelineCacheMax,
|
||||
perRemoteUserUserTimelineCacheMax: instance.perRemoteUserUserTimelineCacheMax,
|
||||
perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax,
|
||||
|
|
|
@ -125,6 +125,7 @@ export const paramDef = {
|
|||
preservedUsernames: { type: 'array', items: { type: 'string' } },
|
||||
manifestJsonOverride: { type: 'string' },
|
||||
enableFanoutTimeline: { type: 'boolean' },
|
||||
enableFanoutTimelineDbFallback: { type: 'boolean' },
|
||||
perLocalUserUserTimelineCacheMax: { type: 'integer' },
|
||||
perRemoteUserUserTimelineCacheMax: { type: 'integer' },
|
||||
perUserHomeTimelineCacheMax: { type: 'integer' },
|
||||
|
@ -489,6 +490,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
set.enableFanoutTimeline = ps.enableFanoutTimeline;
|
||||
}
|
||||
|
||||
if (ps.enableFanoutTimelineDbFallback !== undefined) {
|
||||
set.enableFanoutTimelineDbFallback = ps.enableFanoutTimelineDbFallback;
|
||||
}
|
||||
|
||||
if (ps.perLocalUserUserTimelineCacheMax !== undefined) {
|
||||
set.perLocalUserUserTimelineCacheMax = ps.perLocalUserUserTimelineCacheMax;
|
||||
}
|
||||
|
|
|
@ -411,16 +411,26 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
const newName = updates.name === undefined ? user.name : updates.name;
|
||||
const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description;
|
||||
const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields;
|
||||
|
||||
if (newName != null) {
|
||||
const tokens = mfm.parseSimple(newName);
|
||||
emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!));
|
||||
emojis = emojis.concat(extractCustomEmojisFromMfm(tokens));
|
||||
}
|
||||
|
||||
if (newDescription != null) {
|
||||
const tokens = mfm.parse(newDescription);
|
||||
emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!));
|
||||
tags = extractHashtags(tokens!).map(tag => normalizeForSearch(tag)).splice(0, 32);
|
||||
emojis = emojis.concat(extractCustomEmojisFromMfm(tokens));
|
||||
tags = extractHashtags(tokens).map(tag => normalizeForSearch(tag)).splice(0, 32);
|
||||
}
|
||||
|
||||
for (const field of newFields) {
|
||||
const nameTokens = mfm.parseSimple(field.name);
|
||||
const valueTokens = mfm.parseSimple(field.value);
|
||||
emojis = emojis.concat([
|
||||
...extractCustomEmojisFromMfm(nameTokens),
|
||||
...extractCustomEmojisFromMfm(valueTokens),
|
||||
]);
|
||||
}
|
||||
|
||||
updates.emojis = emojis;
|
||||
|
|
|
@ -70,6 +70,12 @@ export const meta = {
|
|||
id: '749ee0f6-d3da-459a-bf02-282e2da4292c',
|
||||
},
|
||||
|
||||
cannotReplyToInvisibleNote: {
|
||||
message: 'You cannot reply to an invisible Note.',
|
||||
code: 'CANNOT_REPLY_TO_AN_INVISIBLE_NOTE',
|
||||
id: 'b98980fa-3780-406c-a935-b6d0eeee10d1',
|
||||
},
|
||||
|
||||
cannotReplyToPureRenote: {
|
||||
message: 'You can not reply to a pure Renote.',
|
||||
code: 'CANNOT_REPLY_TO_A_PURE_RENOTE',
|
||||
|
@ -276,6 +282,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw new ApiError(meta.errors.noSuchReplyTarget);
|
||||
} else if (isPureRenote(reply)) {
|
||||
throw new ApiError(meta.errors.cannotReplyToPureRenote);
|
||||
} else if (!await this.noteEntityService.isVisibleForMe(reply, me.id)) {
|
||||
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
|
||||
}
|
||||
|
||||
// Check blocking
|
||||
|
|
|
@ -94,101 +94,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
const serverSettings = await this.metaService.fetch();
|
||||
|
||||
if (serverSettings.enableFanoutTimeline) {
|
||||
const [
|
||||
userIdsWhoMeMuting,
|
||||
userIdsWhoMeMutingRenotes,
|
||||
userIdsWhoBlockingMe,
|
||||
] = await Promise.all([
|
||||
this.cacheService.userMutingsCache.fetch(me.id),
|
||||
this.cacheService.renoteMutingsCache.fetch(me.id),
|
||||
this.cacheService.userBlockedCache.fetch(me.id),
|
||||
]);
|
||||
|
||||
let noteIds: string[];
|
||||
let shouldFallbackToDb = false;
|
||||
|
||||
if (ps.withFiles) {
|
||||
const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([
|
||||
`homeTimelineWithFiles:${me.id}`,
|
||||
'localTimelineWithFiles',
|
||||
], untilId, sinceId);
|
||||
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds]));
|
||||
} else if (ps.withReplies) {
|
||||
const [htlNoteIds, ltlNoteIds, ltlReplyNoteIds] = await this.funoutTimelineService.getMulti([
|
||||
`homeTimeline:${me.id}`,
|
||||
'localTimeline',
|
||||
'localTimelineWithReplies',
|
||||
], untilId, sinceId);
|
||||
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds, ...ltlReplyNoteIds]));
|
||||
} else {
|
||||
const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([
|
||||
`homeTimeline:${me.id}`,
|
||||
'localTimeline',
|
||||
], untilId, sinceId);
|
||||
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds]));
|
||||
shouldFallbackToDb = htlNoteIds.length === 0;
|
||||
}
|
||||
|
||||
noteIds.sort((a, b) => a > b ? -1 : 1);
|
||||
noteIds = noteIds.slice(0, ps.limit);
|
||||
|
||||
shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0);
|
||||
|
||||
let redisTimeline: MiNote[] = [];
|
||||
|
||||
if (!shouldFallbackToDb) {
|
||||
const query = this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
redisTimeline = await query.getMany();
|
||||
|
||||
redisTimeline = redisTimeline.filter(note => {
|
||||
if (note.userId === me.id) {
|
||||
return true;
|
||||
}
|
||||
if (isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
||||
if (isUserRelated(note, userIdsWhoMeMuting)) return false;
|
||||
if (note.renoteId) {
|
||||
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
|
||||
if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
|
||||
if (ps.withRenotes === false) return false;
|
||||
}
|
||||
}
|
||||
if (!ps.withBots && note.user?.isBot) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
}
|
||||
|
||||
if (redisTimeline.length > 0) {
|
||||
process.nextTick(() => {
|
||||
this.activeUsersChart.read(me);
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(redisTimeline, me);
|
||||
} else { // fallback to db
|
||||
return await this.getFromDb({
|
||||
untilId,
|
||||
sinceId,
|
||||
limit: ps.limit,
|
||||
includeMyRenotes: ps.includeMyRenotes,
|
||||
includeRenotedMyNotes: ps.includeRenotedMyNotes,
|
||||
includeLocalRenotes: ps.includeLocalRenotes,
|
||||
withFiles: ps.withFiles,
|
||||
withReplies: ps.withReplies,
|
||||
withBots: ps.withBots,
|
||||
}, me);
|
||||
}
|
||||
} else {
|
||||
if (!serverSettings.enableFanoutTimeline) {
|
||||
return await this.getFromDb({
|
||||
untilId,
|
||||
sinceId,
|
||||
|
@ -201,6 +107,105 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
withBots: ps.withBots,
|
||||
}, me);
|
||||
}
|
||||
|
||||
const [
|
||||
userIdsWhoMeMuting,
|
||||
userIdsWhoMeMutingRenotes,
|
||||
userIdsWhoBlockingMe,
|
||||
] = await Promise.all([
|
||||
this.cacheService.userMutingsCache.fetch(me.id),
|
||||
this.cacheService.renoteMutingsCache.fetch(me.id),
|
||||
this.cacheService.userBlockedCache.fetch(me.id),
|
||||
]);
|
||||
|
||||
let noteIds: string[];
|
||||
let shouldFallbackToDb = false;
|
||||
|
||||
if (ps.withFiles) {
|
||||
const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([
|
||||
`homeTimelineWithFiles:${me.id}`,
|
||||
'localTimelineWithFiles',
|
||||
], untilId, sinceId);
|
||||
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds]));
|
||||
} else if (ps.withReplies) {
|
||||
const [htlNoteIds, ltlNoteIds, ltlReplyNoteIds] = await this.funoutTimelineService.getMulti([
|
||||
`homeTimeline:${me.id}`,
|
||||
'localTimeline',
|
||||
'localTimelineWithReplies',
|
||||
], untilId, sinceId);
|
||||
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds, ...ltlReplyNoteIds]));
|
||||
} else {
|
||||
const [htlNoteIds, ltlNoteIds] = await this.funoutTimelineService.getMulti([
|
||||
`homeTimeline:${me.id}`,
|
||||
'localTimeline',
|
||||
], untilId, sinceId);
|
||||
noteIds = Array.from(new Set([...htlNoteIds, ...ltlNoteIds]));
|
||||
shouldFallbackToDb = htlNoteIds.length === 0;
|
||||
}
|
||||
|
||||
noteIds.sort((a, b) => a > b ? -1 : 1);
|
||||
noteIds = noteIds.slice(0, ps.limit);
|
||||
|
||||
shouldFallbackToDb = shouldFallbackToDb || (noteIds.length === 0);
|
||||
|
||||
let redisTimeline: MiNote[] = [];
|
||||
|
||||
if (!shouldFallbackToDb) {
|
||||
const query = this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
redisTimeline = await query.getMany();
|
||||
|
||||
redisTimeline = redisTimeline.filter(note => {
|
||||
if (note.userId === me.id) {
|
||||
return true;
|
||||
}
|
||||
if (isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
||||
if (isUserRelated(note, userIdsWhoMeMuting)) return false;
|
||||
if (note.renoteId) {
|
||||
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
|
||||
if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
|
||||
if (ps.withRenotes === false) return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ps.withBots && note.user?.isBot) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
}
|
||||
|
||||
if (redisTimeline.length > 0) {
|
||||
process.nextTick(() => {
|
||||
this.activeUsersChart.read(me);
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(redisTimeline, me);
|
||||
} else {
|
||||
if (serverSettings.enableFanoutTimelineDbFallback) { // fallback to db
|
||||
return await this.getFromDb({
|
||||
untilId,
|
||||
sinceId,
|
||||
limit: ps.limit,
|
||||
includeMyRenotes: ps.includeMyRenotes,
|
||||
includeRenotedMyNotes: ps.includeRenotedMyNotes,
|
||||
includeLocalRenotes: ps.includeLocalRenotes,
|
||||
withFiles: ps.withFiles,
|
||||
withReplies: ps.withReplies,
|
||||
withBots: ps.withBots,
|
||||
}, me);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -85,86 +85,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
const serverSettings = await this.metaService.fetch();
|
||||
|
||||
if (serverSettings.enableFanoutTimeline) {
|
||||
const [
|
||||
userIdsWhoMeMuting,
|
||||
userIdsWhoMeMutingRenotes,
|
||||
userIdsWhoBlockingMe,
|
||||
] = me ? await Promise.all([
|
||||
this.cacheService.userMutingsCache.fetch(me.id),
|
||||
this.cacheService.renoteMutingsCache.fetch(me.id),
|
||||
this.cacheService.userBlockedCache.fetch(me.id),
|
||||
]) : [new Set<string>(), new Set<string>(), new Set<string>()];
|
||||
|
||||
let noteIds: string[];
|
||||
|
||||
if (ps.withFiles) {
|
||||
noteIds = await this.funoutTimelineService.get('localTimelineWithFiles', untilId, sinceId);
|
||||
} else {
|
||||
const [nonReplyNoteIds, replyNoteIds] = await this.funoutTimelineService.getMulti([
|
||||
'localTimeline',
|
||||
'localTimelineWithReplies',
|
||||
], untilId, sinceId);
|
||||
noteIds = Array.from(new Set([...nonReplyNoteIds, ...replyNoteIds]));
|
||||
noteIds.sort((a, b) => a > b ? -1 : 1);
|
||||
}
|
||||
|
||||
noteIds = noteIds.slice(0, ps.limit);
|
||||
|
||||
let redisTimeline: MiNote[] = [];
|
||||
|
||||
if (noteIds.length > 0) {
|
||||
const query = this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
redisTimeline = await query.getMany();
|
||||
|
||||
redisTimeline = redisTimeline.filter(note => {
|
||||
if (me && (note.userId === me.id)) {
|
||||
return true;
|
||||
}
|
||||
if (!ps.withReplies && note.replyId && note.replyUserId !== note.userId && (me == null || note.replyUserId !== me.id)) return false;
|
||||
if (!ps.withBots && note.user?.isBot) return false;
|
||||
if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
||||
if (me && isUserRelated(note, userIdsWhoMeMuting)) return false;
|
||||
if (note.renoteId) {
|
||||
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
|
||||
if (me && isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
|
||||
if (ps.withRenotes === false) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
}
|
||||
|
||||
if (redisTimeline.length > 0) {
|
||||
process.nextTick(() => {
|
||||
if (me) {
|
||||
this.activeUsersChart.read(me);
|
||||
}
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(redisTimeline, me);
|
||||
} else { // fallback to db
|
||||
return await this.getFromDb({
|
||||
untilId,
|
||||
sinceId,
|
||||
limit: ps.limit,
|
||||
withFiles: ps.withFiles,
|
||||
withReplies: ps.withReplies,
|
||||
withBots: ps.withBots,
|
||||
}, me);
|
||||
}
|
||||
} else {
|
||||
if (!serverSettings.enableFanoutTimeline) {
|
||||
return await this.getFromDb({
|
||||
untilId,
|
||||
sinceId,
|
||||
|
@ -174,6 +95,89 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
withBots: ps.withBots,
|
||||
}, me);
|
||||
}
|
||||
|
||||
const [
|
||||
userIdsWhoMeMuting,
|
||||
userIdsWhoMeMutingRenotes,
|
||||
userIdsWhoBlockingMe,
|
||||
] = me ? await Promise.all([
|
||||
this.cacheService.userMutingsCache.fetch(me.id),
|
||||
this.cacheService.renoteMutingsCache.fetch(me.id),
|
||||
this.cacheService.userBlockedCache.fetch(me.id),
|
||||
]) : [new Set<string>(), new Set<string>(), new Set<string>()];
|
||||
|
||||
let noteIds: string[];
|
||||
|
||||
if (ps.withFiles) {
|
||||
noteIds = await this.funoutTimelineService.get('localTimelineWithFiles', untilId, sinceId);
|
||||
} else {
|
||||
const [nonReplyNoteIds, replyNoteIds] = await this.funoutTimelineService.getMulti([
|
||||
'localTimeline',
|
||||
'localTimelineWithReplies',
|
||||
], untilId, sinceId);
|
||||
noteIds = Array.from(new Set([...nonReplyNoteIds, ...replyNoteIds]));
|
||||
noteIds.sort((a, b) => a > b ? -1 : 1);
|
||||
}
|
||||
|
||||
noteIds = noteIds.slice(0, ps.limit);
|
||||
|
||||
let redisTimeline: MiNote[] = [];
|
||||
|
||||
if (noteIds.length > 0) {
|
||||
const query = this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
redisTimeline = await query.getMany();
|
||||
|
||||
redisTimeline = redisTimeline.filter(note => {
|
||||
if (me && (note.userId === me.id)) {
|
||||
return true;
|
||||
}
|
||||
if (!ps.withReplies && note.replyId && note.replyUserId !== note.userId && (me == null || note.replyUserId !== me.id)) return false;
|
||||
if (!ps.withBots && note.user?.isBot) return false;
|
||||
if (me && isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
||||
if (me && isUserRelated(note, userIdsWhoMeMuting)) return false;
|
||||
if (note.renoteId) {
|
||||
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
|
||||
if (me && isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
|
||||
if (ps.withRenotes === false) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
}
|
||||
|
||||
if (redisTimeline.length > 0) {
|
||||
process.nextTick(() => {
|
||||
if (me) {
|
||||
this.activeUsersChart.read(me);
|
||||
}
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(redisTimeline, me);
|
||||
} else {
|
||||
if (serverSettings.enableFanoutTimelineDbFallback) { // fallback to db
|
||||
return await this.getFromDb({
|
||||
untilId,
|
||||
sinceId,
|
||||
limit: ps.limit,
|
||||
withFiles: ps.withFiles,
|
||||
withReplies: ps.withReplies,
|
||||
withBots: ps.withBots,
|
||||
}, me);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -187,7 +191,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
}, me: MiLocalUser | null) {
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
|
||||
ps.sinceId, ps.untilId)
|
||||
.andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)')
|
||||
.andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL) AND (note.channelId IS NULL)')
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
|
|
|
@ -77,79 +77,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
const serverSettings = await this.metaService.fetch();
|
||||
|
||||
if (serverSettings.enableFanoutTimeline) {
|
||||
const [
|
||||
followings,
|
||||
userIdsWhoMeMuting,
|
||||
userIdsWhoMeMutingRenotes,
|
||||
userIdsWhoBlockingMe,
|
||||
] = await Promise.all([
|
||||
this.cacheService.userFollowingsCache.fetch(me.id),
|
||||
this.cacheService.userMutingsCache.fetch(me.id),
|
||||
this.cacheService.renoteMutingsCache.fetch(me.id),
|
||||
this.cacheService.userBlockedCache.fetch(me.id),
|
||||
]);
|
||||
|
||||
let noteIds = await this.funoutTimelineService.get(ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`, untilId, sinceId);
|
||||
noteIds = noteIds.slice(0, ps.limit);
|
||||
|
||||
let redisTimeline: MiNote[] = [];
|
||||
|
||||
if (noteIds.length > 0) {
|
||||
const query = this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
redisTimeline = await query.getMany();
|
||||
|
||||
redisTimeline = redisTimeline.filter(note => {
|
||||
if (note.userId === me.id) {
|
||||
return true;
|
||||
}
|
||||
if (isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
||||
if (isUserRelated(note, userIdsWhoMeMuting)) return false;
|
||||
if (note.renoteId) {
|
||||
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
|
||||
if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
|
||||
if (ps.withRenotes === false) return false;
|
||||
}
|
||||
}
|
||||
if (note.reply && note.reply.visibility === 'followers') {
|
||||
if (!Object.hasOwn(followings, note.reply.userId)) return false;
|
||||
}
|
||||
if (!ps.withBots && note.user?.isBot) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
}
|
||||
|
||||
if (redisTimeline.length > 0) {
|
||||
process.nextTick(() => {
|
||||
this.activeUsersChart.read(me);
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(redisTimeline, me);
|
||||
} else { // fallback to db
|
||||
return await this.getFromDb({
|
||||
untilId,
|
||||
sinceId,
|
||||
limit: ps.limit,
|
||||
includeMyRenotes: ps.includeMyRenotes,
|
||||
includeRenotedMyNotes: ps.includeRenotedMyNotes,
|
||||
includeLocalRenotes: ps.includeLocalRenotes,
|
||||
withFiles: ps.withFiles,
|
||||
withRenotes: ps.withRenotes,
|
||||
withBots: ps.withBots,
|
||||
}, me);
|
||||
}
|
||||
} else {
|
||||
if (!serverSettings.enableFanoutTimeline) {
|
||||
return await this.getFromDb({
|
||||
untilId,
|
||||
sinceId,
|
||||
|
@ -162,6 +90,82 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
withBots: ps.withBots,
|
||||
}, me);
|
||||
}
|
||||
|
||||
const [
|
||||
followings,
|
||||
userIdsWhoMeMuting,
|
||||
userIdsWhoMeMutingRenotes,
|
||||
userIdsWhoBlockingMe,
|
||||
] = await Promise.all([
|
||||
this.cacheService.userFollowingsCache.fetch(me.id),
|
||||
this.cacheService.userMutingsCache.fetch(me.id),
|
||||
this.cacheService.renoteMutingsCache.fetch(me.id),
|
||||
this.cacheService.userBlockedCache.fetch(me.id),
|
||||
]);
|
||||
|
||||
let noteIds = await this.funoutTimelineService.get(ps.withFiles ? `homeTimelineWithFiles:${me.id}` : `homeTimeline:${me.id}`, untilId, sinceId);
|
||||
noteIds = noteIds.slice(0, ps.limit);
|
||||
|
||||
let redisTimeline: MiNote[] = [];
|
||||
|
||||
if (noteIds.length > 0) {
|
||||
const query = this.notesRepository.createQueryBuilder('note')
|
||||
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('note.channel', 'channel');
|
||||
|
||||
redisTimeline = await query.getMany();
|
||||
|
||||
redisTimeline = redisTimeline.filter(note => {
|
||||
if (note.userId === me.id) {
|
||||
return true;
|
||||
}
|
||||
if (isUserRelated(note, userIdsWhoBlockingMe)) return false;
|
||||
if (isUserRelated(note, userIdsWhoMeMuting)) return false;
|
||||
if (note.renoteId) {
|
||||
if (note.text == null && note.fileIds.length === 0 && !note.hasPoll) {
|
||||
if (isUserRelated(note, userIdsWhoMeMutingRenotes)) return false;
|
||||
if (ps.withRenotes === false) return false;
|
||||
}
|
||||
}
|
||||
if (note.reply && note.reply.visibility === 'followers') {
|
||||
if (!Object.hasOwn(followings, note.reply.userId)) return false;
|
||||
}
|
||||
if (!ps.withBots && note.user?.isBot) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
redisTimeline.sort((a, b) => a.id > b.id ? -1 : 1);
|
||||
}
|
||||
|
||||
if (redisTimeline.length > 0) {
|
||||
process.nextTick(() => {
|
||||
this.activeUsersChart.read(me);
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(redisTimeline, me);
|
||||
} else {
|
||||
if (serverSettings.enableFanoutTimelineDbFallback) { // fallback to db
|
||||
return await this.getFromDb({
|
||||
untilId,
|
||||
sinceId,
|
||||
limit: ps.limit,
|
||||
includeMyRenotes: ps.includeMyRenotes,
|
||||
includeRenotedMyNotes: ps.includeRenotedMyNotes,
|
||||
includeLocalRenotes: ps.includeLocalRenotes,
|
||||
withFiles: ps.withFiles,
|
||||
withRenotes: ps.withRenotes,
|
||||
withBots: ps.withBots,
|
||||
}, me);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { MiNote, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
|
||||
import { Brackets } from 'typeorm';
|
||||
import type { MiNote, MiUserList, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||
|
@ -14,8 +15,9 @@ import { IdService } from '@/core/IdService.js';
|
|||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { FunoutTimelineService } from '@/core/FunoutTimelineService.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { MiLocalUser } from '@/models/User.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { Brackets } from 'typeorm';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes', 'lists'],
|
||||
|
@ -81,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
private idService: IdService,
|
||||
private funoutTimelineService: FunoutTimelineService,
|
||||
private queryService: QueryService,
|
||||
|
||||
private metaService: MetaService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
|
||||
|
@ -96,6 +98,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw new ApiError(meta.errors.noSuchList);
|
||||
}
|
||||
|
||||
const serverSettings = await this.metaService.fetch();
|
||||
|
||||
if (!serverSettings.enableFanoutTimeline) {
|
||||
return await this.getFromDb(list, {
|
||||
untilId,
|
||||
sinceId,
|
||||
limit: ps.limit,
|
||||
includeMyRenotes: ps.includeMyRenotes,
|
||||
includeRenotedMyNotes: ps.includeRenotedMyNotes,
|
||||
includeLocalRenotes: ps.includeLocalRenotes,
|
||||
withFiles: ps.withFiles,
|
||||
withRenotes: ps.withRenotes,
|
||||
}, me);
|
||||
}
|
||||
|
||||
const [
|
||||
userIdsWhoMeMuting,
|
||||
userIdsWhoMeMutingRenotes,
|
||||
|
@ -145,93 +162,119 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
if (redisTimeline.length > 0) {
|
||||
this.activeUsersChart.read(me);
|
||||
return await this.noteEntityService.packMany(redisTimeline, me);
|
||||
} else { // fallback to db
|
||||
//#region Construct query
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
.innerJoin(this.userListMembershipsRepository.metadata.targetName, 'userListMemberships', 'userListMemberships.userId = note.userId')
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.andWhere('userListMemberships.userListId = :userListId', { userListId: list.id })
|
||||
.andWhere('note.channelId IS NULL') // チャンネルノートではない
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('note.replyId IS NULL') // 返信ではない
|
||||
.orWhere(new Brackets(qb => {
|
||||
qb // 返信だけど投稿者自身への返信
|
||||
.where('note.replyId IS NOT NULL')
|
||||
.andWhere('note.replyUserId = note.userId');
|
||||
}))
|
||||
.orWhere(new Brackets(qb => {
|
||||
qb // 返信だけど自分宛ての返信
|
||||
.where('note.replyId IS NOT NULL')
|
||||
.andWhere('note.replyUserId = :meId', { meId: me.id });
|
||||
}))
|
||||
.orWhere(new Brackets(qb => {
|
||||
qb // 返信だけどwithRepliesがtrueの場合
|
||||
.where('note.replyId IS NOT NULL')
|
||||
.andWhere('userListMemberships.withReplies = true');
|
||||
}));
|
||||
}));
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
|
||||
if (ps.includeMyRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.userId != :meId', { meId: me.id });
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
} else {
|
||||
if (serverSettings.enableFanoutTimelineDbFallback) { // fallback to db
|
||||
return await this.getFromDb(list, {
|
||||
untilId,
|
||||
sinceId,
|
||||
limit: ps.limit,
|
||||
includeMyRenotes: ps.includeMyRenotes,
|
||||
includeRenotedMyNotes: ps.includeRenotedMyNotes,
|
||||
includeLocalRenotes: ps.includeLocalRenotes,
|
||||
withFiles: ps.withFiles,
|
||||
withRenotes: ps.withRenotes,
|
||||
}, me);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (ps.includeRenotedMyNotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteUserId != :meId', { meId: me.id });
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.includeLocalRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteUserHost IS NOT NULL');
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.withRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const timeline = await query.limit(ps.limit).getMany();
|
||||
|
||||
this.activeUsersChart.read(me);
|
||||
|
||||
return await this.noteEntityService.packMany(timeline, me);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async getFromDb(list: MiUserList, ps: {
|
||||
untilId: string | null,
|
||||
sinceId: string | null,
|
||||
limit: number,
|
||||
includeMyRenotes: boolean,
|
||||
includeRenotedMyNotes: boolean,
|
||||
includeLocalRenotes: boolean,
|
||||
withFiles: boolean,
|
||||
withRenotes: boolean,
|
||||
}, me: MiLocalUser) {
|
||||
//#region Construct query
|
||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
.innerJoin(this.userListMembershipsRepository.metadata.targetName, 'userListMemberships', 'userListMemberships.userId = note.userId')
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
.leftJoinAndSelect('note.renote', 'renote')
|
||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.andWhere('userListMemberships.userListId = :userListId', { userListId: list.id })
|
||||
.andWhere('note.channelId IS NULL') // チャンネルノートではない
|
||||
.andWhere(new Brackets(qb => {
|
||||
qb
|
||||
.where('note.replyId IS NULL') // 返信ではない
|
||||
.orWhere(new Brackets(qb => {
|
||||
qb // 返信だけど投稿者自身への返信
|
||||
.where('note.replyId IS NOT NULL')
|
||||
.andWhere('note.replyUserId = note.userId');
|
||||
}))
|
||||
.orWhere(new Brackets(qb => {
|
||||
qb // 返信だけど自分宛ての返信
|
||||
.where('note.replyId IS NOT NULL')
|
||||
.andWhere('note.replyUserId = :meId', { meId: me.id });
|
||||
}))
|
||||
.orWhere(new Brackets(qb => {
|
||||
qb // 返信だけどwithRepliesがtrueの場合
|
||||
.where('note.replyId IS NOT NULL')
|
||||
.andWhere('userListMemberships.withReplies = true');
|
||||
}));
|
||||
}));
|
||||
|
||||
this.queryService.generateVisibilityQuery(query, me);
|
||||
this.queryService.generateMutedUserQuery(query, me);
|
||||
this.queryService.generateBlockedUserQuery(query, me);
|
||||
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
|
||||
|
||||
if (ps.includeMyRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.userId != :meId', { meId: me.id });
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.includeRenotedMyNotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteUserId != :meId', { meId: me.id });
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.includeLocalRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteUserHost IS NOT NULL');
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
qb.orWhere('0 < (SELECT COUNT(*) FROM poll WHERE poll."noteId" = note.id)');
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.withRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
qb.orWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.text IS NOT NULL');
|
||||
qb.orWhere('note.fileIds != \'{}\'');
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
if (ps.withFiles) {
|
||||
query.andWhere('note.fileIds != \'{}\'');
|
||||
}
|
||||
//#endregion
|
||||
|
||||
const timeline = await query.limit(ps.limit).getMany();
|
||||
|
||||
this.activeUsersChart.read(me);
|
||||
|
||||
return await this.noteEntityService.packMany(timeline, me);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,7 +51,6 @@ export const paramDef = {
|
|||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
sinceDate: { type: 'integer' },
|
||||
untilDate: { type: 'integer' },
|
||||
includeMyRenotes: { type: 'boolean', default: true },
|
||||
withFiles: { type: 'boolean', default: false },
|
||||
excludeNsfw: { type: 'boolean', default: false },
|
||||
},
|
||||
|
@ -172,7 +171,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
query.andWhere('note.fileIds != \'{}\'');
|
||||
}
|
||||
|
||||
if (ps.includeMyRenotes === false) {
|
||||
if (ps.withRenotes === false) {
|
||||
query.andWhere(new Brackets(qb => {
|
||||
qb.orWhere('note.userId != :userId', { userId: ps.userId });
|
||||
qb.orWhere('note.renoteId IS NULL');
|
||||
|
|
|
@ -55,7 +55,7 @@ class LocalTimelineChannel extends Channel {
|
|||
|
||||
if (note.user.host !== null) return;
|
||||
if (note.visibility !== 'public') return;
|
||||
if (note.channelId != null && !this.followingChannels.has(note.channelId)) return;
|
||||
if (note.channelId != null) return;
|
||||
|
||||
// 関係ない返信は除外
|
||||
if (note.reply && this.user && !this.following[note.userId]?.withReplies && !this.withReplies) {
|
||||
|
|
|
@ -175,6 +175,7 @@ export class ClientServerService {
|
|||
serverErrorImageUrl: meta.serverErrorImageUrl ?? 'https://launcher.moe/error.png',
|
||||
infoImageUrl: meta.infoImageUrl ?? 'https://launcher.moe/nothinghere.png',
|
||||
notFoundImageUrl: meta.notFoundImageUrl ?? 'https://launcher.moe/missingpage.webp',
|
||||
instanceUrl: this.config.url,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ html
|
|||
meta(name='theme-color' content= themeColor || '#86b300')
|
||||
meta(name='theme-color-orig' content= themeColor || '#86b300')
|
||||
meta(property='og:site_name' content= instanceName || 'Sharkey')
|
||||
meta(property='instance_url' content= instanceUrl)
|
||||
meta(name='viewport' content='width=device-width, initial-scale=1')
|
||||
link(rel='icon' href= icon || '/favicon.ico')
|
||||
link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png')
|
||||
|
|
|
@ -152,6 +152,7 @@ describe('Timelines', () => {
|
|||
|
||||
await api('/following/create', { userId: bob.id }, alice);
|
||||
await api('/following/create', { userId: carol.id }, alice);
|
||||
await api('/following/create', { userId: carol.id }, bob);
|
||||
await api('/following/update', { userId: bob.id, withReplies: true }, alice);
|
||||
await sleep(1000);
|
||||
const carolNote = await post(carol, { text: 'hi', visibility: 'followers' });
|
||||
|
|
|
@ -94,6 +94,7 @@ describe('ActivityPub', () => {
|
|||
cacheRemoteFiles: true,
|
||||
cacheRemoteSensitiveFiles: true,
|
||||
enableFanoutTimeline: true,
|
||||
enableFanoutTimelineDbFallback: true,
|
||||
perUserHomeTimelineCacheMax: 800,
|
||||
perLocalUserUserTimelineCacheMax: 800,
|
||||
perRemoteUserUserTimelineCacheMax: 800,
|
||||
|
|
|
@ -24,12 +24,12 @@
|
|||
"@rollup/pluginutils": "5.0.5",
|
||||
"@syuilo/aiscript": "0.16.0",
|
||||
"@phosphor-icons/web": "^2.0.3",
|
||||
"@vitejs/plugin-vue": "4.4.0",
|
||||
"@vue-macros/reactivity-transform": "0.3.23",
|
||||
"@vue/compiler-sfc": "3.3.7",
|
||||
"@vitejs/plugin-vue": "4.5.0",
|
||||
"@vue-macros/reactivity-transform": "0.4.0",
|
||||
"@vue/compiler-sfc": "3.3.8",
|
||||
"astring": "1.8.6",
|
||||
"autosize": "6.0.1",
|
||||
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.0.5",
|
||||
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.0.6",
|
||||
"broadcast-channel": "6.0.0",
|
||||
"browser-image-resizer": "github:misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
|
||||
"buraha": "0.0.1",
|
||||
|
@ -39,7 +39,7 @@
|
|||
"chartjs-chart-matrix": "2.0.1",
|
||||
"chartjs-plugin-gradient": "0.6.1",
|
||||
"chartjs-plugin-zoom": "2.0.1",
|
||||
"chromatic": "7.6.0",
|
||||
"chromatic": "9.0.0",
|
||||
"compare-versions": "6.1.0",
|
||||
"cropperjs": "2.0.0-beta.4",
|
||||
"date-fns": "2.30.0",
|
||||
|
@ -57,7 +57,7 @@
|
|||
"photoswipe": "5.4.2",
|
||||
"punycode": "2.3.1",
|
||||
"querystring": "0.2.1",
|
||||
"rollup": "4.2.0",
|
||||
"rollup": "4.4.1",
|
||||
"sanitize-html": "2.11.0",
|
||||
"shiki": "^0.14.5",
|
||||
"sass": "1.69.5",
|
||||
|
@ -74,62 +74,62 @@
|
|||
"v-code-diff": "1.7.2",
|
||||
"vanilla-tilt": "1.8.1",
|
||||
"vite": "4.5.0",
|
||||
"vue": "3.3.7",
|
||||
"vue": "3.3.8",
|
||||
"vuedraggable": "next"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-actions": "7.5.2",
|
||||
"@storybook/addon-essentials": "7.5.2",
|
||||
"@storybook/addon-interactions": "7.5.2",
|
||||
"@storybook/addon-links": "7.5.2",
|
||||
"@storybook/addon-storysource": "7.5.2",
|
||||
"@storybook/addons": "7.5.2",
|
||||
"@storybook/blocks": "7.5.2",
|
||||
"@storybook/core-events": "7.5.2",
|
||||
"@storybook/addon-actions": "7.5.3",
|
||||
"@storybook/addon-essentials": "7.5.3",
|
||||
"@storybook/addon-interactions": "7.5.3",
|
||||
"@storybook/addon-links": "7.5.3",
|
||||
"@storybook/addon-storysource": "7.5.3",
|
||||
"@storybook/addons": "7.5.3",
|
||||
"@storybook/blocks": "7.5.3",
|
||||
"@storybook/core-events": "7.5.3",
|
||||
"@storybook/jest": "0.2.3",
|
||||
"@storybook/manager-api": "7.5.2",
|
||||
"@storybook/preview-api": "7.5.2",
|
||||
"@storybook/react": "7.5.2",
|
||||
"@storybook/react-vite": "7.5.2",
|
||||
"@storybook/manager-api": "7.5.3",
|
||||
"@storybook/preview-api": "7.5.3",
|
||||
"@storybook/react": "7.5.3",
|
||||
"@storybook/react-vite": "7.5.3",
|
||||
"@storybook/testing-library": "0.2.2",
|
||||
"@storybook/theming": "7.5.2",
|
||||
"@storybook/types": "7.5.2",
|
||||
"@storybook/vue3": "7.5.2",
|
||||
"@storybook/vue3-vite": "7.5.2",
|
||||
"@storybook/theming": "7.5.3",
|
||||
"@storybook/types": "7.5.3",
|
||||
"@storybook/vue3": "7.5.3",
|
||||
"@storybook/vue3-vite": "7.5.3",
|
||||
"@testing-library/vue": "8.0.0",
|
||||
"@types/escape-regexp": "0.0.2",
|
||||
"@types/estree": "1.0.4",
|
||||
"@types/matter-js": "0.19.2",
|
||||
"@types/micromatch": "4.0.4",
|
||||
"@types/node": "20.8.10",
|
||||
"@types/punycode": "2.1.1",
|
||||
"@types/sanitize-html": "2.9.3",
|
||||
"@types/throttle-debounce": "5.0.1",
|
||||
"@types/tinycolor2": "1.4.5",
|
||||
"@types/uuid": "9.0.6",
|
||||
"@types/websocket": "1.0.8",
|
||||
"@types/ws": "8.5.8",
|
||||
"@typescript-eslint/eslint-plugin": "6.9.1",
|
||||
"@typescript-eslint/parser": "6.9.1",
|
||||
"@types/escape-regexp": "0.0.3",
|
||||
"@types/estree": "1.0.5",
|
||||
"@types/matter-js": "0.19.4",
|
||||
"@types/micromatch": "4.0.5",
|
||||
"@types/node": "20.9.1",
|
||||
"@types/punycode": "2.1.2",
|
||||
"@types/sanitize-html": "2.9.4",
|
||||
"@types/throttle-debounce": "5.0.2",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/uuid": "9.0.7",
|
||||
"@types/websocket": "1.0.9",
|
||||
"@types/ws": "8.5.9",
|
||||
"@typescript-eslint/eslint-plugin": "6.11.0",
|
||||
"@typescript-eslint/parser": "6.11.0",
|
||||
"@vitest/coverage-v8": "0.34.6",
|
||||
"@vue/runtime-core": "3.3.7",
|
||||
"@vue/runtime-core": "3.3.8",
|
||||
"acorn": "8.11.2",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "13.4.0",
|
||||
"eslint": "8.52.0",
|
||||
"cypress": "13.5.1",
|
||||
"eslint": "8.53.0",
|
||||
"eslint-plugin-import": "2.29.0",
|
||||
"eslint-plugin-vue": "9.18.1",
|
||||
"fast-glob": "3.3.1",
|
||||
"fast-glob": "3.3.2",
|
||||
"happy-dom": "10.0.3",
|
||||
"micromatch": "4.0.5",
|
||||
"msw": "1.3.2",
|
||||
"msw-storybook-addon": "1.10.0",
|
||||
"nodemon": "3.0.1",
|
||||
"prettier": "3.0.3",
|
||||
"prettier": "3.1.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"start-server-and-test": "2.0.1",
|
||||
"storybook": "7.5.2",
|
||||
"start-server-and-test": "2.0.3",
|
||||
"storybook": "7.5.3",
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"summaly": "github:misskey-dev/summaly",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
|
|
|
@ -8,7 +8,7 @@ import { common } from './common.js';
|
|||
import { version, ui, lang, updateLocale } from '@/config.js';
|
||||
import { i18n, updateI18n } from '@/i18n.js';
|
||||
import { confirm, alert, post, popup, toast } from '@/os.js';
|
||||
import { useStream, isReloading } from '@/stream.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
import * as sound from '@/scripts/sound.js';
|
||||
import { $i, refreshAccount, login, updateAccount, signout } from '@/account.js';
|
||||
import { defaultStore, ColdDeviceStorage } from '@/store.js';
|
||||
|
@ -39,7 +39,6 @@ export async function mainBoot() {
|
|||
|
||||
let reloadDialogShowing = false;
|
||||
stream.on('_disconnected_', async () => {
|
||||
if (isReloading) return;
|
||||
if (defaultStore.state.serverDisconnectedBehavior === 'reload') {
|
||||
location.reload();
|
||||
} else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
|
||||
|
@ -58,7 +57,7 @@ export async function mainBoot() {
|
|||
});
|
||||
|
||||
for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) {
|
||||
import('../plugin').then(async ({ install }) => {
|
||||
import('@/plugin.js').then(async ({ install }) => {
|
||||
// Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740
|
||||
await new Promise(r => setTimeout(r, 0));
|
||||
install(plugin);
|
||||
|
|
|
@ -45,12 +45,12 @@ import contains from '@/scripts/contains.js';
|
|||
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base.js';
|
||||
import { acct } from '@/filters/user.js';
|
||||
import * as os from '@/os.js';
|
||||
import { MFM_TAGS } from '@/scripts/mfm-tags.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { emojilist, getEmojiName } from '@/scripts/emojilist.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { customEmojis } from '@/custom-emojis.js';
|
||||
import { MFM_TAGS } from '@/const.js';
|
||||
|
||||
type EmojiDef = {
|
||||
emoji: string;
|
||||
|
|
|
@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<div v-if="renoteCollapsed" :class="$style.collapsedRenoteTarget">
|
||||
<MkAvatar :class="$style.collapsedRenoteTargetAvatar" :user="appearNote.user" link preview/>
|
||||
<Mfm :text="getNoteSummary(appearNote)" :plain="true" :nowrap="true" :author="appearNote.user" :nyaize="'account'" :class="$style.collapsedRenoteTargetText" @click="renoteCollapsed = false"/>
|
||||
<Mfm :text="getNoteSummary(appearNote)" :plain="true" :nowrap="true" :author="appearNote.user" :nyaize="'respect'" :class="$style.collapsedRenoteTargetText" @click="renoteCollapsed = false"/>
|
||||
</div>
|
||||
<article v-else :class="$style.article" @contextmenu.stop="onContextmenu">
|
||||
<div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div>
|
||||
|
@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/>
|
||||
<div style="container-type: inline-size;">
|
||||
<p v-if="appearNote.cw != null" :class="$style.cw">
|
||||
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'account'"/>
|
||||
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/>
|
||||
<MkCwButton v-model="showContent" :note="appearNote" style="margin: 4px 0;" v-on:click.stop/>
|
||||
</p>
|
||||
<div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]" >
|
||||
|
@ -66,7 +66,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:parsedNodes="parsed"
|
||||
:text="appearNote.text"
|
||||
:author="appearNote.user"
|
||||
:nyaize="'account'"
|
||||
:nyaize="'respect'"
|
||||
:emojiUrls="appearNote.emojis"
|
||||
:enableEmojiMenu="true"
|
||||
:enableEmojiMenuReaction="true"
|
||||
|
@ -76,7 +76,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else>
|
||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'account'" :emojiUrls="appearNote.emojis"/>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
|
||||
</div>
|
||||
</div>
|
||||
<MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
|
||||
|
@ -232,11 +232,17 @@ function noteclick(id: string) {
|
|||
// plugin
|
||||
if (noteViewInterruptors.length > 0) {
|
||||
onMounted(async () => {
|
||||
let result:Misskey.entities.Note | null = deepClone(note);
|
||||
let result: Misskey.entities.Note | null = deepClone(note);
|
||||
for (const interruptor of noteViewInterruptors) {
|
||||
result = await interruptor.handler(result);
|
||||
|
||||
if (result === null) return isDeleted.value = true;
|
||||
try {
|
||||
result = await interruptor.handler(result);
|
||||
if (result === null) {
|
||||
isDeleted.value = true;
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
note = result;
|
||||
});
|
||||
|
@ -265,7 +271,7 @@ const renoteUri = appearNote.renote ? appearNote.renote.uri : null;
|
|||
const isMyRenote = $i && ($i.id === note.userId);
|
||||
const showContent = ref(false);
|
||||
const parsed = $computed(() => appearNote.text ? mfm.parse(appearNote.text) : null);
|
||||
const urls = parsed ? extractUrlFromMfm(parsed) : null;
|
||||
const urls = $computed(() => parsed ? extractUrlFromMfm(parsed) : null);
|
||||
const animated = $computed(() => parsed ? checkAnimationFromMfm(parsed) : null);
|
||||
const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
|
||||
const isLong = shouldCollapsed(appearNote, urls ?? []);
|
||||
|
|
|
@ -68,7 +68,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</header>
|
||||
<div :class="$style.noteContent">
|
||||
<p v-if="appearNote.cw != null" :class="$style.cw">
|
||||
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'account'"/>
|
||||
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/>
|
||||
<MkCwButton v-model="showContent" :note="appearNote"/>
|
||||
</p>
|
||||
<div v-show="appearNote.cw == null || showContent">
|
||||
|
@ -79,7 +79,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:parsedNodes="parsed"
|
||||
:text="appearNote.text"
|
||||
:author="appearNote.user"
|
||||
:nyaize="'account'"
|
||||
:nyaize="'respect'"
|
||||
:emojiUrls="appearNote.emojis"
|
||||
:enableEmojiMenu="true"
|
||||
:enableEmojiMenuReaction="true"
|
||||
|
@ -90,7 +90,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else>
|
||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'account'" :emojiUrls="appearNote.emojis"/>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
|
||||
</div>
|
||||
</div>
|
||||
<MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
|
||||
|
@ -270,11 +270,17 @@ let note = $ref(deepClone(props.note));
|
|||
// plugin
|
||||
if (noteViewInterruptors.length > 0) {
|
||||
onMounted(async () => {
|
||||
let result:Misskey.entities.Note | null = deepClone(note);
|
||||
let result: Misskey.entities.Note | null = deepClone(note);
|
||||
for (const interruptor of noteViewInterruptors) {
|
||||
result = await interruptor.handler(result);
|
||||
|
||||
if (result === null) return isDeleted.value = true;
|
||||
try {
|
||||
result = await interruptor.handler(result);
|
||||
if (result === null) {
|
||||
isDeleted.value = true;
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
note = result;
|
||||
});
|
||||
|
|
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<Mfm :text="text.trim()" :author="user" :nyaize="'account'" :i="user"/>
|
||||
<Mfm :text="text.trim()" :author="user" :nyaize="'respect'" :i="user"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
|
||||
<div>
|
||||
<p v-if="note.cw != null" :class="$style.cw">
|
||||
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'account'" :emojiUrls="note.emojis"/>
|
||||
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
|
||||
<MkCwButton v-model="showContent" :note="note"/>
|
||||
</p>
|
||||
<div v-show="note.cw == null || showContent">
|
||||
|
|
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
|
||||
<div :class="$style.content">
|
||||
<p v-if="note.cw != null" :class="$style.cw">
|
||||
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'account'"/>
|
||||
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'"/>
|
||||
<MkCwButton v-model="showContent" :note="note"/>
|
||||
</p>
|
||||
<div v-show="note.cw == null || showContent">
|
||||
|
|
|
@ -96,6 +96,10 @@ onUnmounted(() => {
|
|||
onDeactivated(() => {
|
||||
if (connection) connection.dispose();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
reload,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -43,12 +43,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from 'vue';
|
||||
import { computed, ComputedRef, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import * as os from '@/os.js';
|
||||
import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@/scripts/scroll.js';
|
||||
import { useDocumentVisibility } from '@/scripts/use-document-visibility.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { MisskeyEntity } from '@/types/date-separated-list';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
@ -91,6 +90,7 @@ function concatMapWithArray(map: MisskeyEntityMap, entities: MisskeyEntity[]): M
|
|||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
pagination: Paging;
|
||||
|
@ -185,9 +185,8 @@ watch([$$(backed), $$(contentEl)], () => {
|
|||
}
|
||||
});
|
||||
|
||||
if (props.pagination.params && isRef(props.pagination.params)) {
|
||||
watch(props.pagination.params, init, { deep: true });
|
||||
}
|
||||
// パラメータに何らかの変更があった際、再読込したい(チャンネル等のIDが変わったなど)
|
||||
watch(() => props.pagination.params, init, { deep: true });
|
||||
|
||||
watch(queue, (a, b) => {
|
||||
if (a.size === 0 && b.size === 0) return;
|
||||
|
@ -440,8 +439,6 @@ const updateItem = (id: MisskeyEntity['id'], replacer: (old: MisskeyEntity) => M
|
|||
if (queueItem) queue.value.set(id, replacer(queueItem));
|
||||
};
|
||||
|
||||
const inited = init();
|
||||
|
||||
onActivated(() => {
|
||||
isBackTop.value = false;
|
||||
});
|
||||
|
@ -454,8 +451,8 @@ function toBottom() {
|
|||
scrollToBottom(contentEl!);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
inited.then(() => {
|
||||
onBeforeMount(() => {
|
||||
init().then(() => {
|
||||
if (props.pagination.reversed) {
|
||||
nextTick(() => {
|
||||
setTimeout(toBottom, 800);
|
||||
|
@ -487,7 +484,6 @@ defineExpose({
|
|||
queue,
|
||||
backed,
|
||||
more,
|
||||
inited,
|
||||
reload,
|
||||
prepend,
|
||||
append: appendItem,
|
||||
|
|
|
@ -757,7 +757,11 @@ async function post(ev?: MouseEvent) {
|
|||
// plugin
|
||||
if (notePostInterruptors.length > 0) {
|
||||
for (const interruptor of notePostInterruptors) {
|
||||
postData = await interruptor.handler(deepClone(postData));
|
||||
try {
|
||||
postData = await interruptor.handler(deepClone(postData));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1075,6 +1079,7 @@ defineExpose({
|
|||
|
||||
.preview {
|
||||
padding: 16px 20px 0 20px;
|
||||
min-height: 75px;
|
||||
max-height: 150px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
|
|
@ -73,7 +73,6 @@ function getReactionName(reaction: string): string {
|
|||
}
|
||||
|
||||
.users {
|
||||
contain: content;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin: -4px 14px 0 10px;
|
||||
|
@ -85,7 +84,7 @@ function getReactionName(reaction: string): string {
|
|||
line-height: 24px;
|
||||
padding-top: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
||||
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span>
|
||||
<MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`" v-on:click.stop><i class="ph-arrow-bend-left-up ph-bold ph-lg"></i></MkA>
|
||||
<Mfm v-if="note.text" :text="note.text" :author="note.user" :nyaize="'account'" :isAnim="allowAnim" :emojiUrls="note.emojis"/>
|
||||
<Mfm v-if="note.text" :text="note.text" :author="note.user" :nyaize="'respect'" :isAnim="allowAnim" :emojiUrls="note.emojis"/>
|
||||
<MkButton v-if="!allowAnim && animated && !hideFiles" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
|
||||
<MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated && !hideFiles" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" v-on:click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
|
||||
<div v-if="note.text && translating || note.text && translation" :class="$style.translation">
|
||||
|
|
|
@ -5,19 +5,28 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<MkPullToRefresh ref="prComponent" :refresher="() => reloadTimeline()">
|
||||
<MkNotes ref="tlComponent" :noGap="!defaultStore.state.showGapBetweenNotesInTimeline" :pagination="pagination" @queue="emit('queue', $event)" @status="prComponent.setDisabled($event)"/>
|
||||
<MkNotes
|
||||
v-if="paginationQuery"
|
||||
ref="tlComponent"
|
||||
:pagination="paginationQuery"
|
||||
:noGap="!defaultStore.state.showGapBetweenNotesInTimeline"
|
||||
@queue="emit('queue', $event)"
|
||||
@status="prComponent.setDisabled($event)"
|
||||
/>
|
||||
</MkPullToRefresh>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, provide, onUnmounted } from 'vue';
|
||||
import { computed, watch, onUnmounted, provide } from 'vue';
|
||||
import { Connection } from 'misskey-js/built/streaming.js';
|
||||
import MkNotes from '@/components/MkNotes.vue';
|
||||
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
||||
import { useStream, reloadStream } from '@/stream.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
import * as sound from '@/scripts/sound.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { Paging } from '@/components/MkPagination.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
src: string;
|
||||
|
@ -44,6 +53,18 @@ const emit = defineEmits<{
|
|||
|
||||
provide('inChannel', computed(() => props.src === 'channel'));
|
||||
|
||||
type TimelineQueryType = {
|
||||
antennaId?: string,
|
||||
withRenotes?: boolean,
|
||||
withReplies?: boolean,
|
||||
withFiles?: boolean,
|
||||
withBots?: boolean,
|
||||
visibility?: string,
|
||||
listId?: string,
|
||||
channelId?: string,
|
||||
roleId?: string
|
||||
}
|
||||
|
||||
const prComponent: InstanceType<typeof MkPullToRefresh> = $ref();
|
||||
const tlComponent: InstanceType<typeof MkNotes> = $ref();
|
||||
|
||||
|
@ -65,13 +86,13 @@ const prepend = note => {
|
|||
}
|
||||
};
|
||||
|
||||
let endpoint;
|
||||
let query;
|
||||
let connection;
|
||||
let connection2;
|
||||
let connection: Connection;
|
||||
let connection2: Connection;
|
||||
let paginationQuery: Paging | null = null;
|
||||
|
||||
const stream = useStream();
|
||||
const connectChannel = () => {
|
||||
|
||||
function connectChannel() {
|
||||
if (props.src === 'antenna') {
|
||||
connection = stream.useChannel('antenna', {
|
||||
antennaId: props.antenna,
|
||||
|
@ -128,89 +149,116 @@ const connectChannel = () => {
|
|||
});
|
||||
}
|
||||
if (props.src !== 'directs' || props.src !== 'mentions') connection.on('note', prepend);
|
||||
};
|
||||
|
||||
if (props.src === 'antenna') {
|
||||
endpoint = 'antennas/notes';
|
||||
query = {
|
||||
antennaId: props.antenna,
|
||||
};
|
||||
} else if (props.src === 'home') {
|
||||
endpoint = 'notes/timeline';
|
||||
query = {
|
||||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
withBots: props.withBots,
|
||||
};
|
||||
} else if (props.src === 'local') {
|
||||
endpoint = 'notes/local-timeline';
|
||||
query = {
|
||||
withRenotes: props.withRenotes,
|
||||
withReplies: props.withReplies,
|
||||
withBots: props.withBots,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
};
|
||||
} else if (props.src === 'social') {
|
||||
endpoint = 'notes/hybrid-timeline';
|
||||
query = {
|
||||
withRenotes: props.withRenotes,
|
||||
withReplies: props.withReplies,
|
||||
withBots: props.withBots,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
};
|
||||
} else if (props.src === 'global') {
|
||||
endpoint = 'notes/global-timeline';
|
||||
query = {
|
||||
withRenotes: props.withRenotes,
|
||||
withBots: props.withBots,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
};
|
||||
} else if (props.src === 'mentions') {
|
||||
endpoint = 'notes/mentions';
|
||||
} else if (props.src === 'directs') {
|
||||
endpoint = 'notes/mentions';
|
||||
query = {
|
||||
visibility: 'specified',
|
||||
};
|
||||
} else if (props.src === 'list') {
|
||||
endpoint = 'notes/user-list-timeline';
|
||||
query = {
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
listId: props.list,
|
||||
};
|
||||
} else if (props.src === 'channel') {
|
||||
endpoint = 'channels/timeline';
|
||||
query = {
|
||||
channelId: props.channel,
|
||||
};
|
||||
} else if (props.src === 'role') {
|
||||
endpoint = 'roles/notes';
|
||||
query = {
|
||||
roleId: props.role,
|
||||
};
|
||||
}
|
||||
|
||||
if (!defaultStore.state.disableStreamingTimeline) {
|
||||
connectChannel();
|
||||
|
||||
onUnmounted(() => {
|
||||
connection.dispose();
|
||||
if (connection2) connection2.dispose();
|
||||
});
|
||||
function disconnectChannel() {
|
||||
if (connection) connection.dispose();
|
||||
if (connection2) connection2.dispose();
|
||||
}
|
||||
|
||||
const pagination = {
|
||||
endpoint: endpoint,
|
||||
limit: 10,
|
||||
params: query,
|
||||
};
|
||||
function updatePaginationQuery() {
|
||||
let endpoint: string | null;
|
||||
let query: TimelineQueryType | null;
|
||||
|
||||
if (props.src === 'antenna') {
|
||||
endpoint = 'antennas/notes';
|
||||
query = {
|
||||
antennaId: props.antenna,
|
||||
};
|
||||
} else if (props.src === 'home') {
|
||||
endpoint = 'notes/timeline';
|
||||
query = {
|
||||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
withBots: props.withBots,
|
||||
};
|
||||
} else if (props.src === 'local') {
|
||||
endpoint = 'notes/local-timeline';
|
||||
query = {
|
||||
withRenotes: props.withRenotes,
|
||||
withReplies: props.withReplies,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
withBots: props.withBots,
|
||||
};
|
||||
} else if (props.src === 'social') {
|
||||
endpoint = 'notes/hybrid-timeline';
|
||||
query = {
|
||||
withRenotes: props.withRenotes,
|
||||
withReplies: props.withReplies,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
withBots: props.withBots,
|
||||
};
|
||||
} else if (props.src === 'global') {
|
||||
endpoint = 'notes/global-timeline';
|
||||
query = {
|
||||
withRenotes: props.withRenotes,
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
withBots: props.withBots,
|
||||
};
|
||||
} else if (props.src === 'mentions') {
|
||||
endpoint = 'notes/mentions';
|
||||
query = null;
|
||||
} else if (props.src === 'directs') {
|
||||
endpoint = 'notes/mentions';
|
||||
query = {
|
||||
visibility: 'specified',
|
||||
};
|
||||
} else if (props.src === 'list') {
|
||||
endpoint = 'notes/user-list-timeline';
|
||||
query = {
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
listId: props.list,
|
||||
};
|
||||
} else if (props.src === 'channel') {
|
||||
endpoint = 'channels/timeline';
|
||||
query = {
|
||||
channelId: props.channel,
|
||||
};
|
||||
} else if (props.src === 'role') {
|
||||
endpoint = 'roles/notes';
|
||||
query = {
|
||||
roleId: props.role,
|
||||
};
|
||||
} else {
|
||||
endpoint = null;
|
||||
query = null;
|
||||
}
|
||||
|
||||
if (endpoint && query) {
|
||||
paginationQuery = {
|
||||
endpoint: endpoint,
|
||||
limit: 10,
|
||||
params: query,
|
||||
};
|
||||
} else {
|
||||
paginationQuery = null;
|
||||
}
|
||||
}
|
||||
|
||||
function refreshEndpointAndChannel() {
|
||||
if (!defaultStore.state.disableStreamingTimeline) {
|
||||
disconnectChannel();
|
||||
connectChannel();
|
||||
}
|
||||
|
||||
updatePaginationQuery();
|
||||
}
|
||||
|
||||
// IDが切り替わったら切り替え先のTLを表示させたい
|
||||
watch(() => [props.list, props.antenna, props.channel, props.role], refreshEndpointAndChannel);
|
||||
|
||||
// 初回表示用
|
||||
refreshEndpointAndChannel();
|
||||
|
||||
onUnmounted(() => {
|
||||
disconnectChannel();
|
||||
});
|
||||
|
||||
function reloadTimeline() {
|
||||
return new Promise<void>((res) => {
|
||||
tlNotesCount = 0;
|
||||
|
||||
tlComponent.pagingComponent?.reload().then(() => {
|
||||
reloadStream();
|
||||
res();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@ import { VNode, h } from 'vue';
|
|||
import * as mfm from 'mfm-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkUrl from '@/components/global/MkUrl.vue';
|
||||
import MkTime from '@/components/global/MkTime.vue';
|
||||
import MkLink from '@/components/MkLink.vue';
|
||||
import MkMention from '@/components/MkMention.vue';
|
||||
import MkEmoji from '@/components/global/MkEmoji.vue';
|
||||
|
@ -36,7 +37,7 @@ type MfmProps = {
|
|||
isNote?: boolean;
|
||||
emojiUrls?: string[];
|
||||
rootScale?: number;
|
||||
nyaize: boolean | 'account';
|
||||
nyaize: boolean | 'respect';
|
||||
parsedNodes?: mfm.MfmNode[] | null;
|
||||
enableEmojiMenu?: boolean;
|
||||
enableEmojiMenuReaction?: boolean;
|
||||
|
@ -46,7 +47,7 @@ type MfmProps = {
|
|||
// eslint-disable-next-line import/no-default-export
|
||||
export default function(props: MfmProps) {
|
||||
const isNote = props.isNote ?? true;
|
||||
const shouldNyaize = props.nyaize ? props.nyaize === 'account' ? props.author?.isCat ? props.author?.speakAsCat : false : false : false;
|
||||
const shouldNyaize = props.nyaize ? props.nyaize === 'respect' ? props.author?.isCat ? props.author?.speakAsCat : false : false : false;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (props.text == null || props.text === '') return;
|
||||
|
@ -239,6 +240,34 @@ export default function(props: MfmProps) {
|
|||
style = `background-color: #${color};`;
|
||||
break;
|
||||
}
|
||||
case 'ruby': {
|
||||
if (token.children.length === 1) {
|
||||
const child = token.children[0];
|
||||
const text = child.type === 'text' ? child.props.text : '';
|
||||
return h('ruby', {}, [text.split(' ')[0], h('rt', text.split(' ')[1])]);
|
||||
} else {
|
||||
const rt = token.children.at(-1)!;
|
||||
const text = rt.type === 'text' ? rt.props.text : '';
|
||||
return h('ruby', {}, [...genEl(token.children.slice(0, token.children.length - 1), scale), h('rt', text.trim())]);
|
||||
}
|
||||
}
|
||||
case 'unixtime': {
|
||||
const child = token.children[0];
|
||||
const unixtime = parseInt(child.type === 'text' ? child.props.text : '');
|
||||
return h('span', {
|
||||
style: 'display: inline-block; font-size: 90%; border: solid 1px var(--divider); border-radius: 999px; padding: 4px 10px 4px 6px;',
|
||||
}, [
|
||||
h('i', {
|
||||
class: 'ti ti-clock',
|
||||
style: 'margin-right: 0.25em;',
|
||||
}),
|
||||
h(MkTime, {
|
||||
key: Math.random(),
|
||||
time: unixtime * 1000,
|
||||
mode: 'detail',
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
||||
if (style == null) {
|
||||
return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']);
|
||||
|
|
|
@ -49,8 +49,15 @@ const relative = $computed<string>(() => {
|
|||
ago >= 3600 ? i18n.t('_ago.hoursAgo', { n: Math.round(ago / 3600).toString() }) :
|
||||
ago >= 60 ? i18n.t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) :
|
||||
ago >= 10 ? i18n.t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) :
|
||||
ago >= -1 ? i18n.ts._ago.justNow :
|
||||
i18n.ts._ago.future);
|
||||
ago >= -3 ? i18n.ts._ago.justNow :
|
||||
ago < -31536000 ? i18n.t('_timeIn.years', { n: Math.round(-ago / 31536000).toString() }) :
|
||||
ago < -2592000 ? i18n.t('_timeIn.months', { n: Math.round(-ago / 2592000).toString() }) :
|
||||
ago < -604800 ? i18n.t('_timeIn.weeks', { n: Math.round(-ago / 604800).toString() }) :
|
||||
ago < -86400 ? i18n.t('_timeIn.days', { n: Math.round(-ago / 86400).toString() }) :
|
||||
ago < -3600 ? i18n.t('_timeIn.hours', { n: Math.round(-ago / 3600).toString() }) :
|
||||
ago < -60 ? i18n.t('_timeIn.minutes', { n: (~~(-ago / 60)).toString() }) :
|
||||
i18n.t('_timeIn.seconds', { n: (~~(-ago % 60)).toString() })
|
||||
);
|
||||
});
|
||||
|
||||
let tickId: number;
|
||||
|
|
|
@ -5,14 +5,14 @@
|
|||
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
|
||||
const address = new URL(location.href);
|
||||
const address = new URL(document.querySelector<HTMLMetaElement>('meta[property="instance_url"]')?.content || location.href);
|
||||
const siteName = document.querySelector<HTMLMetaElement>('meta[property="og:site_name"]')?.content;
|
||||
|
||||
export const host = address.host;
|
||||
export const hostname = address.hostname;
|
||||
export const url = address.origin;
|
||||
export const apiUrl = url + '/api';
|
||||
export const wsUrl = url.replace('http://', 'ws://').replace('https://', 'wss://') + '/streaming';
|
||||
export const apiUrl = location.origin + '/api';
|
||||
export const wsOrigin = location.origin;
|
||||
export const lang = miLocalStorage.getItem('lang') ?? 'en-US';
|
||||
export const langs = _LANGS_;
|
||||
const preParseLocale = miLocalStorage.getItem('locale');
|
||||
|
|
|
@ -144,3 +144,5 @@ export const CURRENT_STICKY_BOTTOM = 'CURRENT_STICKY_BOTTOM';
|
|||
export const DEFAULT_SERVER_ERROR_IMAGE_URL = 'https://launcher.moe/error.png';
|
||||
export const DEFAULT_NOT_FOUND_IMAGE_URL = 'https://launcher.moe/missingpage.webp';
|
||||
export const DEFAULT_INFO_IMAGE_URL = 'https://launcher.moe/nothinghere.png';
|
||||
|
||||
export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'ruby', 'unixtime'];
|
||||
|
|
|
@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<option value="publishing">{{ i18n.ts.publishing }}</option>
|
||||
<option value="suspended">{{ i18n.ts.suspended }}</option>
|
||||
<option value="blocked">{{ i18n.ts.blocked }}</option>
|
||||
<option value="silenced">{{ i18n.ts.silence }}</option>
|
||||
<option value="notResponding">{{ i18n.ts.notResponding }}</option>
|
||||
</MkSelect>
|
||||
<MkSelect v-model="sort">
|
||||
|
@ -83,6 +84,7 @@ const pagination = {
|
|||
state === 'publishing' ? { publishing: true } :
|
||||
state === 'suspended' ? { suspended: true } :
|
||||
state === 'blocked' ? { blocked: true } :
|
||||
state === 'silenced' ? { silenced: true } :
|
||||
state === 'notResponding' ? { notResponding: true } :
|
||||
{}),
|
||||
})),
|
||||
|
@ -91,6 +93,7 @@ const pagination = {
|
|||
function getStatus(instance) {
|
||||
if (instance.isSuspended) return 'Suspended';
|
||||
if (instance.isBlocked) return 'Blocked';
|
||||
if (instance.isSilenced) return 'Silenced';
|
||||
if (instance.isNotResponding) return 'Error';
|
||||
return 'Alive';
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ import MkSuperMenu from '@/components/MkSuperMenu.vue';
|
|||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { instance } from '@/instance.js';
|
||||
import * as os from '@/os.js';
|
||||
import { lookupUser } from '@/scripts/lookup-user.js';
|
||||
import { lookupUser, lookupUserByEmail } from '@/scripts/lookup-user.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { definePageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
|
||||
|
||||
|
@ -279,7 +279,7 @@ provideMetadataReceiver((info) => {
|
|||
}
|
||||
});
|
||||
|
||||
const invite = () => {
|
||||
function invite() {
|
||||
os.api('admin/invite/create').then(x => {
|
||||
os.alert({
|
||||
type: 'info',
|
||||
|
@ -291,15 +291,21 @@ const invite = () => {
|
|||
text: err,
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const lookup = (ev) => {
|
||||
function lookup(ev: MouseEvent) {
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.user,
|
||||
icon: 'ph-user ph-bold ph-lg',
|
||||
action: () => {
|
||||
lookupUser();
|
||||
},
|
||||
}, {
|
||||
text: `${i18n.ts.user} (${i18n.ts.email})`,
|
||||
icon: 'ti ti-user',
|
||||
action: () => {
|
||||
lookupUserByEmail();
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.note,
|
||||
icon: 'ph-pencil ph-bold ph-lg',
|
||||
|
@ -319,7 +325,7 @@ const lookup = (ev) => {
|
|||
alert('TODO');
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
};
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => []);
|
||||
|
||||
|
|
|
@ -95,6 +95,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #caption>{{ i18n.ts._serverSettings.fanoutTimelineDescription }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkSwitch v-model="enableFanoutTimelineDbFallback">
|
||||
<template #label>{{ i18n.ts._serverSettings.fanoutTimelineDbFallback }}</template>
|
||||
<template #caption>{{ i18n.ts._serverSettings.fanoutTimelineDbFallbackDescription }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkInput v-model="perLocalUserUserTimelineCacheMax" type="number">
|
||||
<template #label>perLocalUserUserTimelineCacheMax</template>
|
||||
</MkInput>
|
||||
|
@ -171,6 +176,7 @@ let enableServiceWorker: boolean = $ref(false);
|
|||
let swPublicKey: any = $ref(null);
|
||||
let swPrivateKey: any = $ref(null);
|
||||
let enableFanoutTimeline: boolean = $ref(false);
|
||||
let enableFanoutTimelineDbFallback: boolean = $ref(false);
|
||||
let perLocalUserUserTimelineCacheMax: number = $ref(0);
|
||||
let perRemoteUserUserTimelineCacheMax: number = $ref(0);
|
||||
let perUserHomeTimelineCacheMax: number = $ref(0);
|
||||
|
@ -192,6 +198,7 @@ async function init(): Promise<void> {
|
|||
swPublicKey = meta.swPublickey;
|
||||
swPrivateKey = meta.swPrivateKey;
|
||||
enableFanoutTimeline = meta.enableFanoutTimeline;
|
||||
enableFanoutTimelineDbFallback = meta.enableFanoutTimelineDbFallback;
|
||||
perLocalUserUserTimelineCacheMax = meta.perLocalUserUserTimelineCacheMax;
|
||||
perRemoteUserUserTimelineCacheMax = meta.perRemoteUserUserTimelineCacheMax;
|
||||
perUserHomeTimelineCacheMax = meta.perUserHomeTimelineCacheMax;
|
||||
|
@ -214,6 +221,7 @@ async function save(): void {
|
|||
swPublicKey,
|
||||
swPrivateKey,
|
||||
enableFanoutTimeline,
|
||||
enableFanoutTimelineDbFallback,
|
||||
perLocalUserUserTimelineCacheMax,
|
||||
perRemoteUserUserTimelineCacheMax,
|
||||
perUserHomeTimelineCacheMax,
|
||||
|
|
|
@ -18,11 +18,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSpacer v-else-if="tab === 'users'" :contentMax="1200">
|
||||
<div class="_gaps_s">
|
||||
<div v-if="role">{{ role.description }}</div>
|
||||
<MkUserList :pagination="users" :extractor="(item) => item.user"/>
|
||||
<MkUserList v-if="visible" :pagination="users" :extractor="(item) => item.user"/>
|
||||
<div v-else-if="!visible" class="_fullinfo">
|
||||
<img :src="infoImageUrl" class="_ghost"/>
|
||||
<div>{{ i18n.ts.nothing }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
<MkSpacer v-else-if="tab === 'timeline'" :contentMax="700">
|
||||
<MkTimeline ref="timeline" src="role" :role="props.role"/>
|
||||
<MkTimeline v-if="visible" ref="timeline" src="role" :role="props.role"/>
|
||||
<div v-else-if="!visible" class="_fullinfo">
|
||||
<img :src="infoImageUrl" class="_ghost"/>
|
||||
<div>{{ i18n.ts.nothing }}</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
@ -35,7 +43,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import MkTimeline from '@/components/MkTimeline.vue';
|
||||
import { instanceName } from '@/config.js';
|
||||
import { serverErrorImageUrl } from '@/instance.js';
|
||||
import { serverErrorImageUrl, infoImageUrl } from '@/instance.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
role: string;
|
||||
|
@ -47,6 +55,7 @@ const props = withDefaults(defineProps<{
|
|||
let tab = $ref(props.initialTab);
|
||||
let role = $ref();
|
||||
let error = $ref();
|
||||
let visible = $ref(false);
|
||||
|
||||
watch(() => props.role, () => {
|
||||
os.api('roles/show', {
|
||||
|
@ -54,6 +63,7 @@ watch(() => props.role, () => {
|
|||
}).then(res => {
|
||||
role = res;
|
||||
document.title = `${role?.name} | ${instanceName}`;
|
||||
visible = res.isExplorable && res.isPublic;
|
||||
}).catch((err) => {
|
||||
if (err.code === 'NO_SUCH_ROLE') {
|
||||
error = i18n.ts.noRole;
|
||||
|
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<FormSection first>
|
||||
<template #label>{{ i18n.ts.notificationRecieveConfig }}</template>
|
||||
<div class="_gaps_s">
|
||||
<MkFolder v-for="type in notificationTypes" :key="type">
|
||||
<MkFolder v-for="type in notificationTypes.filter(x => !nonConfigurableNotificationTypes.includes(x))" :key="type">
|
||||
<template #label>{{ i18n.t('_notification._types.' + type) }}</template>
|
||||
<template #suffix>
|
||||
{{
|
||||
|
@ -68,6 +68,8 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
|||
import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
|
||||
import { notificationTypes } from '@/const.js';
|
||||
|
||||
const nonConfigurableNotificationTypes = ['note'];
|
||||
|
||||
let allowButton = $shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>();
|
||||
let pushRegistrationInServer = $computed(() => allowButton?.pushRegistrationInServer);
|
||||
let sendReadMessage = $computed(() => pushRegistrationInServer?.sendReadMessage || false);
|
||||
|
|
|
@ -159,12 +159,13 @@ async function reloadAsk() {
|
|||
}
|
||||
|
||||
async function updateRepliesAll(withReplies: boolean) {
|
||||
const { canceled } = os.confirm({
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: withReplies ? i18n.ts.confirmShowRepliesAll : i18n.ts.confirmHideRepliesAll,
|
||||
});
|
||||
if (canceled) return;
|
||||
await os.api('following/update-all', { withReplies });
|
||||
|
||||
os.api('following/update-all', { withReplies });
|
||||
}
|
||||
|
||||
const exportData = () => {
|
||||
|
|
|
@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--badge);"><i class="ph-shield ph-bold ph-lg"></i></span>
|
||||
<span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ph-lock ph-bold ph-lg"></i></span>
|
||||
<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ph-robot ph-bold ph-lg"></i></span>
|
||||
<button v-if="!isEditingMemo && !memoDraft" class="_button add-note-button" @click="showMemoTextarea">
|
||||
<button v-if="$i && !isEditingMemo && !memoDraft" class="_button add-note-button" @click="showMemoTextarea">
|
||||
<i class="ph-pencil-line ph-bold ph-lg"/> {{ i18n.ts.addMemo }}
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -11,10 +11,9 @@ import { Plugin, noteActions, notePostInterruptors, noteViewInterruptors, postFo
|
|||
const parser = new Parser();
|
||||
const pluginContexts = new Map<string, Interpreter>();
|
||||
|
||||
export function install(plugin: Plugin): void {
|
||||
export async function install(plugin: Plugin): Promise<void> {
|
||||
// 後方互換性のため
|
||||
if (plugin.src == null) return;
|
||||
console.info('Plugin installed:', plugin.name, 'v' + plugin.version);
|
||||
|
||||
const aiscript = new Interpreter(createPluginEnv({
|
||||
plugin: plugin,
|
||||
|
@ -42,7 +41,14 @@ export function install(plugin: Plugin): void {
|
|||
|
||||
initPlugin({ plugin, aiscript });
|
||||
|
||||
aiscript.exec(parser.parse(plugin.src));
|
||||
try {
|
||||
await aiscript.exec(parser.parse(plugin.src));
|
||||
} catch (err) {
|
||||
console.error('Plugin install failed:', plugin.name, 'v' + plugin.version);
|
||||
return;
|
||||
}
|
||||
|
||||
console.info('Plugin installed:', plugin.name, 'v' + plugin.version);
|
||||
}
|
||||
|
||||
function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record<string, values.Value> {
|
||||
|
|
|
@ -39,3 +39,26 @@ export async function lookupUser() {
|
|||
notFound();
|
||||
});
|
||||
}
|
||||
|
||||
export async function lookupUserByEmail() {
|
||||
const { canceled, result } = await os.inputText({
|
||||
title: i18n.ts.emailAddress,
|
||||
type: 'email',
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
try {
|
||||
const user = await os.apiWithDialog('admin/accounts/find-by-email', { email: result });
|
||||
|
||||
os.pageWindow(`/admin/user/${user.id}`);
|
||||
} catch (err) {
|
||||
if (err.code === 'USER_NOT_FOUND') {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts.noSuchUser,
|
||||
});
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'font', 'blur', 'rainbow', 'sparkle', 'rotate'];
|
|
@ -5,7 +5,8 @@
|
|||
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
const cache = new Map<string, HTMLAudioElement>();
|
||||
const ctx = new AudioContext();
|
||||
const cache = new Map<string, AudioBuffer>();
|
||||
|
||||
export const soundsTypes = [
|
||||
null,
|
||||
|
@ -60,15 +61,20 @@ export const soundsTypes = [
|
|||
'noizenecio/kick_gaba7',
|
||||
] as const;
|
||||
|
||||
export function getAudio(file: string, useCache = true): HTMLAudioElement {
|
||||
let audio: HTMLAudioElement;
|
||||
export async function getAudio(file: string, useCache = true) {
|
||||
if (useCache && cache.has(file)) {
|
||||
audio = cache.get(file);
|
||||
} else {
|
||||
audio = new Audio(`/client-assets/sounds/${file}.mp3`);
|
||||
if (useCache) cache.set(file, audio);
|
||||
return cache.get(file)!;
|
||||
}
|
||||
return audio;
|
||||
|
||||
const response = await fetch(`/client-assets/sounds/${file}.mp3`);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const audioBuffer = await ctx.decodeAudioData(arrayBuffer);
|
||||
|
||||
if (useCache) {
|
||||
cache.set(file, audioBuffer);
|
||||
}
|
||||
|
||||
return audioBuffer;
|
||||
}
|
||||
|
||||
export function setVolume(audio: HTMLAudioElement, volume: number): HTMLAudioElement {
|
||||
|
@ -84,8 +90,17 @@ export function play(type: 'noteMy' | 'note' | 'antenna' | 'channel' | 'notifica
|
|||
playFile(sound.type, sound.volume);
|
||||
}
|
||||
|
||||
export function playFile(file: string, volume: number) {
|
||||
const audio = setVolume(getAudio(file), volume);
|
||||
if (audio.volume === 0) return;
|
||||
audio.play();
|
||||
export async function playFile(file: string, volume: number) {
|
||||
const masterVolume = defaultStore.state.sound_masterVolume;
|
||||
if (masterVolume === 0 || volume === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const gainNode = ctx.createGain();
|
||||
gainNode.gain.value = masterVolume * volume;
|
||||
|
||||
const soundSource = ctx.createBufferSource();
|
||||
soundSource.buffer = await getAudio(file);
|
||||
soundSource.connect(gainNode).connect(ctx.destination);
|
||||
soundSource.start();
|
||||
}
|
||||
|
|
|
@ -6,34 +6,18 @@
|
|||
import * as Misskey from 'misskey-js';
|
||||
import { markRaw } from 'vue';
|
||||
import { $i } from '@/account.js';
|
||||
import { url } from '@/config.js';
|
||||
import { wsOrigin } from '@/config.js';
|
||||
|
||||
let stream: Misskey.Stream | null = null;
|
||||
let timeoutHeartBeat: number | null = null;
|
||||
|
||||
export let isReloading: boolean = false;
|
||||
|
||||
export function useStream(): Misskey.Stream {
|
||||
if (stream) return stream;
|
||||
|
||||
stream = markRaw(new Misskey.Stream(url, $i ? {
|
||||
stream = markRaw(new Misskey.Stream(wsOrigin, $i ? {
|
||||
token: $i.token,
|
||||
} : null));
|
||||
|
||||
timeoutHeartBeat = window.setTimeout(heartbeat, 1000 * 60);
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
export function reloadStream() {
|
||||
if (!stream) return useStream();
|
||||
if (timeoutHeartBeat) window.clearTimeout(timeoutHeartBeat);
|
||||
isReloading = true;
|
||||
|
||||
stream.close();
|
||||
stream.once('_connected_', () => isReloading = false);
|
||||
stream.stream.reconnect();
|
||||
timeoutHeartBeat = window.setTimeout(heartbeat, 1000 * 60);
|
||||
window.setTimeout(heartbeat, 1000 * 60);
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
@ -42,5 +26,5 @@ function heartbeat(): void {
|
|||
if (stream != null && document.visibilityState === 'visible') {
|
||||
stream.heartbeat();
|
||||
}
|
||||
timeoutHeartBeat = window.setTimeout(heartbeat, 1000 * 60);
|
||||
window.setTimeout(heartbeat, 1000 * 60);
|
||||
}
|
||||
|
|
|
@ -176,7 +176,7 @@ function more(ev: MouseEvent) {
|
|||
.bottom {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
padding: 20px 0;
|
||||
padding-top: 20px;
|
||||
background: var(--X14);
|
||||
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||
backdrop-filter: var(--blur, blur(8px));
|
||||
|
@ -228,11 +228,10 @@ function more(ev: MouseEvent) {
|
|||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 30px;
|
||||
padding: 20px 0 20px 30px;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
box-sizing: border-box;
|
||||
margin-top: 16px;
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
|
@ -363,7 +362,7 @@ function more(ev: MouseEvent) {
|
|||
.bottom {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
padding: 20px 0;
|
||||
padding-top: 20px;
|
||||
background: var(--X14);
|
||||
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||
backdrop-filter: var(--blur, blur(8px));
|
||||
|
@ -374,7 +373,6 @@ function more(ev: MouseEvent) {
|
|||
position: relative;
|
||||
width: 100%;
|
||||
height: 52px;
|
||||
margin-bottom: 16px;
|
||||
text-align: center;
|
||||
|
||||
&:before {
|
||||
|
@ -411,6 +409,7 @@ function more(ev: MouseEvent) {
|
|||
.account {
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
width: 100%;
|
||||
overflow: clip;
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { onUnmounted } from 'vue';
|
||||
import { useStream, isReloading } from '@/stream.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
|
@ -26,7 +26,6 @@ const zIndex = os.claimZIndex('high');
|
|||
let hasDisconnected = $ref(false);
|
||||
|
||||
function onDisconnected() {
|
||||
if (isReloading) return;
|
||||
hasDisconnected = true;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<XColumn :menu="menu" :column="column" :isStacked="isStacked">
|
||||
<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()">
|
||||
<template #header>
|
||||
<i class="ph-flying-saucer ph-bold ph-lg"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||
</template>
|
||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<XColumn :menu="menu" :column="column" :isStacked="isStacked">
|
||||
<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()">
|
||||
<template #header>
|
||||
<i class="ph-television ph-bold ph-lg"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||
</template>
|
||||
|
|
|
@ -57,6 +57,7 @@ const props = withDefaults(defineProps<{
|
|||
isStacked?: boolean;
|
||||
naked?: boolean;
|
||||
menu?: MenuItem[];
|
||||
refresher?: () => Promise<void>;
|
||||
}>(), {
|
||||
isStacked: false,
|
||||
naked: false,
|
||||
|
@ -183,6 +184,18 @@ function getMenu() {
|
|||
items = props.menu.concat(items);
|
||||
}
|
||||
|
||||
if (props.refresher) {
|
||||
items = [{
|
||||
icon: 'ti ti-refresh',
|
||||
text: i18n.ts.reload,
|
||||
action: () => {
|
||||
if (props.refresher) {
|
||||
props.refresher();
|
||||
}
|
||||
},
|
||||
}, ...items];
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,10 +4,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<XColumn :column="column" :isStacked="isStacked">
|
||||
<XColumn :column="column" :isStacked="isStacked" :refresher="() => reloadTimeline()">
|
||||
<template #header><i class="ph-envelope ph-bold ph-lg" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
||||
|
||||
<MkNotes :pagination="pagination"/>
|
||||
<MkNotes ref="tlComponent" :pagination="pagination"/>
|
||||
</XColumn>
|
||||
</template>
|
||||
|
||||
|
@ -29,4 +29,14 @@ const pagination = {
|
|||
visibility: 'specified',
|
||||
},
|
||||
};
|
||||
|
||||
const tlComponent: InstanceType<typeof MkNotes> = $ref();
|
||||
|
||||
function reloadTimeline() {
|
||||
return new Promise<void>((res) => {
|
||||
tlComponent.pagingComponent?.reload().then(() => {
|
||||
res();
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<XColumn :menu="menu" :column="column" :isStacked="isStacked">
|
||||
<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()">
|
||||
<template #header>
|
||||
<i class="ph-list ph-bold ph-lg"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||
</template>
|
||||
|
|
|
@ -4,10 +4,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<XColumn :column="column" :isStacked="isStacked">
|
||||
<XColumn :column="column" :isStacked="isStacked" :refresher="() => reloadTimeline()">
|
||||
<template #header><i class="ph-at ph-bold ph-lg" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
||||
|
||||
<MkNotes :pagination="pagination"/>
|
||||
<MkNotes ref="tlComponent" :pagination="pagination"/>
|
||||
</XColumn>
|
||||
</template>
|
||||
|
||||
|
@ -22,6 +22,16 @@ defineProps<{
|
|||
isStacked: boolean;
|
||||
}>();
|
||||
|
||||
const tlComponent: InstanceType<typeof MkNotes> = $ref();
|
||||
|
||||
function reloadTimeline() {
|
||||
return new Promise<void>((res) => {
|
||||
tlComponent.pagingComponent?.reload().then(() => {
|
||||
res();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'notes/mentions' as const,
|
||||
limit: 10,
|
||||
|
|
|
@ -4,10 +4,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<XColumn :column="column" :isStacked="isStacked" :menu="menu">
|
||||
<XColumn :column="column" :isStacked="isStacked" :menu="menu" :refresher="() => notificationsComponent.reload()">
|
||||
<template #header><i class="ph-bell ph-bold ph-lg" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
||||
|
||||
<XNotifications :excludeTypes="props.column.excludeTypes"/>
|
||||
<XNotifications ref="notificationsComponent" :excludeTypes="props.column.excludeTypes"/>
|
||||
</XColumn>
|
||||
</template>
|
||||
|
||||
|
@ -24,6 +24,8 @@ const props = defineProps<{
|
|||
isStacked: boolean;
|
||||
}>();
|
||||
|
||||
let notificationsComponent = $shallowRef<InstanceType<typeof XNotifications>>();
|
||||
|
||||
function func() {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSelectWindow.vue')), {
|
||||
excludeTypes: props.column.excludeTypes,
|
||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<XColumn :menu="menu" :column="column" :isStacked="isStacked">
|
||||
<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()">
|
||||
<template #header>
|
||||
<i class="ph-seal-check ph-bold ph-lg"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||
</template>
|
||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<XColumn :menu="menu" :column="column" :isStacked="isStacked">
|
||||
<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()">
|
||||
<template #header>
|
||||
<i v-if="column.tl === 'home'" class="ph-house ph-bold ph-lg"></i>
|
||||
<i v-else-if="column.tl === 'local'" class="ph-planet ph-bold ph-lg"></i>
|
||||
|
@ -48,6 +48,7 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
let disabled = $ref(false);
|
||||
let timeline = $shallowRef<InstanceType<typeof MkTimeline>>();
|
||||
|
||||
const isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable));
|
||||
const isGlobalTimelineAvailable = (($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable));
|
||||
|
|
|
@ -1061,7 +1061,7 @@
|
|||
"💰": ["dollar", "payment", "coins", "sale"],
|
||||
"🪙": ["dollar", "payment", "coins", "sale"],
|
||||
"💳": ["money", "sales", "dollar", "bill", "payment", "shopping"],
|
||||
"🪫": [],
|
||||
"🪪": [],
|
||||
"💎": ["blue", "ruby", "diamond", "jewelry"],
|
||||
"⚖": ["law", "fairness", "weight"],
|
||||
"🧰": ["tools", "diy", "fix", "maintainer", "mechanic"],
|
||||
|
|
|
@ -44,18 +44,27 @@
|
|||
],
|
||||
"preset": "ts-jest/presets/default",
|
||||
"transform": {
|
||||
"^.+\\.(ts|tsx)$": ["ts-jest", {
|
||||
"tsconfig": "tsconfig.json"
|
||||
}]
|
||||
"^.+\\.(ts|tsx)$": [
|
||||
"ts-jest",
|
||||
{
|
||||
"tsconfig": "tsconfig.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"testEnvironment": "node"
|
||||
},
|
||||
"homepage": "https://github.com/h3poteto/megalodon#readme",
|
||||
"dependencies": {
|
||||
"@types/oauth": "^0.9.2",
|
||||
"@types/ws": "^8.5.5",
|
||||
"@types/core-js": "^2.5.8",
|
||||
"@types/form-data": "^2.5.0",
|
||||
"@types/jest": "^29.5.10",
|
||||
"@types/oauth": "^0.9.4",
|
||||
"@types/object-assign-deep": "^0.4.3",
|
||||
"@types/parse-link-header": "^2.0.3",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@types/ws": "^8.5.10",
|
||||
"axios": "1.5.0",
|
||||
"dayjs": "^1.11.9",
|
||||
"dayjs": "^1.11.10",
|
||||
"form-data": "^4.0.0",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
"oauth": "^0.10.0",
|
||||
|
@ -64,24 +73,18 @@
|
|||
"socks-proxy-agent": "^8.0.2",
|
||||
"typescript": "5.1.6",
|
||||
"uuid": "^9.0.1",
|
||||
"ws": "8.14.2",
|
||||
"@types/core-js": "^2.5.6",
|
||||
"@types/form-data": "^2.5.0",
|
||||
"@types/jest": "^29.5.5",
|
||||
"@types/object-assign-deep": "^0.4.1",
|
||||
"@types/parse-link-header": "^2.0.1",
|
||||
"@types/uuid": "^9.0.4"
|
||||
"ws": "8.14.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.2",
|
||||
"@typescript-eslint/parser": "^6.7.2",
|
||||
"eslint": "^8.49.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
||||
"@typescript-eslint/parser": "^6.12.0",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-worker": "^29.7.0",
|
||||
"lodash": "^4.17.14",
|
||||
"prettier": "^3.0.3",
|
||||
"lodash": "4.17.21",
|
||||
"prettier": "^3.1.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"typedoc": "^0.25.1"
|
||||
"typedoc": "^0.25.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,13 +20,13 @@
|
|||
"url": "git+https://github.com/misskey-dev/misskey.js.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/api-extractor": "7.38.2",
|
||||
"@microsoft/api-extractor": "7.38.3",
|
||||
"@swc/jest": "0.2.29",
|
||||
"@types/jest": "29.5.7",
|
||||
"@types/node": "20.8.10",
|
||||
"@typescript-eslint/eslint-plugin": "6.9.1",
|
||||
"@typescript-eslint/parser": "6.9.1",
|
||||
"eslint": "8.52.0",
|
||||
"@types/jest": "29.5.8",
|
||||
"@types/node": "20.9.1",
|
||||
"@typescript-eslint/eslint-plugin": "6.11.0",
|
||||
"@typescript-eslint/parser": "6.11.0",
|
||||
"eslint": "8.53.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-fetch-mock": "3.0.3",
|
||||
"jest-websocket-mock": "2.5.0",
|
||||
|
@ -38,8 +38,8 @@
|
|||
"built"
|
||||
],
|
||||
"dependencies": {
|
||||
"@swc/cli": "0.1.62",
|
||||
"@swc/core": "1.3.95",
|
||||
"@swc/cli": "0.1.63",
|
||||
"@swc/core": "1.3.96",
|
||||
"eventemitter3": "5.0.1",
|
||||
"reconnecting-websocket": "4.4.0"
|
||||
}
|
||||
|
|
|
@ -14,9 +14,9 @@
|
|||
"misskey-js": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/parser": "6.9.1",
|
||||
"@typescript-eslint/parser": "6.11.0",
|
||||
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67",
|
||||
"eslint": "8.52.0",
|
||||
"eslint": "8.53.0",
|
||||
"eslint-plugin-import": "2.29.0",
|
||||
"typescript": "5.2.2"
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue