refactoring

Resolve #7779
This commit is contained in:
syuilo
2021-11-12 02:02:25 +09:00
parent 037837b551
commit 0e4a111f81
1714 changed files with 20803 additions and 11751 deletions

View File

@ -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
});

View 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
});

View 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
};
};

View File

@ -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
});

View 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;
};

View File

@ -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(),
});

View File

@ -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,
});

View 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
}
});

View File

@ -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;
}

View File

@ -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;
}

View 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;
};

View File

@ -0,0 +1,7 @@
import config from '@/config/index';
export default (tag: string) => ({
type: 'Hashtag',
href: `${config.url}/tags/${encodeURIComponent(tag)}`,
name: `#${tag}`
});

View File

@ -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
});

View 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;
};

View 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'
})
});

View 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;
};

View File

@ -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}`,
});

View 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[];
}

View File

@ -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;
}

View File

@ -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;
}

View 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;
}

View 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;
}

View 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
});

View File

@ -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
});

View File

@ -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
});

View File

@ -0,0 +1,4 @@
export default (id: string) => ({
id,
type: 'Tombstone'
});

View 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(),
};
};

View 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;
};

View 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]
}
};
}