mirror of
https://git.joinsharkey.org/Sharkey/Sharkey.git
synced 2024-11-23 20:13:08 +02:00
Implement announce
And bug fixes
This commit is contained in:
parent
0004944708
commit
6e34e77372
17 changed files with 164 additions and 300 deletions
|
@ -112,7 +112,7 @@ export const pack = async (
|
||||||
_note = deepcopy(note);
|
_note = deepcopy(note);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_note) throw 'invalid note arg.';
|
if (!_note) throw `invalid note arg ${note}`;
|
||||||
|
|
||||||
const id = _note._id;
|
const id = _note._id;
|
||||||
|
|
||||||
|
|
|
@ -51,9 +51,6 @@ export interface INotification {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pack a notification for API response
|
* Pack a notification for API response
|
||||||
*
|
|
||||||
* @param {any} notification
|
|
||||||
* @return {Promise<any>}
|
|
||||||
*/
|
*/
|
||||||
export const pack = (notification: any) => new Promise<any>(async (resolve, reject) => {
|
export const pack = (notification: any) => new Promise<any>(async (resolve, reject) => {
|
||||||
let _notification: any;
|
let _notification: any;
|
||||||
|
|
39
src/remote/activitypub/act/announce/index.ts
Normal file
39
src/remote/activitypub/act/announce/index.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import * as debug from 'debug';
|
||||||
|
|
||||||
|
import Resolver from '../../resolver';
|
||||||
|
import { IRemoteUser } from '../../../../models/user';
|
||||||
|
import announceNote from './note';
|
||||||
|
import { IAnnounce } from '../../type';
|
||||||
|
|
||||||
|
const log = debug('misskey:activitypub');
|
||||||
|
|
||||||
|
export default async (actor: IRemoteUser, activity: IAnnounce): Promise<void> => {
|
||||||
|
if ('actor' in activity && actor.uri !== activity.actor) {
|
||||||
|
throw new Error('invalid actor');
|
||||||
|
}
|
||||||
|
|
||||||
|
const uri = activity.id || activity;
|
||||||
|
|
||||||
|
log(`Announce: ${uri}`);
|
||||||
|
|
||||||
|
const resolver = new Resolver();
|
||||||
|
|
||||||
|
let object;
|
||||||
|
|
||||||
|
try {
|
||||||
|
object = await resolver.resolve(activity.object);
|
||||||
|
} catch (e) {
|
||||||
|
log(`Resolution failed: ${e}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (object.type) {
|
||||||
|
case 'Note':
|
||||||
|
announceNote(resolver, actor, activity, object);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.warn(`Unknown announce type: ${object.type}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
52
src/remote/activitypub/act/announce/note.ts
Normal file
52
src/remote/activitypub/act/announce/note.ts
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import * as debug from 'debug';
|
||||||
|
|
||||||
|
import Resolver from '../../resolver';
|
||||||
|
import Note from '../../../../models/note';
|
||||||
|
import post from '../../../../services/note/create';
|
||||||
|
import { IRemoteUser, isRemoteUser } from '../../../../models/user';
|
||||||
|
import { IAnnounce, INote } from '../../type';
|
||||||
|
import createNote from '../create/note';
|
||||||
|
import resolvePerson from '../../resolve-person';
|
||||||
|
|
||||||
|
const log = debug('misskey:activitypub');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* アナウンスアクティビティを捌きます
|
||||||
|
*/
|
||||||
|
export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, note: INote): Promise<void> {
|
||||||
|
const uri = activity.id || activity;
|
||||||
|
|
||||||
|
if (typeof uri !== 'string') {
|
||||||
|
throw new Error('invalid announce');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 既に同じURIを持つものが登録されていないかチェック
|
||||||
|
const exist = await Note.findOne({ uri });
|
||||||
|
if (exist) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// アナウンス元の投稿の投稿者をフェッチ
|
||||||
|
const announcee = await resolvePerson(note.attributedTo);
|
||||||
|
|
||||||
|
const renote = isRemoteUser(announcee)
|
||||||
|
? await createNote(resolver, announcee, note, true)
|
||||||
|
: await Note.findOne({ _id: note.id.split('/').pop() });
|
||||||
|
|
||||||
|
log(`Creating the (Re)Note: ${uri}`);
|
||||||
|
|
||||||
|
//#region Visibility
|
||||||
|
let visibility = 'public';
|
||||||
|
if (!activity.to.includes('https://www.w3.org/ns/activitystreams#Public')) visibility = 'unlisted';
|
||||||
|
if (activity.cc.length == 0) visibility = 'private';
|
||||||
|
// TODO
|
||||||
|
if (visibility != 'public') throw new Error('unspported visibility');
|
||||||
|
//#endergion
|
||||||
|
|
||||||
|
await post(actor, {
|
||||||
|
createdAt: new Date(activity.published),
|
||||||
|
renote,
|
||||||
|
visibility,
|
||||||
|
uri
|
||||||
|
});
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import performDeleteActivity from './delete';
|
||||||
import follow from './follow';
|
import follow from './follow';
|
||||||
import undo from './undo';
|
import undo from './undo';
|
||||||
import like from './like';
|
import like from './like';
|
||||||
|
import announce from './announce';
|
||||||
|
|
||||||
const self = async (actor: IRemoteUser, activity: Object): Promise<void> => {
|
const self = async (actor: IRemoteUser, activity: Object): Promise<void> => {
|
||||||
switch (activity.type) {
|
switch (activity.type) {
|
||||||
|
@ -24,6 +25,10 @@ const self = async (actor: IRemoteUser, activity: Object): Promise<void> => {
|
||||||
// noop
|
// noop
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'Announce':
|
||||||
|
await announce(actor, activity);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'Like':
|
case 'Like':
|
||||||
await like(actor, activity);
|
await like(actor, activity);
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -7,7 +7,7 @@ export default async (actor: IRemoteUser, activity: ILike) => {
|
||||||
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
|
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
|
||||||
|
|
||||||
// Transform:
|
// Transform:
|
||||||
// https://misskey.ex/@syuilo/xxxx to
|
// https://misskey.ex/notes/xxxx to
|
||||||
// xxxx
|
// xxxx
|
||||||
const noteId = id.split('/').pop();
|
const noteId = id.split('/').pop();
|
||||||
|
|
||||||
|
|
4
src/remote/activitypub/renderer/announce.ts
Normal file
4
src/remote/activitypub/renderer/announce.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export default object => ({
|
||||||
|
type: 'Announce',
|
||||||
|
object
|
||||||
|
});
|
|
@ -1,6 +1,7 @@
|
||||||
import config from '../../../config';
|
import config from '../../../config';
|
||||||
|
import { ILocalUser } from '../../../models/user';
|
||||||
|
|
||||||
export default (user, note) => {
|
export default (user: ILocalUser, note) => {
|
||||||
return {
|
return {
|
||||||
type: 'Like',
|
type: 'Like',
|
||||||
actor: `${config.url}/@${user.username}`,
|
actor: `${config.url}/@${user.username}`,
|
||||||
|
|
|
@ -3,9 +3,9 @@ import renderHashtag from './hashtag';
|
||||||
import config from '../../../config';
|
import config from '../../../config';
|
||||||
import DriveFile from '../../../models/drive-file';
|
import DriveFile from '../../../models/drive-file';
|
||||||
import Note, { INote } from '../../../models/note';
|
import Note, { INote } from '../../../models/note';
|
||||||
import User, { IUser } from '../../../models/user';
|
import User from '../../../models/user';
|
||||||
|
|
||||||
export default async (user: IUser, note: INote) => {
|
export default async (note: INote) => {
|
||||||
const promisedFiles = note.mediaIds
|
const promisedFiles = note.mediaIds
|
||||||
? DriveFile.find({ _id: { $in: note.mediaIds } })
|
? DriveFile.find({ _id: { $in: note.mediaIds } })
|
||||||
: Promise.resolve([]);
|
: Promise.resolve([]);
|
||||||
|
@ -30,6 +30,10 @@ export default async (user: IUser, note: INote) => {
|
||||||
inReplyTo = null;
|
inReplyTo = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const user = await User.findOne({
|
||||||
|
_id: note.userId
|
||||||
|
});
|
||||||
|
|
||||||
const attributedTo = `${config.url}/@${user.username}`;
|
const attributedTo = `${config.url}/@${user.username}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -2,18 +2,18 @@ import { JSDOM } from 'jsdom';
|
||||||
import { toUnicode } from 'punycode';
|
import { toUnicode } from 'punycode';
|
||||||
import parseAcct from '../../acct/parse';
|
import parseAcct from '../../acct/parse';
|
||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import User, { validateUsername, isValidName, isValidDescription } from '../../models/user';
|
import User, { validateUsername, isValidName, isValidDescription, IUser } from '../../models/user';
|
||||||
import webFinger from '../webfinger';
|
import webFinger from '../webfinger';
|
||||||
import Resolver from './resolver';
|
import Resolver from './resolver';
|
||||||
import uploadFromUrl from '../../services/drive/upload-from-url';
|
import uploadFromUrl from '../../services/drive/upload-from-url';
|
||||||
import { isCollectionOrOrderedCollection } from './type';
|
import { isCollectionOrOrderedCollection, IObject } from './type';
|
||||||
|
|
||||||
export default async (value, verifier?: string) => {
|
export default async (value: string | IObject, verifier?: string): Promise<IUser> => {
|
||||||
const id = value.id || value;
|
const id = typeof value == 'string' ? value : value.id;
|
||||||
const localPrefix = config.url + '/@';
|
const localPrefix = config.url + '/@';
|
||||||
|
|
||||||
if (id.startsWith(localPrefix)) {
|
if (id.startsWith(localPrefix)) {
|
||||||
return User.findOne(parseAcct(id.slice(localPrefix)));
|
return await User.findOne(parseAcct(id.substr(localPrefix.length)));
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolver = new Resolver();
|
const resolver = new Resolver();
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import * as request from 'request-promise-native';
|
import * as request from 'request-promise-native';
|
||||||
import * as debug from 'debug';
|
import * as debug from 'debug';
|
||||||
import { IObject } from './type';
|
import { IObject } from './type';
|
||||||
|
//import config from '../../config';
|
||||||
|
|
||||||
const log = debug('misskey:activitypub:resolver');
|
const log = debug('misskey:activitypub:resolver');
|
||||||
|
|
||||||
|
@ -47,6 +48,11 @@ export default class Resolver {
|
||||||
|
|
||||||
this.history.add(value);
|
this.history.add(value);
|
||||||
|
|
||||||
|
//#region resolve local objects
|
||||||
|
// TODO
|
||||||
|
//if (value.startsWith(`${config.url}/@`)) {
|
||||||
|
//#endregion
|
||||||
|
|
||||||
const object = await request({
|
const object = await request({
|
||||||
url: value,
|
url: value,
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -60,6 +66,7 @@ export default class Resolver {
|
||||||
!object['@context'].includes('https://www.w3.org/ns/activitystreams') :
|
!object['@context'].includes('https://www.w3.org/ns/activitystreams') :
|
||||||
object['@context'] !== 'https://www.w3.org/ns/activitystreams'
|
object['@context'] !== 'https://www.w3.org/ns/activitystreams'
|
||||||
)) {
|
)) {
|
||||||
|
log(`invalid response: ${JSON.stringify(object, null, 2)}`);
|
||||||
throw new Error('invalid response');
|
throw new Error('invalid response');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,10 @@ export interface IObject {
|
||||||
type: string;
|
type: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
summary?: string;
|
summary?: string;
|
||||||
|
published?: string;
|
||||||
|
cc?: string[];
|
||||||
|
to?: string[];
|
||||||
|
attributedTo: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IActivity extends IObject {
|
export interface IActivity extends IObject {
|
||||||
|
@ -26,6 +30,10 @@ export interface IOrderedCollection extends IObject {
|
||||||
orderedItems: IObject | string | IObject[] | string[];
|
orderedItems: IObject | string | IObject[] | string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface INote extends IObject {
|
||||||
|
type: 'Note';
|
||||||
|
}
|
||||||
|
|
||||||
export const isCollection = (object: IObject): object is ICollection =>
|
export const isCollection = (object: IObject): object is ICollection =>
|
||||||
object.type === 'Collection';
|
object.type === 'Collection';
|
||||||
|
|
||||||
|
@ -59,6 +67,10 @@ export interface ILike extends IActivity {
|
||||||
type: 'Like';
|
type: 'Like';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IAnnounce extends IActivity {
|
||||||
|
type: 'Announce';
|
||||||
|
}
|
||||||
|
|
||||||
export type Object =
|
export type Object =
|
||||||
ICollection |
|
ICollection |
|
||||||
IOrderedCollection |
|
IOrderedCollection |
|
||||||
|
@ -67,4 +79,5 @@ export type Object =
|
||||||
IUndo |
|
IUndo |
|
||||||
IFollow |
|
IFollow |
|
||||||
IAccept |
|
IAccept |
|
||||||
ILike;
|
ILike |
|
||||||
|
IAnnounce;
|
||||||
|
|
|
@ -1,40 +1,25 @@
|
||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
import context from '../../remote/activitypub/renderer/context';
|
import context from '../../remote/activitypub/renderer/context';
|
||||||
import render from '../../remote/activitypub/renderer/note';
|
import render from '../../remote/activitypub/renderer/note';
|
||||||
import parseAcct from '../../acct/parse';
|
|
||||||
import Note from '../../models/note';
|
import Note from '../../models/note';
|
||||||
import User from '../../models/user';
|
|
||||||
|
|
||||||
const app = express.Router();
|
const app = express.Router();
|
||||||
|
|
||||||
app.get('/@:user/:note', async (req, res, next) => {
|
app.get('/notes/:note', async (req, res, next) => {
|
||||||
const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']);
|
const accepted = req.accepts(['html', 'application/activity+json', 'application/ld+json']);
|
||||||
if (!(['application/activity+json', 'application/ld+json'] as any[]).includes(accepted)) {
|
if (!(['application/activity+json', 'application/ld+json'] as any[]).includes(accepted)) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { username, host } = parseAcct(req.params.user);
|
|
||||||
if (host !== null) {
|
|
||||||
return res.sendStatus(422);
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await User.findOne({
|
|
||||||
usernameLower: username.toLowerCase(),
|
|
||||||
host: null
|
|
||||||
});
|
|
||||||
if (user === null) {
|
|
||||||
return res.sendStatus(404);
|
|
||||||
}
|
|
||||||
|
|
||||||
const note = await Note.findOne({
|
const note = await Note.findOne({
|
||||||
_id: req.params.note,
|
_id: req.params.note
|
||||||
userId: user._id
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (note === null) {
|
if (note === null) {
|
||||||
return res.sendStatus(404);
|
return res.sendStatus(404);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rendered = await render(user, note);
|
const rendered = await render(note);
|
||||||
rendered['@context'] = context;
|
rendered['@context'] = context;
|
||||||
|
|
||||||
res.json(rendered);
|
res.json(rendered);
|
||||||
|
|
|
@ -16,7 +16,7 @@ app.get('/@:user/outbox', withUser(username => {
|
||||||
sort: { _id: -1 }
|
sort: { _id: -1 }
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderedNotes = await Promise.all(notes.map(note => renderNote(user, note)));
|
const renderedNotes = await Promise.all(notes.map(note => renderNote(note)));
|
||||||
const rendered = renderOrderedCollection(`${config.url}/@${user.username}/inbox`, user.notesCount, renderedNotes);
|
const rendered = renderOrderedCollection(`${config.url}/@${user.username}/inbox`, user.notesCount, renderedNotes);
|
||||||
rendered['@context'] = context;
|
rendered['@context'] = context;
|
||||||
|
|
||||||
|
|
|
@ -1,251 +0,0 @@
|
||||||
/**
|
|
||||||
* Module dependencies
|
|
||||||
*/
|
|
||||||
import $ from 'cafy';
|
|
||||||
import deepEqual = require('deep-equal');
|
|
||||||
import Note, { INote, isValidText, isValidCw, pack } from '../../../../models/note';
|
|
||||||
import { ILocalUser } from '../../../../models/user';
|
|
||||||
import Channel, { IChannel } from '../../../../models/channel';
|
|
||||||
import DriveFile from '../../../../models/drive-file';
|
|
||||||
import create from '../../../../services/note/create';
|
|
||||||
import { IApp } from '../../../../models/app';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a note
|
|
||||||
*
|
|
||||||
* @param {any} params
|
|
||||||
* @param {any} user
|
|
||||||
* @param {any} app
|
|
||||||
* @return {Promise<any>}
|
|
||||||
*/
|
|
||||||
module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res, rej) => {
|
|
||||||
// Get 'visibility' parameter
|
|
||||||
const [visibility = 'public', visibilityErr] = $(params.visibility).optional.string().or(['public', 'unlisted', 'private', 'direct']).$;
|
|
||||||
if (visibilityErr) return rej('invalid visibility');
|
|
||||||
|
|
||||||
// Get 'text' parameter
|
|
||||||
const [text, textErr] = $(params.text).optional.string().pipe(isValidText).$;
|
|
||||||
if (textErr) return rej('invalid text');
|
|
||||||
|
|
||||||
// Get 'cw' parameter
|
|
||||||
const [cw, cwErr] = $(params.cw).optional.string().pipe(isValidCw).$;
|
|
||||||
if (cwErr) return rej('invalid cw');
|
|
||||||
|
|
||||||
// Get 'viaMobile' parameter
|
|
||||||
const [viaMobile = false, viaMobileErr] = $(params.viaMobile).optional.boolean().$;
|
|
||||||
if (viaMobileErr) return rej('invalid viaMobile');
|
|
||||||
|
|
||||||
// Get 'tags' parameter
|
|
||||||
const [tags = [], tagsErr] = $(params.tags).optional.array('string').unique().eachQ(t => t.range(1, 32)).$;
|
|
||||||
if (tagsErr) return rej('invalid tags');
|
|
||||||
|
|
||||||
// Get 'geo' parameter
|
|
||||||
const [geo, geoErr] = $(params.geo).optional.nullable.strict.object()
|
|
||||||
.have('coordinates', $().array().length(2)
|
|
||||||
.item(0, $().number().range(-180, 180))
|
|
||||||
.item(1, $().number().range(-90, 90)))
|
|
||||||
.have('altitude', $().nullable.number())
|
|
||||||
.have('accuracy', $().nullable.number())
|
|
||||||
.have('altitudeAccuracy', $().nullable.number())
|
|
||||||
.have('heading', $().nullable.number().range(0, 360))
|
|
||||||
.have('speed', $().nullable.number())
|
|
||||||
.$;
|
|
||||||
if (geoErr) return rej('invalid geo');
|
|
||||||
|
|
||||||
// Get 'mediaIds' parameter
|
|
||||||
const [mediaIds, mediaIdsErr] = $(params.mediaIds).optional.array('id').unique().range(1, 4).$;
|
|
||||||
if (mediaIdsErr) return rej('invalid mediaIds');
|
|
||||||
|
|
||||||
let files = [];
|
|
||||||
if (mediaIds !== undefined) {
|
|
||||||
// Fetch files
|
|
||||||
// forEach だと途中でエラーなどがあっても return できないので
|
|
||||||
// 敢えて for を使っています。
|
|
||||||
for (const mediaId of mediaIds) {
|
|
||||||
// Fetch file
|
|
||||||
// SELECT _id
|
|
||||||
const entity = await DriveFile.findOne({
|
|
||||||
_id: mediaId,
|
|
||||||
'metadata.userId': user._id
|
|
||||||
});
|
|
||||||
|
|
||||||
if (entity === null) {
|
|
||||||
return rej('file not found');
|
|
||||||
} else {
|
|
||||||
files.push(entity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
files = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get 'renoteId' parameter
|
|
||||||
const [renoteId, renoteIdErr] = $(params.renoteId).optional.id().$;
|
|
||||||
if (renoteIdErr) return rej('invalid renoteId');
|
|
||||||
|
|
||||||
let renote: INote = null;
|
|
||||||
let isQuote = false;
|
|
||||||
if (renoteId !== undefined) {
|
|
||||||
// Fetch renote to note
|
|
||||||
renote = await Note.findOne({
|
|
||||||
_id: renoteId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (renote == null) {
|
|
||||||
return rej('renoteee is not found');
|
|
||||||
} else if (renote.renoteId && !renote.text && !renote.mediaIds) {
|
|
||||||
return rej('cannot renote to renote');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch recently note
|
|
||||||
const latestNote = await Note.findOne({
|
|
||||||
userId: user._id
|
|
||||||
}, {
|
|
||||||
sort: {
|
|
||||||
_id: -1
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
isQuote = text != null || files != null;
|
|
||||||
|
|
||||||
// 直近と同じRenote対象かつ引用じゃなかったらエラー
|
|
||||||
if (latestNote &&
|
|
||||||
latestNote.renoteId &&
|
|
||||||
latestNote.renoteId.equals(renote._id) &&
|
|
||||||
!isQuote) {
|
|
||||||
return rej('cannot renote same note that already reposted in your latest note');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 直近がRenote対象かつ引用じゃなかったらエラー
|
|
||||||
if (latestNote &&
|
|
||||||
latestNote._id.equals(renote._id) &&
|
|
||||||
!isQuote) {
|
|
||||||
return rej('cannot renote your latest note');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get 'replyId' parameter
|
|
||||||
const [replyId, replyIdErr] = $(params.replyId).optional.id().$;
|
|
||||||
if (replyIdErr) return rej('invalid replyId');
|
|
||||||
|
|
||||||
let reply: INote = null;
|
|
||||||
if (replyId !== undefined) {
|
|
||||||
// Fetch reply
|
|
||||||
reply = await Note.findOne({
|
|
||||||
_id: replyId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (reply === null) {
|
|
||||||
return rej('in reply to note is not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返信対象が引用でないRenoteだったらエラー
|
|
||||||
if (reply.renoteId && !reply.text && !reply.mediaIds) {
|
|
||||||
return rej('cannot reply to renote');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get 'channelId' parameter
|
|
||||||
const [channelId, channelIdErr] = $(params.channelId).optional.id().$;
|
|
||||||
if (channelIdErr) return rej('invalid channelId');
|
|
||||||
|
|
||||||
let channel: IChannel = null;
|
|
||||||
if (channelId !== undefined) {
|
|
||||||
// Fetch channel
|
|
||||||
channel = await Channel.findOne({
|
|
||||||
_id: channelId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (channel === null) {
|
|
||||||
return rej('channel not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返信対象の投稿がこのチャンネルじゃなかったらダメ
|
|
||||||
if (reply && !channelId.equals(reply.channelId)) {
|
|
||||||
return rej('チャンネル内部からチャンネル外部の投稿に返信することはできません');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Renote対象の投稿がこのチャンネルじゃなかったらダメ
|
|
||||||
if (renote && !channelId.equals(renote.channelId)) {
|
|
||||||
return rej('チャンネル内部からチャンネル外部の投稿をRenoteすることはできません');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 引用ではないRenoteはダメ
|
|
||||||
if (renote && !isQuote) {
|
|
||||||
return rej('チャンネル内部では引用ではないRenoteをすることはできません');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 返信対象の投稿がチャンネルへの投稿だったらダメ
|
|
||||||
if (reply && reply.channelId != null) {
|
|
||||||
return rej('チャンネル外部からチャンネル内部の投稿に返信することはできません');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Renote対象の投稿がチャンネルへの投稿だったらダメ
|
|
||||||
if (renote && renote.channelId != null) {
|
|
||||||
return rej('チャンネル外部からチャンネル内部の投稿をRenoteすることはできません');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get 'poll' parameter
|
|
||||||
const [poll, pollErr] = $(params.poll).optional.strict.object()
|
|
||||||
.have('choices', $().array('string')
|
|
||||||
.unique()
|
|
||||||
.range(2, 10)
|
|
||||||
.each(c => c.length > 0 && c.length < 50))
|
|
||||||
.$;
|
|
||||||
if (pollErr) return rej('invalid poll');
|
|
||||||
|
|
||||||
if (poll) {
|
|
||||||
(poll as any).choices = (poll as any).choices.map((choice, i) => ({
|
|
||||||
id: i, // IDを付与
|
|
||||||
text: choice.trim(),
|
|
||||||
votes: 0
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー
|
|
||||||
if (text === undefined && files === null && renote === null && poll === undefined) {
|
|
||||||
return rej('text, mediaIds, renoteId or poll is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 直近の投稿と重複してたらエラー
|
|
||||||
// TODO: 直近の投稿が一日前くらいなら重複とは見なさない
|
|
||||||
if (user.latestNote) {
|
|
||||||
if (deepEqual({
|
|
||||||
text: user.latestNote.text,
|
|
||||||
reply: user.latestNote.replyId ? user.latestNote.replyId.toString() : null,
|
|
||||||
renote: user.latestNote.renoteId ? user.latestNote.renoteId.toString() : null,
|
|
||||||
mediaIds: (user.latestNote.mediaIds || []).map(id => id.toString())
|
|
||||||
}, {
|
|
||||||
text: text,
|
|
||||||
reply: reply ? reply._id.toString() : null,
|
|
||||||
renote: renote ? renote._id.toString() : null,
|
|
||||||
mediaIds: (files || []).map(file => file._id.toString())
|
|
||||||
})) {
|
|
||||||
return rej('duplicate');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 投稿を作成
|
|
||||||
const note = await create(user, {
|
|
||||||
createdAt: new Date(),
|
|
||||||
media: files,
|
|
||||||
poll: poll,
|
|
||||||
text: text,
|
|
||||||
reply,
|
|
||||||
renote,
|
|
||||||
cw: cw,
|
|
||||||
tags: tags,
|
|
||||||
app: app,
|
|
||||||
viaMobile: viaMobile,
|
|
||||||
visibility,
|
|
||||||
geo
|
|
||||||
});
|
|
||||||
|
|
||||||
const noteObj = await pack(note, user);
|
|
||||||
|
|
||||||
// Reponse
|
|
||||||
res({
|
|
||||||
createdNote: noteObj
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -5,6 +5,7 @@ import Following from '../../models/following';
|
||||||
import { deliver } from '../../queue';
|
import { deliver } from '../../queue';
|
||||||
import renderNote from '../../remote/activitypub/renderer/note';
|
import renderNote from '../../remote/activitypub/renderer/note';
|
||||||
import renderCreate from '../../remote/activitypub/renderer/create';
|
import renderCreate from '../../remote/activitypub/renderer/create';
|
||||||
|
import renderAnnounce from '../../remote/activitypub/renderer/announce';
|
||||||
import context from '../../remote/activitypub/renderer/context';
|
import context from '../../remote/activitypub/renderer/context';
|
||||||
import { IDriveFile } from '../../models/drive-file';
|
import { IDriveFile } from '../../models/drive-file';
|
||||||
import notify from '../../publishers/notify';
|
import notify from '../../publishers/notify';
|
||||||
|
@ -34,6 +35,7 @@ export default async (user: IUser, data: {
|
||||||
}, silent = false) => new Promise<INote>(async (res, rej) => {
|
}, silent = false) => new Promise<INote>(async (res, rej) => {
|
||||||
if (data.createdAt == null) data.createdAt = new Date();
|
if (data.createdAt == null) data.createdAt = new Date();
|
||||||
if (data.visibility == null) data.visibility = 'public';
|
if (data.visibility == null) data.visibility = 'public';
|
||||||
|
if (data.viaMobile == null) data.viaMobile = false;
|
||||||
|
|
||||||
const tags = data.tags || [];
|
const tags = data.tags || [];
|
||||||
|
|
||||||
|
@ -77,9 +79,7 @@ export default async (user: IUser, data: {
|
||||||
_user: {
|
_user: {
|
||||||
host: user.host,
|
host: user.host,
|
||||||
hostLower: user.hostLower,
|
hostLower: user.hostLower,
|
||||||
account: isLocalUser(user) ? {} : {
|
inbox: isRemoteUser(user) ? user.inbox : undefined
|
||||||
inbox: user.inbox
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -128,15 +128,25 @@ export default async (user: IUser, data: {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
const content = renderCreate(await renderNote(user, note));
|
const render = async () => {
|
||||||
content['@context'] = context;
|
const content = data.renote && data.text == null
|
||||||
|
? renderAnnounce(data.renote.uri ? data.renote.uri : await renderNote(data.renote))
|
||||||
|
: renderCreate(await renderNote(note));
|
||||||
|
content['@context'] = context;
|
||||||
|
return content;
|
||||||
|
};
|
||||||
|
|
||||||
// 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送
|
// 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送
|
||||||
if (data.reply && isLocalUser(user) && isRemoteUser(data.reply._user)) {
|
if (data.reply && isLocalUser(user) && isRemoteUser(data.reply._user)) {
|
||||||
deliver(user, content, data.reply._user.inbox).save();
|
deliver(user, await render(), data.reply._user.inbox).save();
|
||||||
}
|
}
|
||||||
|
|
||||||
Promise.all(followers.map(follower => {
|
// 投稿がRenoteかつ投稿者がローカルユーザーかつRenote元の投稿の投稿者がリモートユーザーなら配送
|
||||||
|
if (data.renote && isLocalUser(user) && isRemoteUser(data.renote._user)) {
|
||||||
|
deliver(user, await render(), data.renote._user.inbox).save();
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.all(followers.map(async follower => {
|
||||||
follower = follower.user[0];
|
follower = follower.user[0];
|
||||||
|
|
||||||
if (isLocalUser(follower)) {
|
if (isLocalUser(follower)) {
|
||||||
|
@ -145,7 +155,7 @@ export default async (user: IUser, data: {
|
||||||
} else {
|
} else {
|
||||||
// フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信
|
// フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信
|
||||||
if (isLocalUser(user)) {
|
if (isLocalUser(user)) {
|
||||||
deliver(user, content, follower.inbox).save();
|
deliver(user, await render(), follower.inbox).save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
@ -255,15 +265,13 @@ export default async (user: IUser, data: {
|
||||||
// Notify
|
// Notify
|
||||||
const type = data.text ? 'quote' : 'renote';
|
const type = data.text ? 'quote' : 'renote';
|
||||||
notify(data.renote.userId, user._id, type, {
|
notify(data.renote.userId, user._id, type, {
|
||||||
note_id: note._id
|
noteId: note._id
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch watchers
|
// Fetch watchers
|
||||||
NoteWatching.find({
|
NoteWatching.find({
|
||||||
noteId: data.renote._id,
|
noteId: data.renote._id,
|
||||||
userId: { $ne: user._id },
|
userId: { $ne: user._id }
|
||||||
// 削除されたドキュメントは除く
|
|
||||||
deletedAt: { $exists: false }
|
|
||||||
}, {
|
}, {
|
||||||
fields: {
|
fields: {
|
||||||
userId: true
|
userId: true
|
||||||
|
|
|
@ -83,11 +83,11 @@ export default async (user: IUser, note: INote, reaction: string) => new Promise
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region 配信
|
//#region 配信
|
||||||
const content = renderLike(user, note);
|
|
||||||
content['@context'] = context;
|
|
||||||
|
|
||||||
// リアクターがローカルユーザーかつリアクション対象がリモートユーザーの投稿なら配送
|
// リアクターがローカルユーザーかつリアクション対象がリモートユーザーの投稿なら配送
|
||||||
if (isLocalUser(user) && isRemoteUser(note._user)) {
|
if (isLocalUser(user) && isRemoteUser(note._user)) {
|
||||||
|
const content = renderLike(user, note);
|
||||||
|
content['@context'] = context;
|
||||||
|
|
||||||
deliver(user, content, note._user.inbox).save();
|
deliver(user, content, note._user.inbox).save();
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
Loading…
Reference in a new issue