@ -0,0 +1,8 @@
|
||||
import config from '@/config/index';
|
||||
import { User } from '@/models/entities/user';
|
||||
|
||||
export default (object: any, user: { id: User['id']; host: null }) => ({
|
||||
type: 'Accept',
|
||||
actor: `${config.url}/users/${user.id}`,
|
||||
object
|
||||
});
|
9
packages/backend/src/remote/activitypub/renderer/add.ts
Normal file
9
packages/backend/src/remote/activitypub/renderer/add.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import config from '@/config/index';
|
||||
import { ILocalUser } from '@/models/entities/user';
|
||||
|
||||
export default (user: ILocalUser, target: any, object: any) => ({
|
||||
type: 'Add',
|
||||
actor: `${config.url}/users/${user.id}`,
|
||||
target,
|
||||
object
|
||||
});
|
29
packages/backend/src/remote/activitypub/renderer/announce.ts
Normal file
29
packages/backend/src/remote/activitypub/renderer/announce.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import config from '@/config/index';
|
||||
import { Note } from '@/models/entities/note';
|
||||
|
||||
export default (object: any, note: Note) => {
|
||||
const attributedTo = `${config.url}/users/${note.userId}`;
|
||||
|
||||
let to: string[] = [];
|
||||
let cc: string[] = [];
|
||||
|
||||
if (note.visibility === 'public') {
|
||||
to = ['https://www.w3.org/ns/activitystreams#Public'];
|
||||
cc = [`${attributedTo}/followers`];
|
||||
} else if (note.visibility === 'home') {
|
||||
to = [`${attributedTo}/followers`];
|
||||
cc = ['https://www.w3.org/ns/activitystreams#Public'];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: `${config.url}/notes/${note.id}/activity`,
|
||||
actor: `${config.url}/users/${note.userId}`,
|
||||
type: 'Announce',
|
||||
published: note.createdAt.toISOString(),
|
||||
to,
|
||||
cc,
|
||||
object
|
||||
};
|
||||
};
|
@ -0,0 +1,8 @@
|
||||
import config from '@/config/index';
|
||||
import { ILocalUser, IRemoteUser } from '@/models/entities/user';
|
||||
|
||||
export default (blocker: ILocalUser, blockee: IRemoteUser) => ({
|
||||
type: 'Block',
|
||||
actor: `${config.url}/users/${blocker.id}`,
|
||||
object: blockee.uri
|
||||
});
|
17
packages/backend/src/remote/activitypub/renderer/create.ts
Normal file
17
packages/backend/src/remote/activitypub/renderer/create.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import config from '@/config/index';
|
||||
import { Note } from '@/models/entities/note';
|
||||
|
||||
export default (object: any, note: Note) => {
|
||||
const activity = {
|
||||
id: `${config.url}/notes/${note.id}/activity`,
|
||||
actor: `${config.url}/users/${note.userId}`,
|
||||
type: 'Create',
|
||||
published: note.createdAt.toISOString(),
|
||||
object
|
||||
} as any;
|
||||
|
||||
if (object.to) activity.to = object.to;
|
||||
if (object.cc) activity.cc = object.cc;
|
||||
|
||||
return activity;
|
||||
};
|
@ -0,0 +1,9 @@
|
||||
import config from '@/config/index';
|
||||
import { User } from '@/models/entities/user';
|
||||
|
||||
export default (object: any, user: { id: User['id']; host: null }) => ({
|
||||
type: 'Delete',
|
||||
actor: `${config.url}/users/${user.id}`,
|
||||
object,
|
||||
published: new Date().toISOString(),
|
||||
});
|
@ -0,0 +1,9 @@
|
||||
import { DriveFile } from '@/models/entities/drive-file';
|
||||
import { DriveFiles } from '@/models/index';
|
||||
|
||||
export default (file: DriveFile) => ({
|
||||
type: 'Document',
|
||||
mediaType: file.type,
|
||||
url: DriveFiles.getPublicUrl(file),
|
||||
name: file.comment,
|
||||
});
|
14
packages/backend/src/remote/activitypub/renderer/emoji.ts
Normal file
14
packages/backend/src/remote/activitypub/renderer/emoji.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import config from '@/config/index';
|
||||
import { Emoji } from '@/models/entities/emoji';
|
||||
|
||||
export default (emoji: Emoji) => ({
|
||||
id: `${config.url}/emojis/${emoji.name}`,
|
||||
type: 'Emoji',
|
||||
name: `:${emoji.name}:`,
|
||||
updated: emoji.updatedAt != null ? emoji.updatedAt.toISOString() : new Date().toISOString,
|
||||
icon: {
|
||||
type: 'Image',
|
||||
mediaType: emoji.type || 'image/png',
|
||||
url: emoji.url
|
||||
}
|
||||
});
|
@ -0,0 +1,14 @@
|
||||
import config from '@/config/index';
|
||||
import { Relay } from '@/models/entities/relay';
|
||||
import { ILocalUser } from '@/models/entities/user';
|
||||
|
||||
export function renderFollowRelay(relay: Relay, relayActor: ILocalUser) {
|
||||
const follow = {
|
||||
id: `${config.url}/activities/follow-relay/${relay.id}`,
|
||||
type: 'Follow',
|
||||
actor: `${config.url}/users/${relayActor.id}`,
|
||||
object: 'https://www.w3.org/ns/activitystreams#Public'
|
||||
};
|
||||
|
||||
return follow;
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import config from '@/config/index';
|
||||
import { Users } from '@/models/index';
|
||||
import { User } from '@/models/entities/user';
|
||||
|
||||
/**
|
||||
* Convert (local|remote)(Follower|Followee)ID to URL
|
||||
* @param id Follower|Followee ID
|
||||
*/
|
||||
export default async function renderFollowUser(id: User['id']): Promise<any> {
|
||||
const user = await Users.findOneOrFail(id);
|
||||
return Users.isLocalUser(user) ? `${config.url}/users/${user.id}` : user.uri;
|
||||
}
|
15
packages/backend/src/remote/activitypub/renderer/follow.ts
Normal file
15
packages/backend/src/remote/activitypub/renderer/follow.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import config from '@/config/index';
|
||||
import { User } from '@/models/entities/user';
|
||||
import { Users } from '@/models/index';
|
||||
|
||||
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 = {
|
||||
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;
|
||||
};
|
@ -0,0 +1,7 @@
|
||||
import config from '@/config/index';
|
||||
|
||||
export default (tag: string) => ({
|
||||
type: 'Hashtag',
|
||||
href: `${config.url}/tags/${encodeURIComponent(tag)}`,
|
||||
name: `#${tag}`
|
||||
});
|
@ -0,0 +1,9 @@
|
||||
import { DriveFile } from '@/models/entities/drive-file';
|
||||
import { DriveFiles } from '@/models/index';
|
||||
|
||||
export default (file: DriveFile) => ({
|
||||
type: 'Image',
|
||||
url: DriveFiles.getPublicUrl(file),
|
||||
sensitive: file.isSensitive,
|
||||
name: file.comment
|
||||
});
|
59
packages/backend/src/remote/activitypub/renderer/index.ts
Normal file
59
packages/backend/src/remote/activitypub/renderer/index.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import config from '@/config/index';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { IActivity } from '../type';
|
||||
import { LdSignature } from '../misc/ld-signature';
|
||||
import { getUserKeypair } from '@/misc/keypair-store';
|
||||
import { User } from '@/models/entities/user';
|
||||
|
||||
export const renderActivity = (x: any): IActivity | null => {
|
||||
if (x == null) return null;
|
||||
|
||||
if (x !== null && typeof x === 'object' && x.id == null) {
|
||||
x.id = `${config.url}/${uuid()}`;
|
||||
}
|
||||
|
||||
return Object.assign({
|
||||
'@context': [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://w3id.org/security/v1',
|
||||
{
|
||||
// as non-standards
|
||||
manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
|
||||
sensitive: 'as:sensitive',
|
||||
Hashtag: 'as:Hashtag',
|
||||
quoteUrl: 'as:quoteUrl',
|
||||
// Mastodon
|
||||
toot: 'http://joinmastodon.org/ns#',
|
||||
Emoji: 'toot:Emoji',
|
||||
featured: 'toot:featured',
|
||||
discoverable: 'toot:discoverable',
|
||||
// schema
|
||||
schema: 'http://schema.org#',
|
||||
PropertyValue: 'schema:PropertyValue',
|
||||
value: 'schema:value',
|
||||
// Misskey
|
||||
misskey: `${config.url}/ns#`,
|
||||
'_misskey_content': 'misskey:_misskey_content',
|
||||
'_misskey_quote': 'misskey:_misskey_quote',
|
||||
'_misskey_reaction': 'misskey:_misskey_reaction',
|
||||
'_misskey_votes': 'misskey:_misskey_votes',
|
||||
'_misskey_talk': 'misskey:_misskey_talk',
|
||||
'isCat': 'misskey:isCat',
|
||||
// vcard
|
||||
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
||||
}
|
||||
]
|
||||
}, x);
|
||||
};
|
||||
|
||||
export const attachLdSignature = async (activity: any, user: { id: User['id']; host: null; }): Promise<IActivity | null> => {
|
||||
if (activity == null) return null;
|
||||
|
||||
const keypair = await getUserKeypair(user.id);
|
||||
|
||||
const ldSignature = new LdSignature();
|
||||
ldSignature.debug = false;
|
||||
activity = await ldSignature.signRsaSignature2017(activity, keypair.privateKey, `${config.url}/users/${user.id}#main-key`);
|
||||
|
||||
return activity;
|
||||
};
|
14
packages/backend/src/remote/activitypub/renderer/key.ts
Normal file
14
packages/backend/src/remote/activitypub/renderer/key.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import config from '@/config/index';
|
||||
import { ILocalUser } from '@/models/entities/user';
|
||||
import { UserKeypair } from '@/models/entities/user-keypair';
|
||||
import { createPublicKey } from 'crypto';
|
||||
|
||||
export default (user: ILocalUser, key: UserKeypair, postfix?: string) => ({
|
||||
id: `${config.url}/users/${user.id}${postfix || '/publickey'}`,
|
||||
type: 'Key',
|
||||
owner: `${config.url}/users/${user.id}`,
|
||||
publicKeyPem: createPublicKey(key.publicKey).export({
|
||||
type: 'spki',
|
||||
format: 'pem'
|
||||
})
|
||||
});
|
30
packages/backend/src/remote/activitypub/renderer/like.ts
Normal file
30
packages/backend/src/remote/activitypub/renderer/like.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import config from '@/config/index';
|
||||
import { NoteReaction } from '@/models/entities/note-reaction';
|
||||
import { Note } from '@/models/entities/note';
|
||||
import { Emojis } from '@/models/index';
|
||||
import renderEmoji from './emoji';
|
||||
|
||||
export const renderLike = async (noteReaction: NoteReaction, note: Note) => {
|
||||
const reaction = noteReaction.reaction;
|
||||
|
||||
const object = {
|
||||
type: 'Like',
|
||||
id: `${config.url}/likes/${noteReaction.id}`,
|
||||
actor: `${config.url}/users/${noteReaction.userId}`,
|
||||
object: note.uri ? note.uri : `${config.url}/notes/${noteReaction.noteId}`,
|
||||
content: reaction,
|
||||
_misskey_reaction: reaction
|
||||
} as any;
|
||||
|
||||
if (reaction.startsWith(':')) {
|
||||
const name = reaction.replace(/:/g, '');
|
||||
const emoji = await Emojis.findOne({
|
||||
name,
|
||||
host: null
|
||||
});
|
||||
|
||||
if (emoji) object.tag = [ renderEmoji(emoji) ];
|
||||
}
|
||||
|
||||
return object;
|
||||
};
|
@ -0,0 +1,9 @@
|
||||
import config from '@/config/index';
|
||||
import { User, ILocalUser } from '@/models/entities/user';
|
||||
import { Users } from '@/models/index';
|
||||
|
||||
export default (mention: User) => ({
|
||||
type: 'Mention',
|
||||
href: Users.isRemoteUser(mention) ? mention.uri : `${config.url}/users/${(mention as ILocalUser).id}`,
|
||||
name: Users.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as ILocalUser).username}`,
|
||||
});
|
168
packages/backend/src/remote/activitypub/renderer/note.ts
Normal file
168
packages/backend/src/remote/activitypub/renderer/note.ts
Normal file
@ -0,0 +1,168 @@
|
||||
import renderDocument from './document';
|
||||
import renderHashtag from './hashtag';
|
||||
import renderMention from './mention';
|
||||
import renderEmoji from './emoji';
|
||||
import config from '@/config/index';
|
||||
import toHtml from '../misc/get-note-html';
|
||||
import { Note, IMentionedRemoteUsers } from '@/models/entities/note';
|
||||
import { DriveFile } from '@/models/entities/drive-file';
|
||||
import { DriveFiles, Notes, Users, Emojis, Polls } from '@/models/index';
|
||||
import { In } from 'typeorm';
|
||||
import { Emoji } from '@/models/entities/emoji';
|
||||
import { Poll } from '@/models/entities/poll';
|
||||
|
||||
export default async function renderNote(note: Note, dive = true, isTalk = false): Promise<any> {
|
||||
const getPromisedFiles = async (ids: string[]) => {
|
||||
if (!ids || ids.length === 0) return [];
|
||||
const items = await DriveFiles.find({ id: In(ids) });
|
||||
return ids.map(id => items.find(item => item.id === id)).filter(item => item != null) as DriveFile[];
|
||||
};
|
||||
|
||||
let inReplyTo;
|
||||
let inReplyToNote: Note | undefined;
|
||||
|
||||
if (note.replyId) {
|
||||
inReplyToNote = await Notes.findOne(note.replyId);
|
||||
|
||||
if (inReplyToNote != null) {
|
||||
const inReplyToUser = await Users.findOne(inReplyToNote.userId);
|
||||
|
||||
if (inReplyToUser != null) {
|
||||
if (inReplyToNote.uri) {
|
||||
inReplyTo = inReplyToNote.uri;
|
||||
} else {
|
||||
if (dive) {
|
||||
inReplyTo = await renderNote(inReplyToNote, false);
|
||||
} else {
|
||||
inReplyTo = `${config.url}/notes/${inReplyToNote.id}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
inReplyTo = null;
|
||||
}
|
||||
|
||||
let quote;
|
||||
|
||||
if (note.renoteId) {
|
||||
const renote = await Notes.findOne(note.renoteId);
|
||||
|
||||
if (renote) {
|
||||
quote = renote.uri ? renote.uri : `${config.url}/notes/${renote.id}`;
|
||||
}
|
||||
}
|
||||
|
||||
const user = await Users.findOneOrFail(note.userId);
|
||||
|
||||
const attributedTo = `${config.url}/users/${user.id}`;
|
||||
|
||||
const mentions = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri);
|
||||
|
||||
let to: string[] = [];
|
||||
let cc: string[] = [];
|
||||
|
||||
if (note.visibility === 'public') {
|
||||
to = ['https://www.w3.org/ns/activitystreams#Public'];
|
||||
cc = [`${attributedTo}/followers`].concat(mentions);
|
||||
} else if (note.visibility === 'home') {
|
||||
to = [`${attributedTo}/followers`];
|
||||
cc = ['https://www.w3.org/ns/activitystreams#Public'].concat(mentions);
|
||||
} else if (note.visibility === 'followers') {
|
||||
to = [`${attributedTo}/followers`];
|
||||
cc = mentions;
|
||||
} else {
|
||||
to = mentions;
|
||||
}
|
||||
|
||||
const mentionedUsers = note.mentions.length > 0 ? await Users.find({
|
||||
id: In(note.mentions)
|
||||
}) : [];
|
||||
|
||||
const hashtagTags = (note.tags || []).map(tag => renderHashtag(tag));
|
||||
const mentionTags = mentionedUsers.map(u => renderMention(u));
|
||||
|
||||
const files = await getPromisedFiles(note.fileIds);
|
||||
|
||||
const text = note.text;
|
||||
let poll: Poll | undefined;
|
||||
|
||||
if (note.hasPoll) {
|
||||
poll = await Polls.findOne({ noteId: note.id });
|
||||
}
|
||||
|
||||
let apText = text;
|
||||
if (apText == null) apText = '';
|
||||
|
||||
if (quote) {
|
||||
apText += `\n\nRE: ${quote}`;
|
||||
}
|
||||
|
||||
const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
|
||||
|
||||
const content = toHtml(Object.assign({}, note, {
|
||||
text: apText
|
||||
}));
|
||||
|
||||
const emojis = await getEmojis(note.emojis);
|
||||
const apemojis = emojis.map(emoji => renderEmoji(emoji));
|
||||
|
||||
const tag = [
|
||||
...hashtagTags,
|
||||
...mentionTags,
|
||||
...apemojis,
|
||||
];
|
||||
|
||||
const asPoll = poll ? {
|
||||
type: 'Question',
|
||||
content: toHtml(Object.assign({}, note, {
|
||||
text: text
|
||||
})),
|
||||
[poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt,
|
||||
[poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({
|
||||
type: 'Note',
|
||||
name: text,
|
||||
replies: {
|
||||
type: 'Collection',
|
||||
totalItems: poll!.votes[i]
|
||||
}
|
||||
}))
|
||||
} : {};
|
||||
|
||||
const asTalk = isTalk ? {
|
||||
_misskey_talk: true
|
||||
} : {};
|
||||
|
||||
return {
|
||||
id: `${config.url}/notes/${note.id}`,
|
||||
type: 'Note',
|
||||
attributedTo,
|
||||
summary,
|
||||
content,
|
||||
_misskey_content: text,
|
||||
_misskey_quote: quote,
|
||||
quoteUrl: quote,
|
||||
published: note.createdAt.toISOString(),
|
||||
to,
|
||||
cc,
|
||||
inReplyTo,
|
||||
attachment: files.map(renderDocument),
|
||||
sensitive: note.cw != null || files.some(file => file.isSensitive),
|
||||
tag,
|
||||
...asPoll,
|
||||
...asTalk
|
||||
};
|
||||
}
|
||||
|
||||
export async function getEmojis(names: string[]): Promise<Emoji[]> {
|
||||
if (names == null || names.length === 0) return [];
|
||||
|
||||
const emojis = await Promise.all(
|
||||
names.map(name => Emojis.findOne({
|
||||
name,
|
||||
host: null
|
||||
}))
|
||||
);
|
||||
|
||||
return emojis.filter(emoji => emoji != null) as Emoji[];
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Render OrderedCollectionPage
|
||||
* @param id URL of self
|
||||
* @param totalItems Number of total items
|
||||
* @param orderedItems Items
|
||||
* @param partOf URL of base
|
||||
* @param prev URL of prev page (optional)
|
||||
* @param next URL of next page (optional)
|
||||
*/
|
||||
export default function(id: string, totalItems: any, orderedItems: any, partOf: string, prev?: string, next?: string) {
|
||||
const page = {
|
||||
id,
|
||||
partOf,
|
||||
type: 'OrderedCollectionPage',
|
||||
totalItems,
|
||||
orderedItems
|
||||
} as any;
|
||||
|
||||
if (prev) page.prev = prev;
|
||||
if (next) page.next = next;
|
||||
|
||||
return page;
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Render OrderedCollection
|
||||
* @param id URL of self
|
||||
* @param totalItems Total number of items
|
||||
* @param first URL of first page (optional)
|
||||
* @param last URL of last page (optional)
|
||||
* @param orderedItems attached objects (optional)
|
||||
*/
|
||||
export default function(id: string | null, totalItems: any, first?: string, last?: string, orderedItems?: object) {
|
||||
const page: any = {
|
||||
id,
|
||||
type: 'OrderedCollection',
|
||||
totalItems,
|
||||
};
|
||||
|
||||
if (first) page.first = first;
|
||||
if (last) page.last = last;
|
||||
if (orderedItems) page.orderedItems = orderedItems;
|
||||
|
||||
return page;
|
||||
}
|
89
packages/backend/src/remote/activitypub/renderer/person.ts
Normal file
89
packages/backend/src/remote/activitypub/renderer/person.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { URL } from 'url';
|
||||
import * as mfm from 'mfm-js';
|
||||
import renderImage from './image';
|
||||
import renderKey from './key';
|
||||
import config from '@/config/index';
|
||||
import { ILocalUser } from '@/models/entities/user';
|
||||
import { toHtml } from '../../../mfm/to-html';
|
||||
import { getEmojis } from './note';
|
||||
import renderEmoji from './emoji';
|
||||
import { IIdentifier } from '../models/identifier';
|
||||
import renderHashtag from './hashtag';
|
||||
import { DriveFiles, UserProfiles } from '@/models/index';
|
||||
import { getUserKeypair } from '@/misc/keypair-store';
|
||||
|
||||
export async function renderPerson(user: ILocalUser) {
|
||||
const id = `${config.url}/users/${user.id}`;
|
||||
const isSystem = !!user.username.match(/\./);
|
||||
|
||||
const [avatar, banner, profile] = await Promise.all([
|
||||
user.avatarId ? DriveFiles.findOne(user.avatarId) : Promise.resolve(undefined),
|
||||
user.bannerId ? DriveFiles.findOne(user.bannerId) : Promise.resolve(undefined),
|
||||
UserProfiles.findOneOrFail(user.id)
|
||||
]);
|
||||
|
||||
const attachment: {
|
||||
type: 'PropertyValue',
|
||||
name: string,
|
||||
value: string,
|
||||
identifier?: IIdentifier
|
||||
}[] = [];
|
||||
|
||||
if (profile.fields) {
|
||||
for (const field of profile.fields) {
|
||||
attachment.push({
|
||||
type: 'PropertyValue',
|
||||
name: field.name,
|
||||
value: (field.value != null && field.value.match(/^https?:/))
|
||||
? `<a href="${new URL(field.value).href}" rel="me nofollow noopener" target="_blank">${new URL(field.value).href}</a>`
|
||||
: field.value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const emojis = await getEmojis(user.emojis);
|
||||
const apemojis = emojis.map(emoji => renderEmoji(emoji));
|
||||
|
||||
const hashtagTags = (user.tags || []).map(tag => renderHashtag(tag));
|
||||
|
||||
const tag = [
|
||||
...apemojis,
|
||||
...hashtagTags,
|
||||
];
|
||||
|
||||
const keypair = await getUserKeypair(user.id);
|
||||
|
||||
const person = {
|
||||
type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person',
|
||||
id,
|
||||
inbox: `${id}/inbox`,
|
||||
outbox: `${id}/outbox`,
|
||||
followers: `${id}/followers`,
|
||||
following: `${id}/following`,
|
||||
featured: `${id}/collections/featured`,
|
||||
sharedInbox: `${config.url}/inbox`,
|
||||
endpoints: { sharedInbox: `${config.url}/inbox` },
|
||||
url: `${config.url}/@${user.username}`,
|
||||
preferredUsername: user.username,
|
||||
name: user.name,
|
||||
summary: profile.description ? toHtml(mfm.parse(profile.description)) : null,
|
||||
icon: avatar ? renderImage(avatar) : null,
|
||||
image: banner ? renderImage(banner) : null,
|
||||
tag,
|
||||
manuallyApprovesFollowers: user.isLocked,
|
||||
discoverable: !!user.isExplorable,
|
||||
publicKey: renderKey(user, keypair, `#main-key`),
|
||||
isCat: user.isCat,
|
||||
attachment: attachment.length ? attachment : undefined
|
||||
} as any;
|
||||
|
||||
if (profile?.birthday) {
|
||||
person['vcard:bday'] = profile.birthday;
|
||||
}
|
||||
|
||||
if (profile?.location) {
|
||||
person['vcard:Address'] = profile.location;
|
||||
}
|
||||
|
||||
return person;
|
||||
}
|
23
packages/backend/src/remote/activitypub/renderer/question.ts
Normal file
23
packages/backend/src/remote/activitypub/renderer/question.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import config from '@/config/index';
|
||||
import { User } from '@/models/entities/user';
|
||||
import { Note } from '@/models/entities/note';
|
||||
import { Poll } from '@/models/entities/poll';
|
||||
|
||||
export default async function renderQuestion(user: { id: User['id'] }, note: Note, poll: Poll) {
|
||||
const question = {
|
||||
type: 'Question',
|
||||
id: `${config.url}/questions/${note.id}`,
|
||||
actor: `${config.url}/users/${user.id}`,
|
||||
content: note.text || '',
|
||||
[poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({
|
||||
name: text,
|
||||
_misskey_votes: poll.votes[i],
|
||||
replies: {
|
||||
type: 'Collection',
|
||||
totalItems: poll.votes[i]
|
||||
}
|
||||
}))
|
||||
};
|
||||
|
||||
return question;
|
||||
}
|
9
packages/backend/src/remote/activitypub/renderer/read.ts
Normal file
9
packages/backend/src/remote/activitypub/renderer/read.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import config from '@/config/index';
|
||||
import { User } from '@/models/entities/user';
|
||||
import { MessagingMessage } from '@/models/entities/messaging-message';
|
||||
|
||||
export const renderReadActivity = (user: { id: User['id'] }, message: MessagingMessage) => ({
|
||||
type: 'Read',
|
||||
actor: `${config.url}/users/${user.id}`,
|
||||
object: message.uri
|
||||
});
|
@ -0,0 +1,8 @@
|
||||
import config from '@/config/index';
|
||||
import { User } from '@/models/entities/user';
|
||||
|
||||
export default (object: any, user: { id: User['id'] }) => ({
|
||||
type: 'Reject',
|
||||
actor: `${config.url}/users/${user.id}`,
|
||||
object
|
||||
});
|
@ -0,0 +1,9 @@
|
||||
import config from '@/config/index';
|
||||
import { User } from '@/models/entities/user';
|
||||
|
||||
export default (user: { id: User['id'] }, target: any, object: any) => ({
|
||||
type: 'Remove',
|
||||
actor: `${config.url}/users/${user.id}`,
|
||||
target,
|
||||
object
|
||||
});
|
@ -0,0 +1,4 @@
|
||||
export default (id: string) => ({
|
||||
id,
|
||||
type: 'Tombstone'
|
||||
});
|
13
packages/backend/src/remote/activitypub/renderer/undo.ts
Normal file
13
packages/backend/src/remote/activitypub/renderer/undo.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import config from '@/config/index';
|
||||
import { ILocalUser, User } from '@/models/entities/user';
|
||||
|
||||
export default (object: any, user: { id: User['id'] }) => {
|
||||
if (object == null) return null;
|
||||
|
||||
return {
|
||||
type: 'Undo',
|
||||
actor: `${config.url}/users/${user.id}`,
|
||||
object,
|
||||
published: new Date().toISOString(),
|
||||
};
|
||||
};
|
15
packages/backend/src/remote/activitypub/renderer/update.ts
Normal file
15
packages/backend/src/remote/activitypub/renderer/update.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import config from '@/config/index';
|
||||
import { User } from '@/models/entities/user';
|
||||
|
||||
export default (object: any, user: { id: User['id'] }) => {
|
||||
const activity = {
|
||||
id: `${config.url}/users/${user.id}#updates/${new Date().getTime()}`,
|
||||
actor: `${config.url}/users/${user.id}`,
|
||||
type: 'Update',
|
||||
to: [ 'https://www.w3.org/ns/activitystreams#Public' ],
|
||||
object,
|
||||
published: new Date().toISOString(),
|
||||
} as any;
|
||||
|
||||
return activity;
|
||||
};
|
23
packages/backend/src/remote/activitypub/renderer/vote.ts
Normal file
23
packages/backend/src/remote/activitypub/renderer/vote.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import config from '@/config/index';
|
||||
import { Note } from '@/models/entities/note';
|
||||
import { IRemoteUser, User } from '@/models/entities/user';
|
||||
import { PollVote } from '@/models/entities/poll-vote';
|
||||
import { Poll } from '@/models/entities/poll';
|
||||
|
||||
export default async function renderVote(user: { id: User['id'] }, vote: PollVote, note: Note, poll: Poll, pollOwner: IRemoteUser): Promise<any> {
|
||||
return {
|
||||
id: `${config.url}/users/${user.id}#votes/${vote.id}/activity`,
|
||||
actor: `${config.url}/users/${user.id}`,
|
||||
type: 'Create',
|
||||
to: [pollOwner.uri],
|
||||
published: new Date().toISOString(),
|
||||
object: {
|
||||
id: `${config.url}/users/${user.id}#votes/${vote.id}`,
|
||||
type: 'Note',
|
||||
attributedTo: `${config.url}/users/${user.id}`,
|
||||
to: [pollOwner.uri],
|
||||
inReplyTo: note.uri,
|
||||
name: poll.choices[vote.choice]
|
||||
}
|
||||
};
|
||||
}
|
Reference in New Issue
Block a user