Use PostgreSQL instead of MongoDB (#4572)

* wip

* Update note.ts

* Update timeline.ts

* Update core.ts

* wip

* Update generate-visibility-query.ts

* wip

* wip

* wip

* wip

* wip

* Update global-timeline.ts

* wip

* wip

* wip

* Update vote.ts

* wip

* wip

* Update create.ts

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update files.ts

* wip

* wip

* Update CONTRIBUTING.md

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update read-notification.ts

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update cancel.ts

* wip

* wip

* wip

* Update show.ts

* wip

* wip

* Update gen-id.ts

* Update create.ts

* Update id.ts

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Docker: Update files about Docker (#4599)

* Docker: Use cache if files used by `yarn install` was not updated

This patch reduces the number of times to installing node_modules.
For example, `yarn install` step will be skipped when only ".config/default.yml" is updated.

* Docker: Migrate MongoDB to Postgresql

Misskey uses Postgresql as a database instead of Mongodb since version 11.

* Docker: Uncomment about data persistence

This patch will save a lot of databases.

* wip

* wip

* wip

* Update activitypub.ts

* wip

* wip

* wip

* Update logs.ts

* wip

* Update drive-file.ts

* Update register.ts

* wip

* wip

* Update mentions.ts

* wip

* wip

* wip

* Update recommendation.ts

* wip

* Update index.ts

* wip

* Update recommendation.ts

* Doc: Update docker.ja.md and docker.en.md (#1) (#4608)

Update how to set up misskey.

* wip

* ✌️

* wip

* Update note.ts

* Update postgre.ts

* wip

* wip

* wip

* wip

* Update add-file.ts

* wip

* wip

* wip

* Clean up

* Update logs.ts

* wip

* 🍕

* wip

* Ad notes

* wip

* Update api-visibility.ts

* Update note.ts

* Update add-file.ts

* tests

* tests

* Update postgre.ts

* Update utils.ts

* wip

* wip

* Refactor

* wip

* Refactor

* wip

* wip

* Update show-users.ts

* Update update-instance.ts

* wip

* Update feed.ts

* Update outbox.ts

* Update outbox.ts

* Update user.ts

* wip

* Update list.ts

* Update update-hashtag.ts

* wip

* Update update-hashtag.ts

* Refactor

* Update update.ts

* wip

* wip

* ✌️

* clean up

* docs

* Update push.ts

* wip

* Update api.ts

* wip

* ✌️

* Update make-pagination-query.ts

* ✌️

* Delete hashtags.ts

* Update instances.ts

* Update instances.ts

* Update create.ts

* Update search.ts

* Update reversi-game.ts

* Update signup.ts

* Update user.ts

* id

* Update example.yml

* 🎨

* objectid

* fix

* reversi

* reversi

* Fix bug of chart engine

* Add test of chart engine

* Improve test

* Better testing

* Improve chart engine

* Refactor

* Add test of chart engine

* Refactor

* Add chart test

* Fix bug

* コミットし忘れ

* Refactoring

* ✌️

* Add tests

* Add test

* Extarct note tests

* Refactor

* 存在しないユーザーにメンションできなくなっていた問題を修正

* Fix bug

* Update update-meta.ts

* Fix bug

* Update mention.vue

* Fix bug

* Update meta.ts

* Update CONTRIBUTING.md

* Fix bug

* Fix bug

* Fix bug

* Clean up

* Clean up

* Update notification.ts

* Clean up

* Add mute tests

* Add test

* Refactor

* Add test

* Fix test

* Refactor

* Refactor

* Add tests

* Update utils.ts

* Update utils.ts

* Fix test

* Update package.json

* Update update.ts

* Update manifest.ts

* Fix bug

* Fix bug

* Add test

* 🎨

* Update endpoint permissions

* Updaye permisison

* Update person.ts

#4299

* データベースと同期しないように

* Fix bug

* Fix bug

* Update reversi-game.ts

* Use a feature of Node v11.7.0 to extract a public key (#4644)

* wip

* wip

* ✌️

* Refactoring

#1540

* test

* test

* test

* test

* test

* test

* test

* Fix bug

* Fix test

* 🍣

* wip

* #4471

* Add test for #4335

* Refactor

* Fix test

* Add tests

* 🕓

* Fix bug

* Add test

* Add test

* rename

* Fix bug
This commit is contained in:
syuilo 2019-04-07 21:50:36 +09:00 committed by GitHub
parent 13caf37991
commit f0a29721c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
592 changed files with 13463 additions and 14147 deletions

View file

@ -1,8 +1,8 @@
import * as mongo from 'mongodb';
import User, { IRemoteUser } from '../../../../models/user';
import { IRemoteUser } from '../../../../models/entities/user';
import config from '../../../../config';
import accept from '../../../../services/following/requests/accept';
import { IFollow } from '../../type';
import { Users } from '../../../../models';
export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
const id = typeof activity.actor == 'string' ? activity.actor : activity.actor.id;
@ -11,11 +11,11 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
return null;
}
const follower = await User.findOne({
_id: new mongo.ObjectID(id.split('/').pop())
const follower = await Users.findOne({
id: id.split('/').pop()
});
if (follower === null) {
if (follower == null) {
throw new Error('follower not found');
}

View file

@ -1,5 +1,5 @@
import Resolver from '../../resolver';
import { IRemoteUser } from '../../../../models/user';
import { IRemoteUser } from '../../../../models/entities/user';
import acceptFollow from './follow';
import { IAccept, IFollow } from '../../type';
import { apLogger } from '../../logger';

View file

@ -1,4 +1,4 @@
import { IRemoteUser } from '../../../../models/user';
import { IRemoteUser } from '../../../../models/entities/user';
import { IAdd } from '../../type';
import { resolveNote } from '../../models/note';
import { addPinned } from '../../../../services/i/pin';
@ -14,7 +14,7 @@ export default async (actor: IRemoteUser, activity: IAdd): Promise<void> => {
if (activity.target === actor.featured) {
const note = await resolveNote(activity.object);
await addPinned(actor, note._id);
await addPinned(actor, note.id);
return;
}

View file

@ -1,5 +1,5 @@
import Resolver from '../../resolver';
import { IRemoteUser } from '../../../../models/user';
import { IRemoteUser } from '../../../../models/entities/user';
import announceNote from './note';
import { IAnnounce, INote } from '../../type';
import { apLogger } from '../../logger';

View file

@ -1,12 +1,12 @@
import Resolver from '../../resolver';
import post from '../../../../services/note/create';
import { IRemoteUser, IUser } from '../../../../models/user';
import { IRemoteUser, User } from '../../../../models/entities/user';
import { IAnnounce, INote } from '../../type';
import { fetchNote, resolveNote } from '../../models/note';
import { resolvePerson } from '../../models/person';
import { apLogger } from '../../logger';
import { extractDbHost } from '../../../../misc/convert-host';
import Instance from '../../../../models/instance';
import fetchMeta from '../../../../misc/fetch-meta';
const logger = apLogger;
@ -27,8 +27,8 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
// アナウンス先をブロックしてたら中断
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
const instance = await Instance.findOne({ host: extractDbHost(uri) });
if (instance && instance.isBlocked) return;
const meta = await fetchMeta();
if (meta.blockedHosts.includes(extractDbHost(uri))) return;
// 既に同じURIを持つものが登録されていないかチェック
const exist = await fetchNote(uri);
@ -55,7 +55,7 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
//#region Visibility
const visibility = getVisibility(activity.to, activity.cc, actor);
let visibleUsers: IUser[] = [];
let visibleUsers: User[] = [];
if (visibility == 'specified') {
visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri)));
}

View file

@ -1,9 +1,9 @@
import * as mongo from 'mongodb';
import User, { IRemoteUser } from '../../../../models/user';
import config from '../../../../config';
import { IBlock } from '../../type';
import block from '../../../../services/blocking/create';
import { apLogger } from '../../logger';
import { Users } from '../../../../models';
import { IRemoteUser } from '../../../../models/entities/user';
const logger = apLogger;
@ -18,11 +18,9 @@ export default async (actor: IRemoteUser, activity: IBlock): Promise<void> => {
return null;
}
const blockee = await User.findOne({
_id: new mongo.ObjectID(id.split('/').pop())
});
const blockee = await Users.findOne(id.split('/').pop());
if (blockee === null) {
if (blockee == null) {
throw new Error('blockee not found');
}

View file

@ -1,4 +1,4 @@
import { IRemoteUser } from '../../../../models/user';
import { IRemoteUser } from '../../../../models/entities/user';
import { createImage } from '../../models/image';
export default async function(actor: IRemoteUser, image: any): Promise<void> {

View file

@ -1,5 +1,5 @@
import Resolver from '../../resolver';
import { IRemoteUser } from '../../../../models/user';
import { IRemoteUser } from '../../../../models/entities/user';
import createImage from './image';
import createNote from './note';
import { ICreate } from '../../type';

View file

@ -1,5 +1,5 @@
import Resolver from '../../resolver';
import { IRemoteUser } from '../../../../models/user';
import { IRemoteUser } from '../../../../models/entities/user';
import { createNote, fetchNote } from '../../models/note';
/**

View file

@ -1,9 +1,9 @@
import Resolver from '../../resolver';
import deleteNote from './note';
import Note from '../../../../models/note';
import { IRemoteUser } from '../../../../models/user';
import { IRemoteUser } from '../../../../models/entities/user';
import { IDelete } from '../../type';
import { apLogger } from '../../logger';
import { Notes } from '../../../../models';
/**
*
@ -27,7 +27,7 @@ export default async (actor: IRemoteUser, activity: IDelete): Promise<void> => {
break;
case 'Tombstone':
const note = await Note.findOne({ uri });
const note = await Notes.findOne({ uri });
if (note != null) {
deleteNote(actor, uri);
}

View file

@ -1,20 +1,20 @@
import Note from '../../../../models/note';
import { IRemoteUser } from '../../../../models/user';
import { IRemoteUser } from '../../../../models/entities/user';
import deleteNode from '../../../../services/note/delete';
import { apLogger } from '../../logger';
import { Notes } from '../../../../models';
const logger = apLogger;
export default async function(actor: IRemoteUser, uri: string): Promise<void> {
logger.info(`Deleting the Note: ${uri}`);
const note = await Note.findOne({ uri });
const note = await Notes.findOne({ uri });
if (note == null) {
throw new Error('note not found');
}
if (!note.userId.equals(actor._id)) {
if (note.userId !== actor.id) {
throw new Error('投稿を削除しようとしているユーザーは投稿の作成者ではありません');
}

View file

@ -1,8 +1,8 @@
import * as mongo from 'mongodb';
import User, { IRemoteUser } from '../../../models/user';
import { IRemoteUser } from '../../../models/entities/user';
import config from '../../../config';
import follow from '../../../services/following/create';
import { IFollow } from '../type';
import { Users } from '../../../models';
export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
@ -11,11 +11,9 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
return null;
}
const followee = await User.findOne({
_id: new mongo.ObjectID(id.split('/').pop())
});
const followee = await Users.findOne(id.split('/').pop());
if (followee === null) {
if (followee == null) {
throw new Error('followee not found');
}

View file

@ -1,5 +1,5 @@
import { Object } from '../type';
import { IRemoteUser } from '../../../models/user';
import { IRemoteUser } from '../../../models/entities/user';
import create from './create';
import performDeleteActivity from './delete';
import performUpdateActivity from './update';

View file

@ -1,8 +1,7 @@
import * as mongo from 'mongodb';
import Note from '../../../models/note';
import { IRemoteUser } from '../../../models/user';
import { IRemoteUser } from '../../../models/entities/user';
import { ILike } from '../type';
import create from '../../../services/note/reaction/create';
import { Notes } from '../../../models';
export default async (actor: IRemoteUser, activity: ILike) => {
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
@ -10,10 +9,10 @@ export default async (actor: IRemoteUser, activity: ILike) => {
// Transform:
// https://misskey.ex/notes/xxxx to
// xxxx
const noteId = new mongo.ObjectID(id.split('/').pop());
const noteId = id.split('/').pop();
const note = await Note.findOne({ _id: noteId });
if (note === null) {
const note = await Notes.findOne(noteId);
if (note == null) {
throw new Error();
}

View file

@ -1,8 +1,8 @@
import * as mongo from 'mongodb';
import User, { IRemoteUser } from '../../../../models/user';
import { IRemoteUser } from '../../../../models/entities/user';
import config from '../../../../config';
import reject from '../../../../services/following/requests/reject';
import { IFollow } from '../../type';
import { Users } from '../../../../models';
export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
const id = typeof activity.actor == 'string' ? activity.actor : activity.actor.id;
@ -11,11 +11,9 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
return null;
}
const follower = await User.findOne({
_id: new mongo.ObjectID(id.split('/').pop())
});
const follower = await Users.findOne(id.split('/').pop());
if (follower === null) {
if (follower == null) {
throw new Error('follower not found');
}

View file

@ -1,5 +1,5 @@
import Resolver from '../../resolver';
import { IRemoteUser } from '../../../../models/user';
import { IRemoteUser } from '../../../../models/entities/user';
import rejectFollow from './follow';
import { IReject, IFollow } from '../../type';
import { apLogger } from '../../logger';

View file

@ -1,4 +1,4 @@
import { IRemoteUser } from '../../../../models/user';
import { IRemoteUser } from '../../../../models/entities/user';
import { IRemove } from '../../type';
import { resolveNote } from '../../models/note';
import { removePinned } from '../../../../services/i/pin';
@ -14,7 +14,7 @@ export default async (actor: IRemoteUser, activity: IRemove): Promise<void> => {
if (activity.target === actor.featured) {
const note = await resolveNote(activity.object);
await removePinned(actor, note._id);
await removePinned(actor, note.id);
return;
}

View file

@ -1,9 +1,9 @@
import * as mongo from 'mongodb';
import User, { IRemoteUser } from '../../../../models/user';
import config from '../../../../config';
import { IBlock } from '../../type';
import unblock from '../../../../services/blocking/delete';
import { apLogger } from '../../logger';
import { IRemoteUser } from '../../../../models/entities/user';
import { Users } from '../../../../models';
const logger = apLogger;
@ -18,11 +18,9 @@ export default async (actor: IRemoteUser, activity: IBlock): Promise<void> => {
return null;
}
const blockee = await User.findOne({
_id: new mongo.ObjectID(id.split('/').pop())
});
const blockee = await Users.findOne(id.split('/').pop());
if (blockee === null) {
if (blockee == null) {
throw new Error('blockee not found');
}

View file

@ -1,11 +1,9 @@
import * as mongo from 'mongodb';
import User, { IRemoteUser } from '../../../../models/user';
import config from '../../../../config';
import unfollow from '../../../../services/following/delete';
import cancelRequest from '../../../../services/following/requests/cancel';
import { IFollow } from '../../type';
import FollowRequest from '../../../../models/follow-request';
import Following from '../../../../models/following';
import { IRemoteUser } from '../../../../models/entities/user';
import { Users, FollowRequests, Followings } from '../../../../models';
export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
@ -14,11 +12,9 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
return null;
}
const followee = await User.findOne({
_id: new mongo.ObjectID(id.split('/').pop())
});
const followee = await Users.findOne(id.split('/').pop());
if (followee === null) {
if (followee == null) {
throw new Error('followee not found');
}
@ -26,14 +22,14 @@ export default async (actor: IRemoteUser, activity: IFollow): Promise<void> => {
throw new Error('フォロー解除しようとしているユーザーはローカルユーザーではありません');
}
const req = await FollowRequest.findOne({
followerId: actor._id,
followeeId: followee._id
const req = await FollowRequests.findOne({
followerId: actor.id,
followeeId: followee.id
});
const following = await Following.findOne({
followerId: actor._id,
followeeId: followee._id
const following = await Followings.findOne({
followerId: actor.id,
followeeId: followee.id
});
if (req) {

View file

@ -1,4 +1,4 @@
import { IRemoteUser } from '../../../../models/user';
import { IRemoteUser } from '../../../../models/entities/user';
import { IUndo, IFollow, IBlock, ILike } from '../../type';
import unfollow from './follow';
import unblock from './block';

View file

@ -1,8 +1,7 @@
import * as mongo from 'mongodb';
import { IRemoteUser } from '../../../../models/user';
import { IRemoteUser } from '../../../../models/entities/user';
import { ILike } from '../../type';
import Note from '../../../../models/note';
import deleteReaction from '../../../../services/note/reaction/delete';
import { Notes } from '../../../../models';
/**
* Process Undo.Like activity
@ -10,10 +9,10 @@ import deleteReaction from '../../../../services/note/reaction/delete';
export default async (actor: IRemoteUser, activity: ILike): Promise<void> => {
const id = typeof activity.object == 'string' ? activity.object : activity.object.id;
const noteId = new mongo.ObjectID(id.split('/').pop());
const noteId = id.split('/').pop();
const note = await Note.findOne({ _id: noteId });
if (note === null) {
const note = await Notes.findOne(noteId);
if (note == null) {
throw 'note not found';
}

View file

@ -1,4 +1,4 @@
import { IRemoteUser } from '../../../../models/user';
import { IRemoteUser } from '../../../../models/entities/user';
import { IUpdate, IObject } from '../../type';
import { apLogger } from '../../logger';
import { updateQuestion } from '../../models/question';

View file

@ -1,9 +1,9 @@
import { INote } from '../../../models/note';
import { Note } from '../../../models/entities/note';
import { toHtml } from '../../../mfm/toHtml';
import { parse } from '../../../mfm/parse';
export default function(note: INote) {
let html = toHtml(parse(note.text), note.mentionedRemoteUsers);
export default function(note: Note) {
let html = toHtml(parse(note.text), JSON.parse(note.mentionedRemoteUsers));
if (html == null) html = '<p>.</p>';
return html;

View file

@ -1,16 +1,17 @@
import uploadFromUrl from '../../../services/drive/upload-from-url';
import { IRemoteUser } from '../../../models/user';
import DriveFile, { IDriveFile } from '../../../models/drive-file';
import { IRemoteUser } from '../../../models/entities/user';
import Resolver from '../resolver';
import fetchMeta from '../../../misc/fetch-meta';
import { apLogger } from '../logger';
import { DriveFile } from '../../../models/entities/drive-file';
import { DriveFiles } from '../../../models';
const logger = apLogger;
/**
* Imageを作成します
*/
export async function createImage(actor: IRemoteUser, value: any): Promise<IDriveFile> {
export async function createImage(actor: IRemoteUser, value: any): Promise<DriveFile> {
// 投稿者が凍結されていたらスキップ
if (actor.isSuspended) {
return null;
@ -39,18 +40,16 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<IDriv
throw e;
}
if (file.metadata.isRemote) {
if (file.isRemote) {
// URLが異なっている場合、同じ画像が以前に異なるURLで登録されていたということなので、
// URLを更新する
if (file.metadata.url !== image.url) {
file = await DriveFile.findOneAndUpdate({ _id: file._id }, {
$set: {
'metadata.url': image.url,
'metadata.uri': image.url
}
}, {
returnNewDocument: true
if (file.url !== image.url) {
await DriveFiles.update({ id: file.id }, {
url: image.url,
uri: image.url
});
file = DriveFiles.findOne(file.id);
}
}
@ -63,7 +62,7 @@ export async function createImage(actor: IRemoteUser, value: any): Promise<IDriv
* Misskeyに対象のImageが登録されていればそれを返し
* Misskeyに登録しそれを返します
*/
export async function resolveImage(actor: IRemoteUser, value: any): Promise<IDriveFile> {
export async function resolveImage(actor: IRemoteUser, value: any): Promise<DriveFile> {
// TODO
// リモートサーバーからフェッチしてきて登録

View file

@ -1,26 +1,27 @@
import * as mongo from 'mongodb';
import * as promiseLimit from 'promise-limit';
import config from '../../../config';
import Resolver from '../resolver';
import Note, { INote } from '../../../models/note';
import post from '../../../services/note/create';
import { INote as INoteActivityStreamsObject, IObject } from '../type';
import { resolvePerson, updatePerson } from './person';
import { resolveImage } from './image';
import { IRemoteUser, IUser } from '../../../models/user';
import { IRemoteUser, User } from '../../../models/entities/user';
import { fromHtml } from '../../../mfm/fromHtml';
import Emoji, { IEmoji } from '../../../models/emoji';
import { ITag, extractHashtags } from './tag';
import { toUnicode } from 'punycode';
import { unique, concat, difference } from '../../../prelude/array';
import { extractPollFromQuestion } from './question';
import vote from '../../../services/note/polls/vote';
import { apLogger } from '../logger';
import { IDriveFile } from '../../../models/drive-file';
import { DriveFile } from '../../../models/entities/drive-file';
import { deliverQuestionUpdate } from '../../../services/note/polls/update';
import Instance from '../../../models/instance';
import { extractDbHost } from '../../../misc/convert-host';
import { Notes, Emojis, Polls } from '../../../models';
import { Note } from '../../../models/entities/note';
import { IObject, INote } from '../type';
import { Emoji } from '../../../models/entities/emoji';
import { genId } from '../../../misc/gen-id';
import fetchMeta from '../../../misc/fetch-meta';
const logger = apLogger;
@ -29,17 +30,17 @@ const logger = apLogger;
*
* Misskeyに対象のNoteが登録されていればそれを返します
*/
export async function fetchNote(value: string | IObject, resolver?: Resolver): Promise<INote> {
export async function fetchNote(value: string | IObject, resolver?: Resolver): Promise<Note> {
const uri = typeof value == 'string' ? value : value.id;
// URIがこのサーバーを指しているならデータベースからフェッチ
if (uri.startsWith(config.url + '/')) {
const id = new mongo.ObjectID(uri.split('/').pop());
return await Note.findOne({ _id: id });
const id = uri.split('/').pop();
return await Notes.findOne(id);
}
//#region このサーバーに既に登録されていたらそれを返す
const exist = await Note.findOne({ uri });
const exist = await Notes.findOne({ uri });
if (exist) {
return exist;
@ -52,7 +53,7 @@ export async function fetchNote(value: string | IObject, resolver?: Resolver): P
/**
* Noteを作成します
*/
export async function createNote(value: any, resolver?: Resolver, silent = false): Promise<INote> {
export async function createNote(value: any, resolver?: Resolver, silent = false): Promise<Note> {
if (resolver == null) resolver = new Resolver();
const object: any = await resolver.resolve(value);
@ -68,7 +69,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
return null;
}
const note: INoteActivityStreamsObject = object;
const note: INote = object;
logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`);
@ -87,7 +88,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
note.cc = note.cc == null ? [] : typeof note.cc == 'string' ? [note.cc] : note.cc;
let visibility = 'public';
let visibleUsers: IUser[] = [];
let visibleUsers: User[] = [];
if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) {
if (note.cc.includes('https://www.w3.org/ns/activitystreams#Public')) {
visibility = 'home';
@ -113,12 +114,12 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : [];
const files = note.attachment
.map(attach => attach.sensitive = note.sensitive)
? (await Promise.all(note.attachment.map(x => limit(() => resolveImage(actor, x)) as Promise<IDriveFile>)))
? (await Promise.all(note.attachment.map(x => limit(() => resolveImage(actor, x)) as Promise<DriveFile>)))
.filter(image => image != null)
: [];
// リプライ
const reply: INote = note.inReplyTo
const reply: Note = note.inReplyTo
? await resolveNote(note.inReplyTo, resolver).catch(e => {
// 4xxの場合はリプライしてないことにする
if (e.statusCode >= 400 && e.statusCode < 500) {
@ -131,7 +132,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
: null;
// 引用
let quote: INote;
let quote: Note;
if (note._misskey_quote && typeof note._misskey_quote == 'string') {
quote = await resolveNote(note._misskey_quote).catch(e => {
@ -151,22 +152,23 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
const text = note._misskey_content || fromHtml(note.content);
// vote
if (reply && reply.poll) {
if (reply && reply.hasPoll) {
const poll = await Polls.findOne({ noteId: reply.id });
const tryCreateVote = async (name: string, index: number): Promise<null> => {
if (reply.poll.expiresAt && Date.now() > new Date(reply.poll.expiresAt).getTime()) {
if (poll.expiresAt && Date.now() > new Date(poll.expiresAt).getTime()) {
logger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
} else if (index >= 0) {
logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`);
await vote(actor, reply, index);
// リモートフォロワーにUpdate配信
deliverQuestionUpdate(reply._id);
deliverQuestionUpdate(reply.id);
}
return null;
};
if (note.name) {
return await tryCreateVote(note.name, reply.poll.choices.findIndex(x => x.text === note.name));
return await tryCreateVote(note.name, poll.choices.findIndex(x => x === note.name));
}
// 後方互換性のため
@ -181,7 +183,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
const emojis = await extractEmojis(note.tag, actor.host).catch(e => {
logger.info(`extractEmojis: ${e}`);
return [] as IEmoji[];
return [] as Emoji[];
});
const apEmojis = emojis.map(emoji => emoji.name);
@ -222,13 +224,13 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
* Misskeyに対象のNoteが登録されていればそれを返し
* Misskeyに登録しそれを返します
*/
export async function resolveNote(value: string | IObject, resolver?: Resolver): Promise<INote> {
export async function resolveNote(value: string | IObject, resolver?: Resolver): Promise<Note> {
const uri = typeof value == 'string' ? value : value.id;
// ブロックしてたら中断
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
const instance = await Instance.findOne({ host: extractDbHost(uri) });
if (instance && instance.isBlocked) throw { statusCode: 451 };
const meta = await fetchMeta();
if (meta.blockedHosts.includes(extractDbHost(uri))) throw { statusCode: 451 };
//#region このサーバーに既に登録されていたらそれを返す
const exist = await fetchNote(uri);
@ -255,7 +257,7 @@ export async function extractEmojis(tags: ITag[], host_: string) {
eomjiTags.map(async tag => {
const name = tag.name.replace(/^:/, '').replace(/:$/, '');
const exists = await Emoji.findOne({
const exists = await Emojis.findOne({
host,
name
});
@ -263,31 +265,37 @@ export async function extractEmojis(tags: ITag[], host_: string) {
if (exists) {
if ((tag.updated != null && exists.updatedAt == null)
|| (tag.id != null && exists.uri == null)
|| (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt)) {
return await Emoji.findOneAndUpdate({
host,
name,
}, {
$set: {
uri: tag.id,
url: tag.icon.url,
updatedAt: new Date(tag.updated),
}
});
|| (tag.updated != null && exists.updatedAt != null && new Date(tag.updated) > exists.updatedAt)
) {
await Emojis.update({
host,
name,
}, {
uri: tag.id,
url: tag.icon.url,
updatedAt: new Date(tag.updated),
});
return await Emojis.findOne({
host,
name
});
}
return exists;
}
logger.info(`register emoji host=${host}, name=${name}`);
return await Emoji.insert({
return await Emojis.save({
id: genId(),
host,
name,
uri: tag.id,
url: tag.icon.url,
updatedAt: tag.updated ? new Date(tag.updated) : undefined,
aliases: []
});
} as Emoji);
})
);
}
@ -298,7 +306,7 @@ async function extractMentionedUsers(actor: IRemoteUser, to: string[], cc: strin
const limit = promiseLimit(2);
const users = await Promise.all(
uris.map(uri => limit(() => resolvePerson(uri, null, resolver).catch(() => null)) as Promise<IUser>)
uris.map(uri => limit(() => resolvePerson(uri, null, resolver).catch(() => null)) as Promise<User>)
);
return users.filter(x => x != null);

View file

@ -1,29 +1,29 @@
import * as mongo from 'mongodb';
import * as promiseLimit from 'promise-limit';
import { toUnicode } from 'punycode';
import config from '../../../config';
import User, { validateUsername, isValidName, IUser, IRemoteUser, isRemoteUser } from '../../../models/user';
import Resolver from '../resolver';
import { resolveImage } from './image';
import { isCollectionOrOrderedCollection, isCollection, IPerson } from '../type';
import { IDriveFile } from '../../../models/drive-file';
import Meta from '../../../models/meta';
import { DriveFile } from '../../../models/entities/drive-file';
import { fromHtml } from '../../../mfm/fromHtml';
import usersChart from '../../../services/chart/users';
import instanceChart from '../../../services/chart/instance';
import { URL } from 'url';
import { resolveNote, extractEmojis } from './note';
import { registerOrFetchInstanceDoc } from '../../../services/register-or-fetch-instance-doc';
import Instance from '../../../models/instance';
import getDriveFileUrl from '../../../misc/get-drive-file-url';
import { IEmoji } from '../../../models/emoji';
import { ITag, extractHashtags } from './tag';
import Following from '../../../models/following';
import { IIdentifier } from './identifier';
import { apLogger } from '../logger';
import { INote } from '../../../models/note';
import { Note } from '../../../models/entities/note';
import { updateHashtag } from '../../../services/update-hashtag';
import { Users, UserNotePinings, Instances, DriveFiles, Followings, UserServiceLinkings, UserPublickeys } from '../../../models';
import { User, IRemoteUser } from '../../../models/entities/user';
import { Emoji } from '../../../models/entities/emoji';
import { UserNotePining } from '../../../models/entities/user-note-pinings';
import { genId } from '../../../misc/gen-id';
import { UserServiceLinking } from '../../../models/entities/user-service-linking';
import { instanceChart, usersChart } from '../../../services/chart';
import { UserPublickey } from '../../../models/entities/user-publickey';
import { isDuplicateKeyValueError } from '../../../misc/is-duplicate-key-value-error';
const logger = apLogger;
/**
@ -50,11 +50,11 @@ function validatePerson(x: any, uri: string) {
return new Error('invalid person: inbox is not a string');
}
if (!validateUsername(x.preferredUsername, true)) {
if (!Users.validateUsername(x.preferredUsername, true)) {
return new Error('invalid person: invalid username');
}
if (!isValidName(x.name == '' ? null : x.name)) {
if (!Users.isValidName(x.name == '' ? null : x.name)) {
return new Error('invalid person: invalid name');
}
@ -84,17 +84,17 @@ function validatePerson(x: any, uri: string) {
*
* Misskeyに対象のPersonが登録されていればそれを返します
*/
export async function fetchPerson(uri: string, resolver?: Resolver): Promise<IUser> {
export async function fetchPerson(uri: string, resolver?: Resolver): Promise<User> {
if (typeof uri !== 'string') throw 'uri is not string';
// URIがこのサーバーを指しているならデータベースからフェッチ
if (uri.startsWith(config.url + '/')) {
const id = new mongo.ObjectID(uri.split('/').pop());
return await User.findOne({ _id: id });
const id = uri.split('/').pop();
return await Users.findOne(id);
}
//#region このサーバーに既に登録されていたらそれを返す
const exist = await User.findOne({ uri });
const exist = await Users.findOne({ uri });
if (exist) {
return exist;
@ -107,7 +107,7 @@ export async function fetchPerson(uri: string, resolver?: Resolver): Promise<IUs
/**
* Personを作成します
*/
export async function createPerson(uri: string, resolver?: Resolver): Promise<IUser> {
export async function createPerson(uri: string, resolver?: Resolver): Promise<User> {
if (typeof uri !== 'string') throw 'uri is not string';
if (resolver == null) resolver = new Resolver();
@ -124,21 +124,6 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
logger.info(`Creating the Person: ${person.id}`);
const [followersCount = 0, followingCount = 0, notesCount = 0] = await Promise.all([
resolver.resolve(person.followers).then(
resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
() => undefined
),
resolver.resolve(person.following).then(
resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
() => undefined
),
resolver.resolve(person.outbox).then(
resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
() => undefined
)
]);
const host = toUnicode(new URL(object.id).hostname.toLowerCase());
const { fields, services } = analyzeAttachments(person.attachment);
@ -150,24 +135,18 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
// Create user
let user: IRemoteUser;
try {
user = await User.insert({
user = await Users.save({
id: genId(),
avatarId: null,
bannerId: null,
createdAt: Date.parse(person.published) || null,
createdAt: Date.parse(person.published) || new Date(),
lastFetchedAt: new Date(),
description: fromHtml(person.summary),
followersCount,
followingCount,
notesCount,
name: person.name,
isLocked: person.manuallyApprovesFollowers,
username: person.preferredUsername,
usernameLower: person.preferredUsername.toLowerCase(),
host,
publicKey: {
id: person.publicKey.id,
publicKeyPem: person.publicKey.publicKeyPem
},
inbox: person.inbox,
sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined),
featured: person.featured,
@ -179,10 +158,22 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
tags,
isBot,
isCat: (person as any).isCat === true
}) as IRemoteUser;
} as Partial<User>) as IRemoteUser;
await UserPublickeys.save({
id: genId(),
userId: user.id,
keyId: person.publicKey.id,
keyPem: person.publicKey.publicKeyPem
} as UserPublickey);
await UserServiceLinkings.save({
id: genId(),
userId: user.id,
} as UserServiceLinking);
} catch (e) {
// duplicate key error
if (e.code === 11000) {
if (isDuplicateKeyValueError(e)) {
throw new Error('already registered');
}
@ -190,33 +181,25 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
throw e;
}
await UserServiceLinkings.save({
id: genId(),
userId: user.id
} as UserServiceLinking);
// Register host
registerOrFetchInstanceDoc(host).then(i => {
Instance.update({ _id: i._id }, {
$inc: {
usersCount: 1
}
});
Instances.increment({ id: i.id }, 'usersCount', 1);
instanceChart.newUser(i.host);
});
//#region Increment users count
Meta.update({}, {
$inc: {
'stats.usersCount': 1
}
}, { upsert: true });
usersChart.update(user, true);
//#endregion
// ハッシュタグ更新
for (const tag of tags) updateHashtag(user, tag, true, true);
for (const tag of (user.tags || []).filter(x => !tags.includes(x))) updateHashtag(user, tag, true, false);
//#region アイコンとヘッダー画像をフェッチ
const [avatar, banner] = (await Promise.all<IDriveFile>([
const [avatar, banner] = (await Promise.all<DriveFile>([
person.icon,
person.image
].map(img =>
@ -225,22 +208,20 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
: resolveImage(user, img).catch(() => null)
)));
const avatarId = avatar ? avatar._id : null;
const bannerId = banner ? banner._id : null;
const avatarUrl = getDriveFileUrl(avatar, true);
const bannerUrl = getDriveFileUrl(banner, false);
const avatarColor = avatar && avatar.metadata.properties.avgColor ? avatar.metadata.properties.avgColor : null;
const bannerColor = banner && avatar.metadata.properties.avgColor ? banner.metadata.properties.avgColor : null;
const avatarId = avatar ? avatar.id : null;
const bannerId = banner ? banner.id : null;
const avatarUrl = DriveFiles.getPublicUrl(avatar);
const bannerUrl = DriveFiles.getPublicUrl(banner);
const avatarColor = avatar && avatar.properties.avgColor ? avatar.properties.avgColor : null;
const bannerColor = banner && avatar.properties.avgColor ? banner.properties.avgColor : null;
await User.update({ _id: user._id }, {
$set: {
avatarId,
bannerId,
avatarUrl,
bannerUrl,
avatarColor,
bannerColor
}
await Users.update(user.id, {
avatarId,
bannerId,
avatarUrl,
bannerUrl,
avatarColor,
bannerColor
});
user.avatarId = avatarId;
@ -254,19 +235,17 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
//#region カスタム絵文字取得
const emojis = await extractEmojis(person.tag, host).catch(e => {
logger.info(`extractEmojis: ${e}`);
return [] as IEmoji[];
return [] as Emoji[];
});
const emojiNames = emojis.map(emoji => emoji.name);
await User.update({ _id: user._id }, {
$set: {
emojis: emojiNames
}
await Users.update(user.id, {
emojis: emojiNames
});
//#endregion
await updateFeatured(user._id).catch(err => logger.error(err));
await updateFeatured(user.id).catch(err => logger.error(err));
return user;
}
@ -287,7 +266,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
}
//#region このサーバーに既に登録されているか
const exist = await User.findOne({ uri }) as IRemoteUser;
const exist = await Users.findOne({ uri }) as IRemoteUser;
if (exist == null) {
return;
@ -295,10 +274,8 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
//#endregion
// 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する
await User.update({ _id: exist._id }, {
$set: {
lastFetchedAt: new Date(),
},
await Users.update(exist.id, {
lastFetchedAt: new Date(),
});
if (resolver == null) resolver = new Resolver();
@ -315,23 +292,8 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
logger.info(`Updating the Person: ${person.id}`);
const [followersCount = 0, followingCount = 0, notesCount = 0] = await Promise.all([
resolver.resolve(person.followers).then(
resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
() => undefined
),
resolver.resolve(person.following).then(
resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
() => undefined
),
resolver.resolve(person.outbox).then(
resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
() => undefined
)
]);
// アイコンとヘッダー画像をフェッチ
const [avatar, banner] = (await Promise.all<IDriveFile>([
const [avatar, banner] = (await Promise.all<DriveFile>([
person.icon,
person.image
].map(img =>
@ -343,7 +305,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
// カスタム絵文字取得
const emojis = await extractEmojis(person.tag, exist.host).catch(e => {
logger.info(`extractEmojis: ${e}`);
return [] as IEmoji[];
return [] as Emoji[];
});
const emojiNames = emojis.map(emoji => emoji.name);
@ -359,40 +321,45 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
featured: person.featured,
emojis: emojiNames,
description: fromHtml(person.summary),
followersCount,
followingCount,
notesCount,
name: person.name,
url: person.url,
endpoints: person.endpoints,
fields,
...services,
tags,
isBot: object.type == 'Service',
isCat: (person as any).isCat === true,
isLocked: person.manuallyApprovesFollowers,
createdAt: Date.parse(person.published) || null,
publicKey: {
id: person.publicKey.id,
publicKeyPem: person.publicKey.publicKeyPem
},
} as any;
createdAt: new Date(Date.parse(person.published)) || null,
} as Partial<User>;
if (avatar) {
updates.avatarId = avatar._id;
updates.avatarUrl = getDriveFileUrl(avatar, true);
updates.avatarColor = avatar.metadata.properties.avgColor ? avatar.metadata.properties.avgColor : null;
updates.avatarId = avatar.id;
updates.avatarUrl = DriveFiles.getPublicUrl(avatar);
updates.avatarColor = avatar.properties.avgColor ? avatar.properties.avgColor : null;
}
if (banner) {
updates.bannerId = banner._id;
updates.bannerUrl = getDriveFileUrl(banner, true);
updates.bannerColor = banner.metadata.properties.avgColor ? banner.metadata.properties.avgColor : null;
updates.bannerId = banner.id;
updates.bannerUrl = DriveFiles.getPublicUrl(banner);
updates.bannerColor = banner.properties.avgColor ? banner.properties.avgColor : null;
}
// Update user
await User.update({ _id: exist._id }, {
$set: updates
await Users.update(exist.id, updates);
await UserPublickeys.update({ userId: exist.id }, {
keyId: person.publicKey.id,
keyPem: person.publicKey.publicKeyPem
});
await UserServiceLinkings.update({ userId: exist.id }, {
twitterUserId: services.twitter.userId,
twitterScreenName: services.twitter.screenName,
githubId: services.github.id,
githubLogin: services.github.login,
discordId: services.discord.id,
discordUsername: services.discord.username,
discordDiscriminator: services.discord.discriminator,
});
// ハッシュタグ更新
@ -400,17 +367,13 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
for (const tag of (exist.tags || []).filter(x => !tags.includes(x))) updateHashtag(exist, tag, true, false);
// 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする
await Following.update({
followerId: exist._id
await Followings.update({
followerId: exist.id
}, {
$set: {
'_follower.sharedInbox': person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined)
}
}, {
multi: true
followerSharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined)
});
await updateFeatured(exist._id).catch(err => logger.error(err));
await updateFeatured(exist.id).catch(err => logger.error(err));
}
/**
@ -419,7 +382,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
* Misskeyに対象のPersonが登録されていればそれを返し
* Misskeyに登録しそれを返します
*/
export async function resolvePerson(uri: string, verifier?: string, resolver?: Resolver): Promise<IUser> {
export async function resolvePerson(uri: string, verifier?: string, resolver?: Resolver): Promise<User> {
if (typeof uri !== 'string') throw 'uri is not string';
//#region このサーバーに既に登録されていたらそれを返す
@ -492,9 +455,9 @@ export function analyzeAttachments(attachments: ITag[]) {
return { fields, services };
}
export async function updateFeatured(userId: mongo.ObjectID) {
const user = await User.findOne({ _id: userId });
if (!isRemoteUser(user)) return;
export async function updateFeatured(userId: User['id']) {
const user = await Users.findOne(userId);
if (!Users.isRemoteUser(user)) return;
if (!user.featured) return;
logger.info(`Updating the featured: ${user.uri}`);
@ -515,11 +478,14 @@ export async function updateFeatured(userId: mongo.ObjectID) {
const featuredNotes = await Promise.all(items
.filter(item => item.type === 'Note')
.slice(0, 5)
.map(item => limit(() => resolveNote(item, resolver)) as Promise<INote>));
.map(item => limit(() => resolveNote(item, resolver)) as Promise<Note>));
await User.update({ _id: user._id }, {
$set: {
pinnedNoteIds: featuredNotes.filter(note => note != null).map(note => note._id)
}
});
for (const note of featuredNotes.filter(note => note != null)) {
UserNotePinings.save({
id: genId(),
createdAt: new Date(),
userId: user.id,
noteId: note.id
} as UserNotePining);
}
}

View file

@ -1,8 +1,9 @@
import config from '../../../config';
import Note, { IChoice, IPoll } from '../../../models/note';
import Resolver from '../resolver';
import { IQuestion } from '../type';
import { apLogger } from '../logger';
import { Notes, Polls } from '../../../models';
import { IPoll } from '../../../models/entities/poll';
export async function extractPollFromQuestion(source: string | IQuestion): Promise<IPoll> {
const question = typeof source === 'string' ? await new Resolver().resolve(source) as IQuestion : source;
@ -14,14 +15,14 @@ export async function extractPollFromQuestion(source: string | IQuestion): Promi
}
const choices = question[multiple ? 'anyOf' : 'oneOf']
.map((x, i) => ({
id: i,
text: x.name,
votes: x.replies && x.replies.totalItems || x._misskey_votes || 0,
} as IChoice));
.map((x, i) => x.name);
const votes = question[multiple ? 'anyOf' : 'oneOf']
.map((x, i) => x.replies && x.replies.totalItems || x._misskey_votes || 0);
return {
choices,
votes,
multiple,
expiresAt
};
@ -39,9 +40,11 @@ export async function updateQuestion(value: any) {
if (uri.startsWith(config.url + '/')) throw 'uri points local';
//#region このサーバーに既に登録されているか
const note = await Note.findOne({ uri });
const note = await Notes.findOne({ uri });
if (note == null) throw 'Question is not registed';
const poll = await Polls.findOne({ noteId: note.id });
if (poll == null) throw 'Question is not registed';
//#endregion
// resolve new Question object
@ -52,27 +55,25 @@ export async function updateQuestion(value: any) {
if (question.type !== 'Question') throw 'object is not a Question';
const apChoices = question.oneOf || question.anyOf;
const dbChoices = note.poll.choices;
let changed = false;
for (const db of dbChoices) {
const oldCount = db.votes;
const newCount = apChoices.filter(ap => ap.name === db.text)[0].replies.totalItems;
for (const choice of poll.choices) {
const oldCount = poll.votes[poll.choices.indexOf(choice)];
const newCount = apChoices.filter(ap => ap.name === choice)[0].replies.totalItems;
if (oldCount != newCount) {
changed = true;
db.votes = newCount;
poll.votes[poll.choices.indexOf(choice)] = newCount;
}
}
await Note.update({
_id: note._id
}, {
$set: {
'poll.choices': dbChoices,
updatedAt: new Date(),
}
await Notes.update(note.id, {
updatedAt: new Date(),
});
await Polls.update(poll.id, {
votes: poll.votes
});
return changed;

View file

@ -1,5 +1,5 @@
import { Object } from './type';
import { IRemoteUser } from '../../models/user';
import { IRemoteUser } from '../../models/entities/user';
import kernel from './kernel';
export default async (actor: IRemoteUser, activity: Object): Promise<void> => {

View file

@ -1,8 +1,8 @@
import config from '../../../config';
import { ILocalUser } from '../../../models/user';
import { ILocalUser } from '../../../models/entities/user';
export default (object: any, user: ILocalUser) => ({
type: 'Accept',
actor: `${config.url}/users/${user._id}`,
actor: `${config.url}/users/${user.id}`,
object
});

View file

@ -1,9 +1,9 @@
import config from '../../../config';
import { ILocalUser } from '../../../models/user';
import { ILocalUser } from '../../../models/entities/user';
export default (user: ILocalUser, target: any, object: any) => ({
type: 'Add',
actor: `${config.url}/users/${user._id}`,
actor: `${config.url}/users/${user.id}`,
target,
object
});

View file

@ -1,7 +1,7 @@
import config from '../../../config';
import { INote } from '../../../models/note';
import { Note } from '../../../models/entities/note';
export default (object: any, note: INote) => {
export default (object: any, note: Note) => {
const attributedTo = `${config.url}/users/${note.userId}`;
let to: string[] = [];
@ -18,7 +18,7 @@ export default (object: any, note: INote) => {
}
return {
id: `${config.url}/notes/${note._id}/activity`,
id: `${config.url}/notes/${note.id}/activity`,
actor: `${config.url}/users/${note.userId}`,
type: 'Announce',
published: note.createdAt.toISOString(),

View file

@ -1,8 +1,8 @@
import config from '../../../config';
import { ILocalUser, IRemoteUser } from '../../../models/user';
import { ILocalUser, IRemoteUser } from '../../../models/entities/user';
export default (blocker?: ILocalUser, blockee?: IRemoteUser) => ({
type: 'Block',
actor: `${config.url}/users/${blocker._id}`,
actor: `${config.url}/users/${blocker.id}`,
object: blockee.uri
});

View file

@ -1,9 +1,9 @@
import config from '../../../config';
import { INote } from '../../../models/note';
import { Note } from '../../../models/entities/note';
export default (object: any, note: INote) => {
export default (object: any, note: Note) => {
const activity = {
id: `${config.url}/notes/${note._id}/activity`,
id: `${config.url}/notes/${note.id}/activity`,
actor: `${config.url}/users/${note.userId}`,
type: 'Create',
published: note.createdAt.toISOString(),

View file

@ -1,8 +1,8 @@
import config from '../../../config';
import { ILocalUser } from '../../../models/user';
import { ILocalUser } from '../../../models/entities/user';
export default (object: any, user: ILocalUser) => ({
type: 'Delete',
actor: `${config.url}/users/${user._id}`,
actor: `${config.url}/users/${user.id}`,
object
});

View file

@ -1,8 +1,8 @@
import { IDriveFile } from '../../../models/drive-file';
import getDriveFileUrl from '../../../misc/get-drive-file-url';
import { DriveFile } from '../../../models/entities/drive-file';
import { DriveFiles } from '../../../models';
export default (file: IDriveFile) => ({
export default (file: DriveFile) => ({
type: 'Document',
mediaType: file.contentType,
url: getDriveFileUrl(file)
mediaType: file.type,
url: DriveFiles.getPublicUrl(file)
});

View file

@ -1,7 +1,7 @@
import { IEmoji } from '../../../models/emoji';
import config from '../../../config';
import { Emoji } from '../../../models/entities/emoji';
export default (emoji: IEmoji) => ({
export default (emoji: Emoji) => ({
id: `${config.url}/emojis/${emoji.name}`,
type: 'Emoji',
name: `:${emoji.name}:`,

View file

@ -1,16 +1,12 @@
import config from '../../../config';
import * as mongo from 'mongodb';
import User, { isLocalUser } from '../../../models/user';
import { Users } from '../../../models';
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: mongo.ObjectID): Promise<any> {
const user = await User.findOne({
_id: id
});
return isLocalUser(user) ? `${config.url}/users/${user._id}` : user.uri;
export default async function renderFollowUser(id: User['id']): Promise<any> {
const user = await Users.findOne(id);
return Users.isLocalUser(user) ? `${config.url}/users/${user.id}` : user.uri;
}

View file

@ -1,11 +1,12 @@
import config from '../../../config';
import { IUser, isLocalUser } from '../../../models/user';
import { User } from '../../../models/entities/user';
import { Users } from '../../../models';
export default (follower: IUser, followee: IUser, requestId?: string) => {
export default (follower: User, followee: User, requestId?: string) => {
const follow = {
type: 'Follow',
actor: isLocalUser(follower) ? `${config.url}/users/${follower._id}` : follower.uri,
object: isLocalUser(followee) ? `${config.url}/users/${followee._id}` : followee.uri
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;

View file

@ -1,8 +1,8 @@
import { IDriveFile } from '../../../models/drive-file';
import getDriveFileUrl from '../../../misc/get-drive-file-url';
import { DriveFile } from '../../../models/entities/drive-file';
import { DriveFiles } from '../../../models';
export default (file: IDriveFile) => ({
export default (file: DriveFile) => ({
type: 'Image',
url: getDriveFileUrl(file),
sensitive: file.metadata.isSensitive
url: DriveFiles.getPublicUrl(file),
sensitive: file.isSensitive
});

View file

@ -1,10 +1,11 @@
import { createPublicKey } from 'crypto';
import config from '../../../config';
import { extractPublic } from '../../../crypto_key';
import { ILocalUser } from '../../../models/user';
import { ILocalUser } from '../../../models/entities/user';
import { UserKeypair } from '../../../models/entities/user-keypair';
export default (user: ILocalUser) => ({
id: `${config.url}/users/${user._id}/publickey`,
export default (user: ILocalUser, key: UserKeypair) => ({
id: `${config.url}/users/${user.id}/publickey`,
type: 'Key',
owner: `${config.url}/users/${user._id}`,
publicKeyPem: extractPublic(user.keypair)
owner: `${config.url}/users/${user.id}`,
publicKeyPem: createPublicKey(key.keyPem)
});

View file

@ -1,10 +1,10 @@
import config from '../../../config';
import { ILocalUser } from '../../../models/user';
import { INote } from '../../../models/note';
import { ILocalUser } from '../../../models/entities/user';
import { Note } from '../../../models/entities/note';
export default (user: ILocalUser, note: INote, reaction: string) => ({
export default (user: ILocalUser, note: Note, reaction: string) => ({
type: 'Like',
actor: `${config.url}/users/${user._id}`,
object: note.uri ? note.uri : `${config.url}/notes/${note._id}`,
actor: `${config.url}/users/${user.id}`,
object: note.uri ? note.uri : `${config.url}/notes/${note.id}`,
_misskey_reaction: reaction
});

View file

@ -1,8 +1,9 @@
import { IUser, isRemoteUser } from '../../../models/user';
import config from '../../../config';
import { User, ILocalUser } from '../../../models/entities/user';
import { Users } from '../../../models';
export default (mention: IUser) => ({
export default (mention: User) => ({
type: 'Mention',
href: isRemoteUser(mention) ? mention.uri : `${config.url}/@${mention.username}`,
name: isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${mention.username}`,
href: Users.isRemoteUser(mention) ? mention.uri : `${config.url}/@${(mention as ILocalUser).username}`,
name: Users.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as ILocalUser).username}`,
});

View file

@ -3,29 +3,27 @@ import renderHashtag from './hashtag';
import renderMention from './mention';
import renderEmoji from './emoji';
import config from '../../../config';
import DriveFile, { IDriveFile } from '../../../models/drive-file';
import Note, { INote } from '../../../models/note';
import User from '../../../models/user';
import toHtml from '../misc/get-note-html';
import Emoji, { IEmoji } from '../../../models/emoji';
import { Note, IMentionedRemoteUsers } from '../../../models/entities/note';
import { DriveFile } from '../../../models/entities/drive-file';
import { DriveFiles, Notes, Users, Emojis, Polls } from '../../../models';
import { In } from 'typeorm';
import { Emoji } from '../../../models/entities/emoji';
import { Poll } from '../../../models/entities/poll';
export default async function renderNote(note: INote, dive = true): Promise<any> {
const promisedFiles: Promise<IDriveFile[]> = note.fileIds
? DriveFile.find({ _id: { $in: note.fileIds } })
export default async function renderNote(note: Note, dive = true): Promise<any> {
const promisedFiles: Promise<DriveFile[]> = note.fileIds.length > 1
? DriveFiles.find({ id: In(note.fileIds) })
: Promise.resolve([]);
let inReplyTo;
let inReplyToNote: INote;
let inReplyToNote: Note;
if (note.replyId) {
inReplyToNote = await Note.findOne({
_id: note.replyId,
});
inReplyToNote = await Notes.findOne(note.replyId);
if (inReplyToNote !== null) {
const inReplyToUser = await User.findOne({
_id: inReplyToNote.userId,
});
const inReplyToUser = await Users.findOne(inReplyToNote.userId);
if (inReplyToUser !== null) {
if (inReplyToNote.uri) {
@ -34,7 +32,7 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
if (dive) {
inReplyTo = await renderNote(inReplyToNote, false);
} else {
inReplyTo = `${config.url}/notes/${inReplyToNote._id}`;
inReplyTo = `${config.url}/notes/${inReplyToNote.id}`;
}
}
}
@ -46,24 +44,20 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
let quote;
if (note.renoteId) {
const renote = await Note.findOne({
_id: note.renoteId,
});
const renote = await Notes.findOne(note.renoteId);
if (renote) {
quote = renote.uri ? renote.uri : `${config.url}/notes/${renote._id}`;
quote = renote.uri ? renote.uri : `${config.url}/notes/${renote.id}`;
}
}
const user = await User.findOne({
_id: note.userId
const user = await Users.findOne({
id: note.userId
});
const attributedTo = `${config.url}/users/${user._id}`;
const attributedTo = `${config.url}/users/${user.id}`;
const mentions = note.mentionedRemoteUsers && note.mentionedRemoteUsers.length > 0
? note.mentionedRemoteUsers.map(x => x.uri)
: [];
const mentions = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri);
let to: string[] = [];
let cc: string[] = [];
@ -81,10 +75,8 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
to = mentions;
}
const mentionedUsers = note.mentions ? await User.find({
_id: {
$in: note.mentions
}
const mentionedUsers = note.mentions.length > 0 ? await Users.find({
id: In(note.mentions)
}) : [];
const hashtagTags = (note.tags || []).map(tag => renderHashtag(tag));
@ -93,23 +85,28 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
const files = await promisedFiles;
let text = note.text;
let poll: Poll;
if (note.hasPoll) {
poll = await Polls.findOne({ noteId: note.id });
}
let question: string;
if (note.poll != null) {
if (poll) {
if (text == null) text = '';
const url = `${config.url}/notes/${note._id}`;
const url = `${config.url}/notes/${note.id}`;
// TODO: i18n
text += `\n[リモートで結果を表示](${url})`;
question = `${config.url}/questions/${note._id}`;
question = `${config.url}/questions/${note.id}`;
}
let apText = text;
if (apText == null) apText = '';
// Provides choices as text for AP
if (note.poll != null) {
const cs = note.poll.choices.map(c => `${c.id}: ${c.text}`);
if (poll) {
const cs = poll.choices.map((c, i) => `${i}: ${c}`);
apText += '\n----------------------------------------\n';
apText += cs.join('\n');
apText += '\n----------------------------------------\n';
@ -135,31 +132,25 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
...apemojis,
];
const {
choices = [],
expiresAt = null,
multiple = false
} = note.poll || {};
const asPoll = note.poll ? {
const asPoll = poll ? {
type: 'Question',
content: toHtml(Object.assign({}, note, {
text: text
})),
_misskey_fallback_content: content,
[expiresAt && expiresAt < new Date() ? 'closed' : 'endTime']: expiresAt,
[multiple ? 'anyOf' : 'oneOf']: choices.map(({ text, votes }) => ({
[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: votes
totalItems: poll.votes[i]
}
}))
} : {};
return {
id: `${config.url}/notes/${note._id}`,
id: `${config.url}/notes/${note.id}`,
type: 'Note',
attributedTo,
summary,
@ -172,17 +163,17 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
cc,
inReplyTo,
attachment: files.map(renderDocument),
sensitive: files.some(file => file.metadata.isSensitive),
sensitive: files.some(file => file.isSensitive),
tag,
...asPoll
};
}
export async function getEmojis(names: string[]): Promise<IEmoji[]> {
if (names == null || names.length < 1) return [];
export async function getEmojis(names: string[]): Promise<Emoji[]> {
if (names == null || names.length === 0) return [];
const emojis = await Promise.all(
names.map(name => Emoji.findOne({
names.map(name => Emojis.findOne({
name,
host: null
}))

View file

@ -1,21 +1,22 @@
import renderImage from './image';
import renderKey from './key';
import config from '../../../config';
import { ILocalUser } from '../../../models/user';
import { ILocalUser } from '../../../models/entities/user';
import { toHtml } from '../../../mfm/toHtml';
import { parse } from '../../../mfm/parse';
import DriveFile from '../../../models/drive-file';
import { getEmojis } from './note';
import renderEmoji from './emoji';
import { IIdentifier } from '../models/identifier';
import renderHashtag from './hashtag';
import { DriveFiles, UserServiceLinkings, UserKeypairs } from '../../../models';
export default async (user: ILocalUser) => {
const id = `${config.url}/users/${user._id}`;
export async function renderPerson(user: ILocalUser) {
const id = `${config.url}/users/${user.id}`;
const [avatar, banner] = await Promise.all([
DriveFile.findOne({ _id: user.avatarId }),
DriveFile.findOne({ _id: user.bannerId })
const [avatar, banner, links] = await Promise.all([
DriveFiles.findOne(user.avatarId),
DriveFiles.findOne(user.bannerId),
UserServiceLinkings.findOne({ userId: user.id })
]);
const attachment: {
@ -26,41 +27,41 @@ export default async (user: ILocalUser) => {
identifier?: IIdentifier
}[] = [];
if (user.twitter) {
if (links.twitter) {
attachment.push({
type: 'PropertyValue',
name: 'Twitter',
value: `<a href="https://twitter.com/intent/user?user_id=${user.twitter.userId}" rel="me nofollow noopener" target="_blank"><span>@${user.twitter.screenName}</span></a>`,
value: `<a href="https://twitter.com/intent/user?user_id=${links.twitterUserId}" rel="me nofollow noopener" target="_blank"><span>@${links.twitterScreenName}</span></a>`,
identifier: {
type: 'PropertyValue',
name: 'misskey:authentication:twitter',
value: `${user.twitter.userId}@${user.twitter.screenName}`
value: `${links.twitterUserId}@${links.twitterScreenName}`
}
});
}
if (user.github) {
if (links.github) {
attachment.push({
type: 'PropertyValue',
name: 'GitHub',
value: `<a href="https://github.com/${user.github.login}" rel="me nofollow noopener" target="_blank"><span>@${user.github.login}</span></a>`,
value: `<a href="https://github.com/${links.githubLogin}" rel="me nofollow noopener" target="_blank"><span>@${links.githubLogin}</span></a>`,
identifier: {
type: 'PropertyValue',
name: 'misskey:authentication:github',
value: `${user.github.id}@${user.github.login}`
value: `${links.githubId}@${links.githubLogin}`
}
});
}
if (user.discord) {
if (links.discord) {
attachment.push({
type: 'PropertyValue',
name: 'Discord',
value: `<a href="https://discordapp.com/users/${user.discord.id}" rel="me nofollow noopener" target="_blank"><span>${user.discord.username}#${user.discord.discriminator}</span></a>`,
value: `<a href="https://discordapp.com/users/${links.discordId}" rel="me nofollow noopener" target="_blank"><span>${links.discordUsername}#${links.discordDiscriminator}</span></a>`,
identifier: {
type: 'PropertyValue',
name: 'misskey:authentication:discord',
value: `${user.discord.id}@${user.discord.username}#${user.discord.discriminator}`
value: `${links.discordId}@${links.discordUsername}#${links.discordDiscriminator}`
}
});
}
@ -75,6 +76,10 @@ export default async (user: ILocalUser) => {
...hashtagTags,
];
const keypair = await UserKeypairs.findOne({
userId: user.id
});
return {
type: user.isBot ? 'Service' : 'Person',
id,
@ -93,8 +98,8 @@ export default async (user: ILocalUser) => {
image: user.bannerId && renderImage(banner),
tag,
manuallyApprovesFollowers: user.isLocked,
publicKey: renderKey(user),
publicKey: renderKey(user, keypair),
isCat: user.isCat,
attachment: attachment.length ? attachment : undefined
};
};
}

View file

@ -1,19 +1,20 @@
import config from '../../../config';
import { ILocalUser } from '../../../models/user';
import { INote } from '../../../models/note';
import { ILocalUser } from '../../../models/entities/user';
import { Note } from '../../../models/entities/note';
import { Poll } from '../../../models/entities/poll';
export default async function renderQuestion(user: ILocalUser, note: INote) {
export default async function renderQuestion(user: ILocalUser, note: Note, poll: Poll) {
const question = {
type: 'Question',
id: `${config.url}/questions/${note._id}`,
actor: `${config.url}/users/${user._id}`,
id: `${config.url}/questions/${note.id}`,
actor: `${config.url}/users/${user.id}`,
content: note.text || '',
[note.poll.multiple ? 'anyOf' : 'oneOf']: note.poll.choices.map(c => ({
name: c.text,
_misskey_votes: c.votes,
[poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({
name: text,
_misskey_votes: poll.votes[i],
replies: {
type: 'Collection',
totalItems: c.votes
totalItems: poll.votes[i]
}
}))
};

View file

@ -1,8 +1,8 @@
import config from '../../../config';
import { ILocalUser } from '../../../models/user';
import { ILocalUser } from '../../../models/entities/user';
export default (object: any, user: ILocalUser) => ({
type: 'Reject',
actor: `${config.url}/users/${user._id}`,
actor: `${config.url}/users/${user.id}`,
object
});

View file

@ -1,9 +1,9 @@
import config from '../../../config';
import { ILocalUser } from '../../../models/user';
import { ILocalUser } from '../../../models/entities/user';
export default (user: ILocalUser, target: any, object: any) => ({
type: 'Remove',
actor: `${config.url}/users/${user._id}`,
actor: `${config.url}/users/${user.id}`,
target,
object
});

View file

@ -1,8 +1,8 @@
import config from '../../../config';
import { ILocalUser, IUser } from '../../../models/user';
import { ILocalUser, User } from '../../../models/entities/user';
export default (object: any, user: ILocalUser | IUser) => ({
export default (object: any, user: ILocalUser | User) => ({
type: 'Undo',
actor: `${config.url}/users/${user._id}`,
actor: `${config.url}/users/${user.id}`,
object
});

View file

@ -1,10 +1,10 @@
import config from '../../../config';
import { ILocalUser } from '../../../models/user';
import { ILocalUser } from '../../../models/entities/user';
export default (object: any, user: ILocalUser) => {
const activity = {
id: `${config.url}/users/${user._id}#updates/${new Date().getTime()}`,
actor: `${config.url}/users/${user._id}`,
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

View file

@ -1,22 +1,23 @@
import config from '../../../config';
import { INote } from '../../../models/note';
import { IRemoteUser, ILocalUser } from '../../../models/user';
import { IPollVote } from '../../../models/poll-vote';
import { Note } from '../../../models/entities/note';
import { IRemoteUser, ILocalUser } from '../../../models/entities/user';
import { PollVote } from '../../../models/entities/poll-vote';
import { Poll } from '../../../models/entities/poll';
export default async function renderVote(user: ILocalUser, vote: IPollVote, pollNote: INote, pollOwner: IRemoteUser): Promise<any> {
export default async function renderVote(user: ILocalUser, 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}`,
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}`,
id: `${config.url}/users/${user.id}#votes/${vote.id}`,
type: 'Note',
attributedTo: `${config.url}/users/${user._id}`,
attributedTo: `${config.url}/users/${user.id}`,
to: [pollOwner.uri],
inReplyTo: pollNote.uri,
name: pollNote.poll.choices.find(x => x.id === vote.choice).text
inReplyTo: note.uri,
name: poll.choices[vote.choice]
}
};
}

View file

@ -7,10 +7,11 @@ import * as promiseAny from 'promise-any';
import { toUnicode } from 'punycode';
import config from '../../config';
import { ILocalUser } from '../../models/user';
import { ILocalUser } from '../../models/entities/user';
import { publishApLogStream } from '../../services/stream';
import { apLogger } from './logger';
import Instance from '../../models/instance';
import { UserKeypairs } from '../../models';
import fetchMeta from '../../misc/fetch-meta';
export const logger = apLogger.createSubLogger('deliver');
@ -23,8 +24,8 @@ export default async (user: ILocalUser, url: string, object: any) => {
// ブロックしてたら中断
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
const instance = await Instance.findOne({ host: toUnicode(host) });
if (instance && instance.isBlocked) return;
const meta = await fetchMeta();
if (meta.blockedHosts.includes(toUnicode(host))) return;
const data = JSON.stringify(object);
@ -35,6 +36,10 @@ export default async (user: ILocalUser, url: string, object: any) => {
const addr = await resolveAddr(hostname);
if (!addr) return;
const keypair = await UserKeypairs.findOne({
userId: user.id
});
const _ = new Promise((resolve, reject) => {
const req = request({
protocol,
@ -62,8 +67,8 @@ export default async (user: ILocalUser, url: string, object: any) => {
sign(req, {
authorizationHeaderName: 'Signature',
key: user.keypair,
keyId: `${config.url}/users/${user._id}/publickey`,
key: keypair.keyPem,
keyId: `${config.url}/users/${user.id}/publickey`,
headers: ['date', 'host', 'digest']
});

View file

@ -64,7 +64,7 @@ export default class Resolver {
json: true
});
if (object === null || (
if (object == null || (
Array.isArray(object['@context']) ?
!object['@context'].includes('https://www.w3.org/ns/activitystreams') :
object['@context'] !== 'https://www.w3.org/ns/activitystreams'

View file

@ -1,20 +1,21 @@
import { toUnicode, toASCII } from 'punycode';
import User, { IUser, IRemoteUser } from '../models/user';
import webFinger from './webfinger';
import config from '../config';
import { createPerson, updatePerson } from './activitypub/models/person';
import { URL } from 'url';
import { remoteLogger } from './logger';
import chalk from 'chalk';
import { User, IRemoteUser } from '../models/entities/user';
import { Users } from '../models';
const logger = remoteLogger.createSubLogger('resolve-user');
export default async (username: string, _host: string, option?: any, resync?: boolean): Promise<IUser> => {
export default async (username: string, _host: string, option?: any, resync = false): Promise<User> => {
const usernameLower = username.toLowerCase();
if (_host == null) {
logger.info(`return local user: ${usernameLower}`);
return await User.findOne({ usernameLower, host: null });
return await Users.findOne({ usernameLower, host: null });
}
const configHostAscii = toASCII(config.host).toLowerCase();
@ -25,14 +26,14 @@ export default async (username: string, _host: string, option?: any, resync?: bo
if (configHost == host) {
logger.info(`return local user: ${usernameLower}`);
return await User.findOne({ usernameLower, host: null });
return await Users.findOne({ usernameLower, host: null });
}
const user = await User.findOne({ usernameLower, host }, option);
const user = await Users.findOne({ usernameLower, host }, option);
const acctLower = `${usernameLower}@${hostAscii}`;
if (user === null) {
if (user == null) {
const self = await resolveSelf(acctLower);
logger.succ(`return new remote user: ${chalk.magenta(acctLower)}`);
@ -54,13 +55,11 @@ export default async (username: string, _host: string, option?: any, resync?: bo
throw new Error(`Invalied uri`);
}
await User.update({
await Users.update({
usernameLower,
host: host
}, {
$set: {
uri: self.href
}
uri: self.href
});
} else {
logger.info(`uri is fine: ${acctLower}`);
@ -69,7 +68,7 @@ export default async (username: string, _host: string, option?: any, resync?: bo
await updatePerson(self.href);
logger.info(`return resynced remote user: ${acctLower}`);
return await User.findOne({ uri: self.href });
return await Users.findOne({ uri: self.href });
}
logger.info(`return existing remote user: ${acctLower}`);