Merge branch 'develop' into pr/ThatOneCalculator/8764

This commit is contained in:
tamaina 2022-06-04 08:34:56 +00:00
commit 2665322b23
21 changed files with 285 additions and 114 deletions

View file

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

View file

@ -10,18 +10,17 @@ You should also include the user name that made the change.
--> -->
## 12.x.x (unreleased) ## 12.x.x (unreleased)
### NOTE
- From this version, Node 18.0.0 or later is required.
### Improvements ### Improvements
- enhance: ドライブに画像ファイルをアップロードするときオリジナル画像を破棄してwebpublicのみ保持するオプション @tamaina - Supports Unicode Emoji 14.0 @mei23
- enhance: API: notifications/readは配列でも受け付けるように #7667 @tamaina - プッシュ通知を複数アカウント対応に #7667 @tamaina
- enhance: プッシュ通知を複数アカウント対応に #7667 @tamaina - プッシュ通知にクリックやactionを設定 #7667 @tamaina
- enhance: プッシュ通知にクリックやactionを設定 #7667 @tamaina - ドライブに画像ファイルをアップロードするときオリジナル画像を破棄してwebpublicのみ保持するオプション @tamaina
- replaced webpack with Vite @tamaina - Server: always remove completed tasks of job queue @Johann150
- update dependencies @syuilo - Client: make emoji stand out more on reaction button @Johann150
- enhance: display URL of QR code for TOTP registration @syuilo - Client: display URL of QR code for TOTP registration @tamaina
- enhance: Supports Unicode Emoji 14.0 @mei23 - 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 - The theme color is now better validated. @Johann150
Your own theme color may be unset if it was in an invalid format. Your own theme color may be unset if it was in an invalid format.
Admins should check their instance settings if in doubt. Admins should check their instance settings if in doubt.
@ -31,20 +30,31 @@ You should also include the user name that made the change.
- Migrate to Yarn v3.2.0 @ThatOneCalculator - Migrate to Yarn v3.2.0 @ThatOneCalculator
### Bugfixes ### Bugfixes
- Client: fix settings page @tamaina - Server: keep file order of note attachement @Johann150
- Client: fix profile tabs @futchitwo - Server: fix caching @Johann150
- Server: await promises when following or unfollowing users @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 missing foreign key for reports leading to reports page being unusable @Johann150
- Server: fix internal in-memory caching @Johann150 - Server: fix internal in-memory caching @Johann150
- Server: use correct order of attachments on notes @Johann150 - Server: use correct order of attachments on notes @Johann150
- Server: prevent crash when processing certain PNGs @syuilo - Server: prevent crash when processing certain PNGs @syuilo
- Server: Fix unable to generate video thumbnails @mei23 - Server: Fix unable to generate video thumbnails @mei23
- Server: Fix `Cannot find module` issue @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) ## 12.110.1 (2022/04/23)

View file

@ -71,15 +71,17 @@ 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. However, minor fixes, refactoring, and urgent changes may be merged at the discretion of a contributor.
## Release ## 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 ### Release Instructions
1. commit version changes in the `develop` branch ([package.json](https://github.com/misskey-dev/misskey/blob/develop/package.json)) 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. 2. Create a release PR.
3. Create a [release of GitHub](https://github.com/misskey-dev/misskey/releases) - Into `master` from `develop` branch.
- The target branch must be `master` - The title must be in the format `Release: x.y.z`.
- The tag name must be the version - `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
## Localization (l10n) ## Localization (l10n)
Misskey uses [Crowdin](https://crowdin.com/project/misskey) for localization management. Misskey uses [Crowdin](https://crowdin.com/project/misskey) for localization management.

View file

@ -109,7 +109,7 @@
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0", "stringz": "2.1.0",
"style-loader": "3.3.1", "style-loader": "3.3.1",
"summaly": "2.5.0", "summaly": "2.5.1",
"syslog-pro": "1.0.0", "syslog-pro": "1.0.0",
"systeminformation": "5.11.16", "systeminformation": "5.11.16",
"tinycolor2": "1.4.2", "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 { Webhook } from '@/models/entities/webhook.js';
import { envOption } from '../env.js'; import { envOption } from '../env.js';
import { dbLogger } from './logger.js'; import { dbLogger } from './logger.js';
import { redisClient } from './redis.js';
const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false); const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false);
@ -217,6 +218,7 @@ export async function initDb() {
export async function resetDb() { export async function resetDb() {
const reset = async () => { const reset = async () => {
await redisClient.FLUSHDB();
const tables = await db.query(`SELECT relname AS "table" const tables = await db.query(`SELECT relname AS "table"
FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
WHERE nspname NOT IN ('pg_catalog', 'information_schema') 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'] { getPublicProperties(file: DriveFile): DriveFile['properties'] {
if (file.properties.orientation != null) { 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) { if (file.properties.orientation >= 5) {
[properties.width, properties.height] = [properties.height, properties.width]; [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 { UserPublickey } from '@/models/entities/user-publickey.js';
import { MessagingMessage } from '@/models/entities/messaging-message.js'; import { MessagingMessage } from '@/models/entities/messaging-message.js';
import { Notes, Users, UserPublickeys, MessagingMessages } from '@/models/index.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 { Cache } from '@/misc/cache.js';
import { uriPersonCache, userByIdCache } from '@/services/user-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 publicKeyCache = new Cache<UserPublickey | null>(Infinity);
const publicKeyByUserIdCache = 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 { export default class DbResolver {
constructor() { constructor() {
} }
@ -21,60 +59,54 @@ export default class DbResolver {
* AP Note => Misskey Note in DB * AP Note => Misskey Note in DB
*/ */
public async getNoteFromApId(value: string | IObject): Promise<Note | null> { 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({ return await Notes.findOneBy({
id: parsed.id, id: parsed.id,
}); });
} } else {
if (parsed.uri) {
return await Notes.findOneBy({ return await Notes.findOneBy({
uri: parsed.uri, uri: parsed.uri,
}); });
} }
return null;
} }
public async getMessageFromApId(value: string | IObject): Promise<MessagingMessage | 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({ return await MessagingMessages.findOneBy({
id: parsed.id, id: parsed.id,
}); });
} } else {
if (parsed.uri) {
return await MessagingMessages.findOneBy({ return await MessagingMessages.findOneBy({
uri: parsed.uri, uri: parsed.uri,
}); });
} }
return null;
} }
/** /**
* AP Person => Misskey User in DB * AP Person => Misskey User in DB
*/ */
public async getUserFromApId(value: string | IObject): Promise<CacheableUser | null> { 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({ return await userByIdCache.fetchMaybe(parsed.id, () => Users.findOneBy({
id: parsed.id, id: parsed.id,
}).then(x => x ?? undefined)) ?? null; }).then(x => x ?? undefined)) ?? null;
} } else {
if (parsed.uri) {
return await uriPersonCache.fetch(parsed.uri, () => Users.findOneBy({ return await uriPersonCache.fetch(parsed.uri, () => Users.findOneBy({
uri: parsed.uri, uri: parsed.uri,
})); }));
} }
return null;
} }
/** /**
@ -120,31 +152,4 @@ export default class DbResolver {
key, 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'; import { toHtml } from '../../../mfm/to-html.js';
export default function(note: Note) { export default function(note: Note) {
let html = note.text ? toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers)) : null; if (!note.text) return '';
if (html == null) html = '<p>.</p>'; return toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers));
return html;
} }

View file

@ -1,8 +1,20 @@
import config from '@/config/index.js'; 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) => ({ /**
type: 'Block', * Renders a block into its ActivityPub representation.
actor: `${config.url}/users/${blocker.id}`, *
object: blockee.uri, * @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',
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) => { 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 = { const follow = {
id: requestId ?? `${config.url}/follows/${follower.id}/${followee.id}`,
type: 'Follow', type: 'Follow',
actor: Users.isLocalUser(follower) ? `${config.url}/users/${follower.id}` : follower.uri, actor: Users.isLocalUser(follower) ? `${config.url}/users/${follower.id}` : follower.uri,
object: Users.isLocalUser(followee) ? `${config.url}/users/${followee.id}` : followee.uri, object: Users.isLocalUser(followee) ? `${config.url}/users/${followee.id}` : followee.uri,
} as any; } as any;
if (requestId) follow.id = requestId;
return follow; 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 files = await getPromisedFiles(note.fileIds);
const text = note.text; // text should never be undefined
const text = note.text ?? null;
let poll: Poll | null = null; let poll: Poll | null = null;
if (note.hasPoll) { if (note.hasPoll) {
poll = await Polls.findOneBy({ noteId: note.id }); poll = await Polls.findOneBy({ noteId: note.id });
} }
let apText = text; let apText = text ?? '';
if (apText == null) apText = '';
if (quote) { if (quote) {
apText += `\n\nRE: ${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 { ILocalUser } from '@/models/entities/user.js';
import { getInstanceActor } from '@/services/instance-actor.js'; import { getInstanceActor } from '@/services/instance-actor.js';
import { fetchMeta } from '@/misc/fetch-meta.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 { signedGet } from './request.js';
import { IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type.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 { export default class Resolver {
private history: Set<string>; private history: Set<string>;
@ -40,14 +49,25 @@ export default class Resolver {
return value; 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)) { if (this.history.has(value)) {
throw new Error('cannot resolve already resolved one'); throw new Error('cannot resolve already resolved one');
} }
this.history.add(value); this.history.add(value);
const meta = await fetchMeta();
const host = extractDbHost(value); const host = extractDbHost(value);
if (isSelfHost(host)) {
return await this.resolveLocal(value);
}
const meta = await fetchMeta();
if (meta.blockedHosts.includes(host)) { if (meta.blockedHosts.includes(host)) {
throw new Error('Instance is blocked'); throw new Error('Instance is blocked');
} }
@ -70,4 +90,44 @@ export default class Resolver {
return object; 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 { isSelfHost } from '@/misc/convert-host.js';
import { Notes, Users, Emojis, NoteReactions } from '@/models/index.js'; import { Notes, Users, Emojis, NoteReactions } from '@/models/index.js';
import { ILocalUser, User } from '@/models/entities/user.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 { renderLike } from '@/remote/activitypub/renderer/like.js';
import { getUserKeypair } from '@/misc/keypair-store.js'; import { getUserKeypair } from '@/misc/keypair-store.js';
import renderFollow from '@/remote/activitypub/renderer/follow.js';
// Init router // Init router
const router = new Router(); const router = new Router();
@ -224,4 +225,30 @@ router.get('/likes/:like', async ctx => {
setResponseType(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; export default router;

View file

@ -1,5 +1,5 @@
import { Signins, UserProfiles, Users } from '@/models/index.js';
import define from '../../define.js'; import define from '../../define.js';
import { Users } from '@/models/index.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -23,9 +23,12 @@ export const paramDef = {
// eslint-disable-next-line import/no-default-export // eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, me) => { 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'); 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'); throw new Error('cannot show info of admin');
} }
if (!_me.isAdmin) {
return {
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 { return {
...user, email: profile.email,
token: user.token != null ? '<MASKED>' : user.token, 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' }; import manifest from './manifest.json' assert { type: 'json' };
export const manifestHandler = async (ctx: Koa.Context) => { 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); 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 { renderActivity } from '@/remote/activitypub/renderer/index.js';
import renderFollow from '@/remote/activitypub/renderer/follow.js'; import renderFollow from '@/remote/activitypub/renderer/follow.js';
import renderUndo from '@/remote/activitypub/renderer/undo.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 { deliver } from '@/queue/index.js';
import renderReject from '@/remote/activitypub/renderer/reject.js'; import renderReject from '@/remote/activitypub/renderer/reject.js';
import { Blocking } from '@/models/entities/blocking.js';
import { User } from '@/models/entities/user.js'; import { User } from '@/models/entities/user.js';
import { Blockings, Users, FollowRequests, Followings, UserListJoinings, UserLists } from '@/models/index.js'; import { Blockings, Users, FollowRequests, Followings, UserListJoinings, UserLists } from '@/models/index.js';
import { perUserFollowingChart } from '@/services/chart/index.js'; import { perUserFollowingChart } from '@/services/chart/index.js';
@ -22,15 +23,19 @@ export default async function(blocker: User, blockee: User) {
removeFromList(blockee, blocker), removeFromList(blockee, blocker),
]); ]);
await Blockings.insert({ const blocking = {
id: genId(), id: genId(),
createdAt: new Date(), createdAt: new Date(),
blocker,
blockerId: blocker.id, blockerId: blocker.id,
blockee,
blockeeId: blockee.id, blockeeId: blockee.id,
}); } as Blocking;
await Blockings.insert(blocking);
if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) { if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) {
const content = renderActivity(renderBlock(blocker, blockee)); const content = renderActivity(renderBlock(blocking));
deliver(blocker, content, blockee.inbox); deliver(blocker, content, blockee.inbox);
} }
} }

View file

@ -1,5 +1,5 @@
import { renderActivity } from '@/remote/activitypub/renderer/index.js'; 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 renderUndo from '@/remote/activitypub/renderer/undo.js';
import { deliver } from '@/queue/index.js'; import { deliver } from '@/queue/index.js';
import Logger from '../logger.js'; import Logger from '../logger.js';
@ -19,11 +19,16 @@ export default async function(blocker: CacheableUser, blockee: CacheableUser) {
return; 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); Blockings.delete(blocking.id);
// deliver if remote bloking // deliver if remote bloking
if (Users.isLocalUser(blocker) && Users.isRemoteUser(blockee)) { 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); deliver(blocker, content, blockee.inbox);
} }
} }

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 { renderFollowRelay } from '@/remote/activitypub/renderer/follow-relay.js';
import { renderActivity, attachLdSignature } from '@/remote/activitypub/renderer/index.js'; import { renderActivity, attachLdSignature } from '@/remote/activitypub/renderer/index.js';
import renderUndo from '@/remote/activitypub/renderer/undo.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 { genId } from '@/misc/gen-id.js';
import { Cache } from '@/misc/cache.js'; import { Cache } from '@/misc/cache.js';
import { Relay } from '@/models/entities/relay.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; 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; 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']; if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public'];
const signed = await attachLdSignature(copy, user); const signed = await attachLdSignature(copy, user);

View file

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

View file

@ -42,6 +42,7 @@ import MkSignin from '@/components/signin.vue';
import MkButton from '@/components/ui/button.vue'; import MkButton from '@/components/ui/button.vue';
import * as os from '@/os'; import * as os from '@/os';
import { login } from '@/account'; import { login } from '@/account';
import { appendQuery, query } from '@/scripts/url';
export default defineComponent({ export default defineComponent({
components: { components: {
@ -82,7 +83,9 @@ export default defineComponent({
this.state = 'accepted'; this.state = 'accepted';
if (this.callback) { if (this.callback) {
location.href = `${this.callback}?session=${this.session}`; location.href = appendQuery(this.callback, query({
session: this.session
}));
} }
}, },
deny() { 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> <FormButton v-if="user.host != null" class="_formBlock" @click="updateRemoteUser"><i class="fas fa-sync"></i> {{ $ts.updateRemoteUser }}</FormButton>
</FormSection> </FormSection>
<MkObjectView v-if="info && $i.isAdmin" tall :value="info">
</MkObjectView>
<MkObjectView tall :value="user"> <MkObjectView tall :value="user">
</MkObjectView> </MkObjectView>
</div> </div>