Merge branch 'misskey-dev:develop' into develop

This commit is contained in:
Ryu jongheon 2022-06-07 20:33:08 +09:00 committed by GitHub
commit 91a366fda5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 408 additions and 222 deletions

View file

@ -1 +1 @@
v18.0.0
v16.15.0

View file

@ -10,18 +10,17 @@ You should also include the user name that made the change.
-->
## 12.x.x (unreleased)
### NOTE
- From this version, Node 18.0.0 or later is required.
### Improvements
- enhance: ドライブに画像ファイルをアップロードするときオリジナル画像を破棄してwebpublicのみ保持するオプション @tamaina
- enhance: API: notifications/readは配列でも受け付けるように #7667 @tamaina
- enhance: プッシュ通知を複数アカウント対応に #7667 @tamaina
- enhance: プッシュ通知にクリックやactionを設定 #7667 @tamaina
- replaced webpack with Vite @tamaina
- update dependencies @syuilo
- enhance: display URL of QR code for TOTP registration @syuilo
- enhance: Supports Unicode Emoji 14.0 @mei23
- Supports Unicode Emoji 14.0 @mei23
- プッシュ通知を複数アカウント対応に #7667 @tamaina
- プッシュ通知にクリックやactionを設定 #7667 @tamaina
- ドライブに画像ファイルをアップロードするときオリジナル画像を破棄してwebpublicのみ保持するオプション @tamaina
- Server: always remove completed tasks of job queue @Johann150
- Client: make emoji stand out more on reaction button @Johann150
- Client: display URL of QR code for TOTP registration @tamaina
- API: notifications/readは配列でも受け付けるように #7667 @tamaina
- API: ユーザー検索で、クエリがusernameの条件を満たす場合はusernameもLIKE検索するように @tamaina
- MFM: Allow speed changes in all animated MFMs @Johann150
- The theme color is now better validated. @Johann150
Your own theme color may be unset if it was in an invalid format.
Admins should check their instance settings if in doubt.
@ -30,20 +29,31 @@ You should also include the user name that made the change.
Admins should make sure the reverse proxy sets the `X-Forwarded-For` header to the original address.
### Bugfixes
- Client: fix settings page @tamaina
- Client: fix profile tabs @futchitwo
- Server: keep file order of note attachement @Johann150
- Server: fix caching @Johann150
- Server: await promises when following or unfollowing users @Johann150
- Client: fix abuse reports page to be able to show all reports @Johann150
- Federation: Add rel attribute to host-meta @mei23
- Client: fix profile picture height in mentions @tamaina
- MFM: more animated functions support `speed` parameter @futchitwo
- Federation: Fix quote renotes containing no text being federated correctly @Johann150
- Server: fix missing foreign key for reports leading to reports page being unusable @Johann150
- Server: fix internal in-memory caching @Johann150
- Server: use correct order of attachments on notes @Johann150
- Server: prevent crash when processing certain PNGs @syuilo
- Server: Fix unable to generate video thumbnails @mei23
- Server: Fix `Cannot find module` issue @mei23
- Federation: Add rel attribute to host-meta @mei23
- Federation: add id for activitypub follows @Johann150
- Federation: ensure resolver does not fetch local resources via HTTP(S) @Johann150
- Federation: correctly render empty note text @Johann150
- Federation: Fix quote renotes containing no text being federated correctly @Johann150
- Federation: remove duplicate br tag/newline @Johann150
- Federation: add missing authorization checks @Johann150
- Client: fix profile picture height in mentions @tamaina
- Client: fix abuse reports page to be able to show all reports @Johann150
- Client: fix settings page @tamaina
- Client: fix profile tabs @futchitwo
- Client: fix popout URL @futchitwo
- Client: correctly handle MiAuth URLs with query string @sn0w
- Client: ノート詳細ページの新しいノートを表示する機能の動作が正しくなるように修正する @xianonn
- MFM: more animated functions support `speed` parameter @futchitwo
- MFM: limit large MFM @Johann150
## 12.110.1 (2022/04/23)

View file

@ -71,13 +71,15 @@ For now, basically only @syuilo has the authority to merge PRs into develop beca
However, minor fixes, refactoring, and urgent changes may be merged at the discretion of a contributor.
## Release
For now, basically only @syuilo has the authority to release Misskey.
However, in case of emergency, a release can be made at the discretion of a contributor.
### Release Instructions
1. commit version changes in the `develop` branch ([package.json](https://github.com/misskey-dev/misskey/blob/develop/package.json))
2. follow the `master` branch to the `develop` branch.
3. Create a [release of GitHub](https://github.com/misskey-dev/misskey/releases)
1. Commit version changes in the `develop` branch ([package.json](https://github.com/misskey-dev/misskey/blob/develop/package.json))
2. Create a release PR.
- Into `master` from `develop` branch.
- The title must be in the format `Release: x.y.z`.
- `x.y.z` is the new version you are trying to release.
- Assign about 2~3 reviewers.
3. The release PR is approved, merge it.
4. Create a [release of GitHub](https://github.com/misskey-dev/misskey/releases)
- The target branch must be `master`
- The tag name must be the version

View file

@ -5,6 +5,6 @@
"loader=./test/loader.js"
],
"slow": 1000,
"timeout": 3000,
"timeout": 10000,
"exit": true
}

View file

@ -101,7 +101,7 @@
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"style-loader": "3.3.1",
"summaly": "2.5.0",
"summaly": "2.5.1",
"syslog-pro": "1.0.0",
"systeminformation": "5.11.15",
"tinycolor2": "1.4.2",

View file

@ -73,6 +73,7 @@ import { entities as charts } from '@/services/chart/entities.js';
import { Webhook } from '@/models/entities/webhook.js';
import { envOption } from '../env.js';
import { dbLogger } from './logger.js';
import { redisClient } from './redis.js';
const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false);
@ -207,7 +208,15 @@ export const db = new DataSource({
migrations: ['../../migration/*.js'],
});
export async function initDb() {
export async function initDb(force = false) {
if (force) {
if (db.isInitialized) {
await db.destroy();
}
await db.initialize();
return;
}
if (db.isInitialized) {
// nop
} else {
@ -217,6 +226,7 @@ export async function initDb() {
export async function resetDb() {
const reset = async () => {
await redisClient.FLUSHDB();
const tables = await db.query(`SELECT relname AS "table"
FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
WHERE nspname NOT IN ('pg_catalog', 'information_schema')

View file

@ -29,7 +29,9 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
getPublicProperties(file: DriveFile): DriveFile['properties'] {
if (file.properties.orientation != null) {
const properties = structuredClone(file.properties);
// TODO
//const properties = structuredClone(file.properties);
const properties = JSON.parse(JSON.stringify(file.properties));
if (file.properties.orientation >= 5) {
[properties.width, properties.height] = [properties.height, properties.width];
}

View file

@ -5,14 +5,52 @@ import { User, IRemoteUser, CacheableRemoteUser, CacheableUser } from '@/models/
import { UserPublickey } from '@/models/entities/user-publickey.js';
import { MessagingMessage } from '@/models/entities/messaging-message.js';
import { Notes, Users, UserPublickeys, MessagingMessages } from '@/models/index.js';
import { IObject, getApId } from './type.js';
import { resolvePerson } from './models/person.js';
import { Cache } from '@/misc/cache.js';
import { uriPersonCache, userByIdCache } from '@/services/user-cache.js';
import { IObject, getApId } from './type.js';
import { resolvePerson } from './models/person.js';
const publicKeyCache = new Cache<UserPublickey | null>(Infinity);
const publicKeyByUserIdCache = new Cache<UserPublickey | null>(Infinity);
export type UriParseResult = {
/** wether the URI was generated by us */
local: true;
/** id in DB */
id: string;
/** hint of type, e.g. "notes", "users" */
type: string;
/** any remaining text after type and id, not including the slash after id. undefined if empty */
rest?: string;
} | {
/** wether the URI was generated by us */
local: false;
/** uri in DB */
uri: string;
};
export function parseUri(value: string | IObject): UriParseResult {
const uri = getApId(value);
// the host part of a URL is case insensitive, so use the 'i' flag.
const localRegex = new RegExp('^' + escapeRegexp(config.url) + '/(\\w+)/(\\w+)(?:\/(.+))?', 'i');
const matchLocal = uri.match(localRegex);
if (matchLocal) {
return {
local: true,
type: matchLocal[1],
id: matchLocal[2],
rest: matchLocal[3],
};
} else {
return {
local: false,
uri,
};
}
}
export default class DbResolver {
constructor() {
}
@ -21,60 +59,54 @@ export default class DbResolver {
* AP Note => Misskey Note in DB
*/
public async getNoteFromApId(value: string | IObject): Promise<Note | null> {
const parsed = this.parseUri(value);
const parsed = parseUri(value);
if (parsed.local) {
if (parsed.type !== 'notes') return null;
if (parsed.id) {
return await Notes.findOneBy({
id: parsed.id,
});
}
if (parsed.uri) {
} else {
return await Notes.findOneBy({
uri: parsed.uri,
});
}
return null;
}
public async getMessageFromApId(value: string | IObject): Promise<MessagingMessage | null> {
const parsed = this.parseUri(value);
const parsed = parseUri(value);
if (parsed.local) {
if (parsed.type !== 'notes') return null;
if (parsed.id) {
return await MessagingMessages.findOneBy({
id: parsed.id,
});
}
if (parsed.uri) {
} else {
return await MessagingMessages.findOneBy({
uri: parsed.uri,
});
}
return null;
}
/**
* AP Person => Misskey User in DB
*/
public async getUserFromApId(value: string | IObject): Promise<CacheableUser | null> {
const parsed = this.parseUri(value);
const parsed = parseUri(value);
if (parsed.local) {
if (parsed.type !== 'users') return null;
if (parsed.id) {
return await userByIdCache.fetchMaybe(parsed.id, () => Users.findOneBy({
id: parsed.id,
}).then(x => x ?? undefined)) ?? null;
}
if (parsed.uri) {
} else {
return await uriPersonCache.fetch(parsed.uri, () => Users.findOneBy({
uri: parsed.uri,
}));
}
return null;
}
/**
@ -120,31 +152,4 @@ export default class DbResolver {
key,
};
}
public parseUri(value: string | IObject): UriParseResult {
const uri = getApId(value);
const localRegex = new RegExp('^' + escapeRegexp(config.url) + '/' + '(\\w+)' + '/' + '(\\w+)');
const matchLocal = uri.match(localRegex);
if (matchLocal) {
return {
type: matchLocal[1],
id: matchLocal[2],
};
} else {
return {
uri,
};
}
}
}
type UriParseResult = {
/** id in DB (local object only) */
id?: string;
/** uri in DB (remote object only) */
uri?: string;
/** hint of type (local object only, ex: notes, users) */
type?: string
};

View file

@ -3,8 +3,6 @@ import { Note } from '@/models/entities/note.js';
import { toHtml } from '../../../mfm/to-html.js';
export default function(note: Note) {
let html = note.text ? toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers)) : null;
if (html == null) html = '<p>.</p>';
return html;
if (!note.text) return '';
return toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers));
}

View file

@ -1,8 +1,20 @@
import config from '@/config/index.js';
import { ILocalUser, IRemoteUser } from '@/models/entities/user.js';
import { Blocking } from '@/models/entities/blocking.js';
export default (blocker: ILocalUser, blockee: IRemoteUser) => ({
/**
* Renders a block into its ActivityPub representation.
*
* @param block The block to be rendered. The blockee relation must be loaded.
*/
export function renderBlock(block: Blocking) {
if (block.blockee?.url == null) {
throw new Error('renderBlock: missing blockee uri');
}
return {
type: 'Block',
actor: `${config.url}/users/${blocker.id}`,
object: blockee.uri,
});
id: `${config.url}/blocks/${block.id}`,
actor: `${config.url}/users/${block.blockerId}`,
object: block.blockee.uri,
};
}

View file

@ -4,12 +4,11 @@ import { Users } from '@/models/index.js';
export default (follower: { id: User['id']; host: User['host']; uri: User['host'] }, followee: { id: User['id']; host: User['host']; uri: User['host'] }, requestId?: string) => {
const follow = {
id: requestId ?? `${config.url}/follows/${follower.id}/${followee.id}`,
type: 'Follow',
actor: Users.isLocalUser(follower) ? `${config.url}/users/${follower.id}` : follower.uri,
object: Users.isLocalUser(followee) ? `${config.url}/users/${followee.id}` : followee.uri,
} as any;
if (requestId) follow.id = requestId;
return follow;
};

View file

@ -82,15 +82,15 @@ export default async function renderNote(note: Note, dive = true, isTalk = false
const files = await getPromisedFiles(note.fileIds);
const text = note.text;
// text should never be undefined
const text = note.text ?? null;
let poll: Poll | null = null;
if (note.hasPoll) {
poll = await Polls.findOneBy({ noteId: note.id });
}
let apText = text;
if (apText == null) apText = '';
let apText = text ?? '';
if (quote) {
apText += `\n\nRE: ${quote}`;

View file

@ -3,9 +3,18 @@ import { getJson } from '@/misc/fetch.js';
import { ILocalUser } from '@/models/entities/user.js';
import { getInstanceActor } from '@/services/instance-actor.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { extractDbHost } from '@/misc/convert-host.js';
import { extractDbHost, isSelfHost } from '@/misc/convert-host.js';
import { signedGet } from './request.js';
import { IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type.js';
import { FollowRequests, Notes, NoteReactions, Polls, Users } from '@/models/index.js';
import { parseUri } from './db-resolver.js';
import renderNote from '@/remote/activitypub/renderer/note.js';
import { renderLike } from '@/remote/activitypub/renderer/like.js';
import { renderPerson } from '@/remote/activitypub/renderer/person.js';
import renderQuestion from '@/remote/activitypub/renderer/question.js';
import renderCreate from '@/remote/activitypub/renderer/create.js';
import { renderActivity } from '@/remote/activitypub/renderer/index.js';
import renderFollow from '@/remote/activitypub/renderer/follow.js';
export default class Resolver {
private history: Set<string>;
@ -40,14 +49,25 @@ export default class Resolver {
return value;
}
if (value.includes('#')) {
// URLs with fragment parts cannot be resolved correctly because
// the fragment part does not get transmitted over HTTP(S).
// Avoid strange behaviour by not trying to resolve these at all.
throw new Error(`cannot resolve URL with fragment: ${value}`);
}
if (this.history.has(value)) {
throw new Error('cannot resolve already resolved one');
}
this.history.add(value);
const meta = await fetchMeta();
const host = extractDbHost(value);
if (isSelfHost(host)) {
return await this.resolveLocal(value);
}
const meta = await fetchMeta();
if (meta.blockedHosts.includes(host)) {
throw new Error('Instance is blocked');
}
@ -70,4 +90,44 @@ export default class Resolver {
return object;
}
private resolveLocal(url: string): Promise<IObject> {
const parsed = parseUri(url);
if (!parsed.local) throw new Error('resolveLocal: not local');
switch (parsed.type) {
case 'notes':
return Notes.findOneByOrFail({ id: parsed.id })
.then(note => {
if (parsed.rest === 'activity') {
// this refers to the create activity and not the note itself
return renderActivity(renderCreate(renderNote(note)));
} else {
return renderNote(note);
}
});
case 'users':
return Users.findOneByOrFail({ id: parsed.id })
.then(user => renderPerson(user as ILocalUser));
case 'questions':
// Polls are indexed by the note they are attached to.
return Promise.all([
Notes.findOneByOrFail({ id: parsed.id }),
Polls.findOneByOrFail({ noteId: parsed.id }),
])
.then(([note, poll]) => renderQuestion({ id: note.userId }, note, poll));
case 'likes':
return NoteReactions.findOneByOrFail({ id: parsed.id }).then(reaction => renderActivity(renderLike(reaction, { uri: null })));
case 'follows':
// rest should be <followee id>
if (parsed.rest == null || !/^\w+$/.test(parsed.rest)) throw new Error('resolveLocal: invalid follow URI');
return Promise.all(
[parsed.id, parsed.rest].map(id => Users.findOneByOrFail({ id }))
)
.then(([follower, followee]) => renderActivity(renderFollow(follower, followee, url)));
default:
throw new Error(`resolveLocal: type ${type} unhandled`);
}
}
}

View file

@ -15,9 +15,10 @@ import { inbox as processInbox } from '@/queue/index.js';
import { isSelfHost } from '@/misc/convert-host.js';
import { Notes, Users, Emojis, NoteReactions } from '@/models/index.js';
import { ILocalUser, User } from '@/models/entities/user.js';
import { In, IsNull } from 'typeorm';
import { In, IsNull, Not } from 'typeorm';
import { renderLike } from '@/remote/activitypub/renderer/like.js';
import { getUserKeypair } from '@/misc/keypair-store.js';
import renderFollow from '@/remote/activitypub/renderer/follow.js';
// Init router
const router = new Router();
@ -224,4 +225,30 @@ router.get('/likes/:like', async ctx => {
setResponseType(ctx);
});
// follow
router.get('/follows/:follower/:followee', async ctx => {
// This may be used before the follow is completed, so we do not
// check if the following exists.
const [follower, followee] = await Promise.all([
Users.findOneBy({
id: ctx.params.follower,
host: IsNull(),
}),
Users.findOneBy({
id: ctx.params.followee,
host: Not(IsNull()),
}),
]);
if (follower == null || followee == null) {
ctx.status = 404;
return;
}
ctx.body = renderActivity(renderFollow(follower, followee));
ctx.set('Cache-Control', 'public, max-age=180');
setResponseType(ctx);
});
export default router;

View file

@ -1,5 +1,5 @@
import { Signins, UserProfiles, Users } from '@/models/index.js';
import define from '../../define.js';
import { Users } from '@/models/index.js';
export const meta = {
tags: ['admin'],
@ -23,9 +23,12 @@ export const paramDef = {
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, me) => {
const user = await Users.findOneBy({ id: ps.userId });
const [user, profile] = await Promise.all([
Users.findOneBy({ id: ps.userId }),
UserProfiles.findOneBy({ userId: ps.userId })
]);
if (user == null) {
if (user == null || profile == null) {
throw new Error('user not found');
}
@ -34,8 +37,37 @@ export default define(meta, paramDef, async (ps, me) => {
throw new Error('cannot show info of admin');
}
if (!_me.isAdmin) {
return {
...user,
token: user.token != null ? '<MASKED>' : user.token,
isModerator: user.isModerator,
isSilenced: user.isSilenced,
isSuspended: user.isSuspended,
};
}
const maskedKeys = ['accessToken', 'accessTokenSecret', 'refreshToken'];
Object.keys(profile.integrations).forEach(integration => {
maskedKeys.forEach(key => profile.integrations[integration][key] = '<MASKED>');
});
const signins = await Signins.findBy({ userId: user.id });
return {
email: profile.email,
emailVerified: profile.emailVerified,
autoAcceptFollowed: profile.autoAcceptFollowed,
noCrawle: profile.noCrawle,
alwaysMarkNsfw: profile.alwaysMarkNsfw,
carefulBot: profile.carefulBot,
injectFeaturedNote: profile.injectFeaturedNote,
receiveAnnouncementEmail: profile.receiveAnnouncementEmail,
integrations: profile.integrations,
mutedWords: profile.mutedWords,
mutedInstances: profile.mutedInstances,
mutingNotificationTypes: profile.mutingNotificationTypes,
isModerator: user.isModerator,
isSilenced: user.isSilenced,
isSuspended: user.isSuspended,
signins,
};
});

View file

@ -3,7 +3,9 @@ import { fetchMeta } from '@/misc/fetch-meta.js';
import manifest from './manifest.json' assert { type: 'json' };
export const manifestHandler = async (ctx: Koa.Context) => {
const res = structuredClone(manifest);
// TODO
//const res = structuredClone(manifest);
const res = JSON.parse(JSON.stringify(manifest));
const instance = await fetchMeta(true);

View file

@ -2,9 +2,10 @@ import { publishMainStream, publishUserEvent } from '@/services/stream.js';
import { renderActivity } from '@/remote/activitypub/renderer/index.js';
import renderFollow from '@/remote/activitypub/renderer/follow.js';
import renderUndo from '@/remote/activitypub/renderer/undo.js';
import renderBlock from '@/remote/activitypub/renderer/block.js';
import { renderBlock } from '@/remote/activitypub/renderer/block.js';
import { deliver } from '@/queue/index.js';
import renderReject from '@/remote/activitypub/renderer/reject.js';
import { Blocking } from '@/models/entities/blocking.js';
import { User } from '@/models/entities/user.js';
import { Blockings, Users, FollowRequests, Followings, UserListJoinings, UserLists } from '@/models/index.js';
import { perUserFollowingChart } from '@/services/chart/index.js';
@ -22,15 +23,19 @@ export default async function(blocker: User, blockee: User) {
removeFromList(blockee, blocker),
]);
await Blockings.insert({
const blocking = {
id: genId(),
createdAt: new Date(),
blocker,
blockerId: blocker.id,
blockee,
blockeeId: blockee.id,
});
} as Blocking;
await Blockings.insert(blocking);
if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) {
const content = renderActivity(renderBlock(blocker, blockee));
const content = renderActivity(renderBlock(blocking));
deliver(blocker, content, blockee.inbox);
}
}

View file

@ -1,5 +1,5 @@
import { renderActivity } from '@/remote/activitypub/renderer/index.js';
import renderBlock from '@/remote/activitypub/renderer/block.js';
import { renderBlock } from '@/remote/activitypub/renderer/block.js';
import renderUndo from '@/remote/activitypub/renderer/undo.js';
import { deliver } from '@/queue/index.js';
import Logger from '../logger.js';
@ -19,11 +19,16 @@ export default async function(blocker: CacheableUser, blockee: CacheableUser) {
return;
}
// Since we already have the blocker and blockee, we do not need to fetch
// them in the query above and can just manually insert them here.
blocking.blocker = blocker;
blocking.blockee = blockee;
Blockings.delete(blocking.id);
// deliver if remote bloking
if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) {
const content = renderActivity(renderUndo(renderBlock(blocker, blockee), blocker));
const content = renderActivity(renderUndo(renderBlock(blocking), blocker));
deliver(blocker, content, blockee.inbox);
}
}

View file

@ -11,6 +11,11 @@ import { entity as PerUserFollowingChart } from './charts/entities/per-user-foll
import { entity as PerUserDriveChart } from './charts/entities/per-user-drive.js';
import { entity as ApRequestChart } from './charts/entities/ap-request.js';
import { entity as TestChart } from './charts/entities/test.js';
import { entity as TestGroupedChart } from './charts/entities/test-grouped.js';
import { entity as TestUniqueChart } from './charts/entities/test-unique.js';
import { entity as TestIntersectionChart } from './charts/entities/test-intersection.js';
export const entities = [
FederationChart.hour, FederationChart.day,
NotesChart.hour, NotesChart.day,
@ -24,4 +29,11 @@ export const entities = [
PerUserFollowingChart.hour, PerUserFollowingChart.day,
PerUserDriveChart.hour, PerUserDriveChart.day,
ApRequestChart.hour, ApRequestChart.day,
...(process.env.NODE_ENV === 'test' ? [
TestChart.hour, TestChart.day,
TestGroupedChart.hour, TestGroupedChart.day,
TestUniqueChart.hour, TestUniqueChart.day,
TestIntersectionChart.hour, TestIntersectionChart.day,
] : []),
];

View file

@ -1,4 +1,4 @@
import { createSystemUser } from './create-system-user.js';
import { IsNull } from 'typeorm';
import { renderFollowRelay } from '@/remote/activitypub/renderer/follow-relay.js';
import { renderActivity, attachLdSignature } from '@/remote/activitypub/renderer/index.js';
import renderUndo from '@/remote/activitypub/renderer/undo.js';
@ -8,7 +8,7 @@ import { Users, Relays } from '@/models/index.js';
import { genId } from '@/misc/gen-id.js';
import { Cache } from '@/misc/cache.js';
import { Relay } from '@/models/entities/relay.js';
import { IsNull } from 'typeorm';
import { createSystemUser } from './create-system-user.js';
const ACTOR_USERNAME = 'relay.actor' as const;
@ -88,7 +88,9 @@ export async function deliverToRelays(user: { id: User['id']; host: null; }, act
}));
if (relays.length === 0) return;
const copy = structuredClone(activity);
// TODO
//const copy = structuredClone(activity);
const copy = JSON.parse(JSON.stringify(activity));
if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public'];
const signed = await attachLdSignature(copy, user);

View file

@ -2,11 +2,13 @@ process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import rndstr from 'rndstr';
import { initDb } from '../src/db/postgre.js';
import { initTestDb } from './utils.js';
describe('ActivityPub', () => {
before(async () => {
await initTestDb();
//await initTestDb();
await initDb();
});
describe('Parse minimum object', () => {

View file

@ -6,26 +6,17 @@ import TestChart from '../src/services/chart/charts/test.js';
import TestGroupedChart from '../src/services/chart/charts/test-grouped.js';
import TestUniqueChart from '../src/services/chart/charts/test-unique.js';
import TestIntersectionChart from '../src/services/chart/charts/test-intersection.js';
import * as _TestChart from '../src/services/chart/charts/entities/test.js';
import * as _TestGroupedChart from '../src/services/chart/charts/entities/test-grouped.js';
import * as _TestUniqueChart from '../src/services/chart/charts/entities/test-unique.js';
import * as _TestIntersectionChart from '../src/services/chart/charts/entities/test-intersection.js';
import { async, initTestDb } from './utils.js';
import { initDb } from '../src/db/postgre.js';
describe('Chart', () => {
let testChart: TestChart;
let testGroupedChart: TestGroupedChart;
let testUniqueChart: TestUniqueChart;
let testIntersectionChart: TestIntersectionChart;
let clock: lolex.Clock;
let clock: lolex.InstalledClock;
beforeEach(async(async () => {
await initTestDb(false, [
_TestChart.entity.hour, _TestChart.entity.day,
_TestGroupedChart.entity.hour, _TestGroupedChart.entity.day,
_TestUniqueChart.entity.hour, _TestUniqueChart.entity.day,
_TestIntersectionChart.entity.hour, _TestIntersectionChart.entity.day,
]);
beforeEach(async () => {
await initDb(true);
testChart = new TestChart();
testGroupedChart = new TestGroupedChart();
@ -34,14 +25,15 @@ describe('Chart', () => {
clock = lolex.install({
now: new Date(Date.UTC(2000, 0, 1, 0, 0, 0)),
shouldClearNativeTimers: true,
});
});
}));
afterEach(async(async () => {
afterEach(() => {
clock.uninstall();
}));
});
it('Can updates', async(async () => {
it('Can updates', async () => {
await testChart.increment();
await testChart.save();
@ -63,9 +55,9 @@ describe('Chart', () => {
total: [1, 0, 0],
},
});
}));
});
it('Can updates (dec)', async(async () => {
it('Can updates (dec)', async () => {
await testChart.decrement();
await testChart.save();
@ -87,9 +79,9 @@ describe('Chart', () => {
total: [-1, 0, 0],
},
});
}));
});
it('Empty chart', async(async () => {
it('Empty chart', async () => {
const chartHours = await testChart.getChart('hour', 3, null);
const chartDays = await testChart.getChart('day', 3, null);
@ -108,9 +100,9 @@ describe('Chart', () => {
total: [0, 0, 0],
},
});
}));
});
it('Can updates at multiple times at same time', async(async () => {
it('Can updates at multiple times at same time', async () => {
await testChart.increment();
await testChart.increment();
await testChart.increment();
@ -134,9 +126,9 @@ describe('Chart', () => {
total: [3, 0, 0],
},
});
}));
});
it('複数回saveされてもデータの更新は一度だけ', async(async () => {
it('複数回saveされてもデータの更新は一度だけ', async () => {
await testChart.increment();
await testChart.save();
await testChart.save();
@ -160,9 +152,9 @@ describe('Chart', () => {
total: [1, 0, 0],
},
});
}));
});
it('Can updates at different times', async(async () => {
it('Can updates at different times', async () => {
await testChart.increment();
await testChart.save();
@ -189,11 +181,11 @@ describe('Chart', () => {
total: [2, 0, 0],
},
});
}));
});
// 仕様上はこうなってほしいけど、実装は難しそうなのでskip
/*
it('Can updates at different times without save', async(async () => {
it('Can updates at different times without save', async () => {
await testChart.increment();
clock.tick('01:00:00');
@ -219,10 +211,10 @@ describe('Chart', () => {
total: [2, 0, 0]
},
});
}));
});
*/
it('Can padding', async(async () => {
it('Can padding', async () => {
await testChart.increment();
await testChart.save();
@ -249,10 +241,10 @@ describe('Chart', () => {
total: [2, 0, 0],
},
});
}));
});
// 要求された範囲にログがひとつもない場合でもパディングできる
it('Can padding from past range', async(async () => {
it('Can padding from past range', async () => {
await testChart.increment();
await testChart.save();
@ -276,11 +268,11 @@ describe('Chart', () => {
total: [1, 0, 0],
},
});
}));
});
// 要求された範囲の最も古い箇所に位置するログが存在しない場合でもパディングできる
// Issue #3190
it('Can padding from past range 2', async(async () => {
it('Can padding from past range 2', async () => {
await testChart.increment();
await testChart.save();
@ -307,9 +299,9 @@ describe('Chart', () => {
total: [2, 0, 0],
},
});
}));
});
it('Can specify offset', async(async () => {
it('Can specify offset', async () => {
await testChart.increment();
await testChart.save();
@ -336,9 +328,9 @@ describe('Chart', () => {
total: [2, 0, 0],
},
});
}));
});
it('Can specify offset (floor time)', async(async () => {
it('Can specify offset (floor time)', async () => {
clock.tick('00:30:00');
await testChart.increment();
@ -367,10 +359,10 @@ describe('Chart', () => {
total: [2, 0, 0],
},
});
}));
});
describe('Grouped', () => {
it('Can updates', async(async () => {
it('Can updates', async () => {
await testGroupedChart.increment('alice');
await testGroupedChart.save();
@ -410,11 +402,11 @@ describe('Chart', () => {
total: [0, 0, 0],
},
});
}));
});
});
describe('Unique increment', () => {
it('Can updates', async(async () => {
it('Can updates', async () => {
await testUniqueChart.uniqueIncrement('alice');
await testUniqueChart.uniqueIncrement('alice');
await testUniqueChart.uniqueIncrement('bob');
@ -430,10 +422,10 @@ describe('Chart', () => {
assert.deepStrictEqual(chartDays, {
foo: [2, 0, 0],
});
}));
});
describe('Intersection', () => {
it('条件が満たされていない場合はカウントされない', async(async () => {
it('条件が満たされていない場合はカウントされない', async () => {
await testIntersectionChart.addA('alice');
await testIntersectionChart.addA('bob');
await testIntersectionChart.addB('carol');
@ -453,9 +445,9 @@ describe('Chart', () => {
b: [1, 0, 0],
aAndB: [0, 0, 0],
});
}));
});
it('条件が満たされている場合にカウントされる', async(async () => {
it('条件が満たされている場合にカウントされる', async () => {
await testIntersectionChart.addA('alice');
await testIntersectionChart.addA('bob');
await testIntersectionChart.addB('carol');
@ -476,12 +468,12 @@ describe('Chart', () => {
b: [2, 0, 0],
aAndB: [1, 0, 0],
});
}));
});
});
});
describe('Resync', () => {
it('Can resync', async(async () => {
it('Can resync', async () => {
testChart.total = 1;
await testChart.resync();
@ -504,9 +496,9 @@ describe('Chart', () => {
total: [1, 0, 0],
},
});
}));
});
it('Can resync (2)', async(async () => {
it('Can resync (2)', async () => {
await testChart.increment();
await testChart.save();
@ -534,6 +526,6 @@ describe('Chart', () => {
total: [100, 0, 0],
},
});
}));
});
});
});

View file

@ -6520,16 +6520,17 @@ style-loader@3.3.1:
resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.1.tgz#057dfa6b3d4d7c7064462830f9113ed417d38575"
integrity sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==
summaly@2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/summaly/-/summaly-2.5.0.tgz#ec5af6e84857efcb6c844d896e83569e64a923ea"
integrity sha512-IzvO2s7yj/PUyH42qWjVjSPpIiPlgTRWGh33t4cIZKOqPQJ2INo7e83hXhHFr4hXTb3JRcIdCuM1ELjlrujiUQ==
summaly@2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/summaly/-/summaly-2.5.1.tgz#742fe6631987f84ad2e95d2b0f7902ec57e0f6b3"
integrity sha512-WWvl7rLs3wm61Xc2JqgTbSuqtIOmGqKte+rkbnxe6ISy4089lQ+7F2ajooQNee6PWHl9kZ27SDd1ZMoL3/6R4A==
dependencies:
cheerio "0.22.0"
debug "4.3.3"
escape-regexp "0.0.1"
got "11.5.1"
html-entities "2.3.2"
iconv-lite "0.6.3"
jschardet "3.0.0"
koa "2.13.4"
private-ip "2.3.3"

View file

@ -39,8 +39,8 @@ export default defineComponent({
inject: {
sideViewHook: {
default: null
}
default: null,
},
},
provide() {
@ -94,31 +94,31 @@ export default defineComponent({
}, {
icon: 'fas fa-expand-alt',
text: this.$ts.showInPage,
action: this.expand
action: this.expand,
}, this.sideViewHook ? {
icon: 'fas fa-columns',
text: this.$ts.openInSideView,
action: () => {
this.sideViewHook(this.path);
this.$refs.window.close();
}
},
} : undefined, {
icon: 'fas fa-external-link-alt',
text: this.$ts.popout,
action: this.popout
action: this.popout,
}, null, {
icon: 'fas fa-external-link-alt',
text: this.$ts.openInNewTab,
action: () => {
window.open(this.url, '_blank');
this.$refs.window.close();
}
},
}, {
icon: 'fas fa-link',
text: this.$ts.copyLink,
action: () => {
copyToClipboard(this.url);
}
},
}];
},
},
@ -155,7 +155,7 @@ export default defineComponent({
onContextmenu(ev: MouseEvent) {
os.contextMenu(this.contextmenu, ev);
}
},
},
});
</script>

View file

@ -222,7 +222,7 @@ function react(viaKeyboard = false): void {
reactionPicker.show(reactButton.value, reaction => {
os.api('notes/reactions/create', {
noteId: appearNote.id,
reaction: reaction
reaction: reaction,
});
}, () => {
focus();
@ -233,7 +233,7 @@ function undoReact(note): void {
const oldReaction = note.myReaction;
if (!oldReaction) return;
os.api('notes/reactions/delete', {
noteId: note.id
noteId: note.id,
});
}
@ -257,7 +257,7 @@ function onContextmenu(ev: MouseEvent): void {
function menu(viaKeyboard = false): void {
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton }), menuButton.value, {
viaKeyboard
viaKeyboard,
}).then(focus);
}
@ -269,12 +269,12 @@ function showRenoteMenu(viaKeyboard = false): void {
danger: true,
action: () => {
os.api('notes/delete', {
noteId: note.id
noteId: note.id,
});
isDeleted.value = true;
}
},
}], renoteTime.value, {
viaKeyboard: viaKeyboard
viaKeyboard: viaKeyboard,
});
}
@ -288,14 +288,14 @@ function blur() {
os.api('notes/children', {
noteId: appearNote.id,
limit: 30
limit: 30,
}).then(res => {
replies.value = res;
});
if (appearNote.replyId) {
os.api('notes/conversation', {
noteId: appearNote.replyId
noteId: appearNote.replyId,
}).then(res => {
conversation.value = res.reverse();
});

View file

@ -210,7 +210,7 @@ function react(viaKeyboard = false): void {
reactionPicker.show(reactButton.value, reaction => {
os.api('notes/reactions/create', {
noteId: appearNote.id,
reaction: reaction
reaction: reaction,
});
}, () => {
focus();
@ -221,7 +221,7 @@ function undoReact(note): void {
const oldReaction = note.myReaction;
if (!oldReaction) return;
os.api('notes/reactions/delete', {
noteId: note.id
noteId: note.id,
});
}
@ -245,7 +245,7 @@ function onContextmenu(ev: MouseEvent): void {
function menu(viaKeyboard = false): void {
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton }), menuButton.value, {
viaKeyboard
viaKeyboard,
}).then(focus);
}
@ -257,12 +257,12 @@ function showRenoteMenu(viaKeyboard = false): void {
danger: true,
action: () => {
os.api('notes/delete', {
noteId: note.id
noteId: note.id,
});
isDeleted.value = true;
}
},
}], renoteTime.value, {
viaKeyboard: viaKeyboard
viaKeyboard: viaKeyboard,
});
}
@ -284,7 +284,7 @@ function focusAfter() {
function readPromo() {
os.api('promo/read', {
noteId: appearNote.id
noteId: appearNote.id,
});
isDeleted.value = true;
}

View file

@ -1,5 +1,6 @@
<template>
<XModalWindow ref="dialog"
<XModalWindow
ref="dialog"
:width="400"
:height="450"
:with-ok-button="true"
@ -28,18 +29,18 @@
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import XModalWindow from '@/components/ui/modal-window.vue';
import { notificationTypes } from 'misskey-js';
import MkSwitch from './form/switch.vue';
import MkInfo from './ui/info.vue';
import MkButton from './ui/button.vue';
import { notificationTypes } from 'misskey-js';
import XModalWindow from '@/components/ui/modal-window.vue';
export default defineComponent({
components: {
XModalWindow,
MkSwitch,
MkInfo,
MkButton
MkButton,
},
props: {
@ -53,7 +54,7 @@ export default defineComponent({
type: Boolean,
required: false,
default: true,
}
},
},
emits: ['done', 'closed'],
@ -93,7 +94,7 @@ export default defineComponent({
for (const type in this.typesMap) {
this.typesMap[type as typeof notificationTypes[number]] = true;
}
}
}
},
},
});
</script>

View file

@ -16,7 +16,8 @@
<i v-else-if="notification.type === 'pollVote'" class="fas fa-poll-h"></i>
<i v-else-if="notification.type === 'pollEnded'" class="fas fa-poll-h"></i>
<!-- notification.reaction null になることはまずないがここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
<XReactionIcon v-else-if="notification.type === 'reaction'"
<XReactionIcon
v-else-if="notification.type === 'reaction'"
ref="reactionRef"
:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
:custom-emojis="notification.note.emojis"
@ -74,10 +75,10 @@
<script lang="ts">
import { defineComponent, ref, onMounted, onUnmounted, watch } from 'vue';
import * as misskey from 'misskey-js';
import { getNoteSummary } from '@/scripts/get-note-summary';
import XReactionIcon from './reaction-icon.vue';
import MkFollowButton from './follow-button.vue';
import XReactionTooltip from './reaction-tooltip.vue';
import { getNoteSummary } from '@/scripts/get-note-summary';
import { notePage } from '@/filters/note';
import { userPage } from '@/filters/user';
import { i18n } from '@/i18n';
@ -87,7 +88,7 @@ import { useTooltip } from '@/scripts/use-tooltip';
export default defineComponent({
components: {
XReactionIcon, MkFollowButton
XReactionIcon, MkFollowButton,
},
props: {
@ -116,7 +117,7 @@ export default defineComponent({
const readObserver = new IntersectionObserver((entries, observer) => {
if (!entries.some(entry => entry.isIntersecting)) return;
stream.send('readNotification', {
id: props.notification.id
id: props.notification.id,
});
observer.disconnect();
});

View file

@ -19,8 +19,7 @@
<script lang="ts" setup>
import { defineComponent, markRaw, onUnmounted, onMounted, computed, ref } from 'vue';
import { notificationTypes } from 'misskey-js';
import MkPagination from '@/components/ui/pagination.vue';
import { Paging } from '@/components/ui/pagination.vue';
import MkPagination, { Paging } from '@/components/ui/pagination.vue';
import XNotification from '@/components/notification.vue';
import XList from '@/components/date-separated-list.vue';
import XNote from '@/components/note.vue';
@ -49,14 +48,14 @@ const onNotification = (notification) => {
const isMuted = props.includeTypes ? !props.includeTypes.includes(notification.type) : $i.mutingNotificationTypes.includes(notification.type);
if (isMuted || document.visibilityState === 'visible') {
stream.send('readNotification', {
id: notification.id
id: notification.id,
});
}
if (!isMuted) {
pagingComponent.value.prepend({
...notification,
isRead: document.visibilityState === 'visible'
isRead: document.visibilityState === 'visible',
});
}
};

View file

@ -12,7 +12,7 @@ export default defineComponent({
props: {
value: {
type: Number,
required: true
required: true,
},
},
@ -26,7 +26,7 @@ export default defineComponent({
isZero,
number,
};
}
},
});
</script>

View file

@ -1,5 +1,6 @@
<template>
<div ref="itemsEl" v-hotkey="keymap"
<div
ref="itemsEl" v-hotkey="keymap"
class="rrevdjwt"
:class="{ center: align === 'center', asDrawer }"
:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
@ -162,6 +163,15 @@ function focusDown() {
position: relative;
}
&:not(:disabled):hover {
color: var(--accent);
text-decoration: none;
&:before {
background: var(--accentedBg);
}
}
&.danger {
color: #ff2a2a;
@ -191,15 +201,6 @@ function focusDown() {
}
}
&:not(:disabled):hover {
color: var(--accent);
text-decoration: none;
&:before {
background: var(--accentedBg);
}
}
&:not(:active):focus-visible {
box-shadow: 0 0 0 2px var(--focus) inset;
}

View file

@ -42,6 +42,7 @@ import MkSignin from '@/components/signin.vue';
import MkButton from '@/components/ui/button.vue';
import * as os from '@/os';
import { login } from '@/account';
import { appendQuery, query } from '@/scripts/url';
export default defineComponent({
components: {
@ -82,7 +83,9 @@ export default defineComponent({
this.state = 'accepted';
if (this.callback) {
location.href = `${this.callback}?session=${this.session}`;
location.href = appendQuery(this.callback, query({
session: this.session
}));
}
},
deny() {

View file

@ -54,6 +54,9 @@
<FormButton v-if="user.host != null" class="_formBlock" @click="updateRemoteUser"><i class="fas fa-sync"></i> {{ $ts.updateRemoteUser }}</FormButton>
</FormSection>
<MkObjectView v-if="info && $i.isAdmin" tall :value="info">
</MkObjectView>
<MkObjectView tall :value="user">
</MkObjectView>
</div>