Merge branch 'develop' into pr/ThatOneCalculator/8764
This commit is contained in:
commit
09d1ba9f68
21 changed files with 285 additions and 114 deletions
|
@ -1 +1 @@
|
||||||
v18.2.0
|
v16.0.0
|
||||||
|
|
46
CHANGELOG.md
46
CHANGELOG.md
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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.
|
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 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 target branch must be `master`
|
||||||
- The tag name must be the version
|
- The tag name must be the version
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
};
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) => ({
|
/**
|
||||||
|
* 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',
|
type: 'Block',
|
||||||
actor: `${config.url}/users/${blocker.id}`,
|
id: `${config.url}/blocks/${block.id}`,
|
||||||
object: blockee.uri,
|
actor: `${config.url}/users/${block.blockerId}`,
|
||||||
});
|
object: block.blockee.uri,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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}`;
|
||||||
|
|
|
@ -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`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
return {
|
||||||
...user,
|
isModerator: user.isModerator,
|
||||||
token: user.token != null ? '<MASKED>' : user.token,
|
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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue