This commit is contained in:
MeiMei 2019-09-09 22:46:45 +09:00 committed by syuilo
parent 9b91b92bca
commit 827c378ac1
6 changed files with 100 additions and 60 deletions

View file

@ -199,6 +199,7 @@
"recaptcha-promise": "0.1.3", "recaptcha-promise": "0.1.3",
"reconnecting-websocket": "4.2.0", "reconnecting-websocket": "4.2.0",
"redis": "2.8.0", "redis": "2.8.0",
"redis-lock": "0.1.4",
"reflect-metadata": "0.1.13", "reflect-metadata": "0.1.13",
"rename": "1.0.4", "rename": "1.0.4",
"request": "2.88.0", "request": "2.88.0",

22
src/misc/app-lock.ts Normal file
View file

@ -0,0 +1,22 @@
import redis from '../db/redis';
import { promisify } from 'util';
/**
* Retry delay (ms) for lock acquisition
*/
const retryDelay = 100;
const lock: (key: string, timeout?: number) => Promise<() => void>
= redis
? promisify(require('redis-lock')(redis, retryDelay))
: async () => () => { };
/**
* Get AP Object lock
* @param uri AP object ID
* @param timeout Lock timeout (ms), The timeout releases previous lock.
* @returns Unlock function
*/
export function getApLock(uri: string, timeout = 30 * 1000) {
return lock(`ap-object:${uri}`, timeout);
}

View file

@ -7,6 +7,7 @@ import { resolvePerson } from '../../models/person';
import { apLogger } from '../../logger'; import { apLogger } from '../../logger';
import { extractDbHost } from '../../../../misc/convert-host'; import { extractDbHost } from '../../../../misc/convert-host';
import { fetchMeta } from '../../../../misc/fetch-meta'; import { fetchMeta } from '../../../../misc/fetch-meta';
import { getApLock } from '../../../../misc/app-lock';
const logger = apLogger; const logger = apLogger;
@ -25,47 +26,53 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
const meta = await fetchMeta(); const meta = await fetchMeta();
if (meta.blockedHosts.includes(extractDbHost(uri))) return; if (meta.blockedHosts.includes(extractDbHost(uri))) return;
// 既に同じURIを持つものが登録されていないかチェック const unlock = await getApLock(uri);
const exist = await fetchNote(uri);
if (exist) {
return;
}
// Announce対象をresolve
let renote;
try { try {
renote = await resolveNote(note); // 既に同じURIを持つものが登録されていないかチェック
} catch (e) { const exist = await fetchNote(uri);
// 対象が4xxならスキップ if (exist) {
if (e.statusCode >= 400 && e.statusCode < 500) {
logger.warn(`Ignored announce target ${note.inReplyTo} - ${e.statusCode}`);
return; return;
} }
logger.warn(`Error in announce target ${note.inReplyTo} - ${e.statusCode || e}`);
throw e; // Announce対象をresolve
let renote;
try {
renote = await resolveNote(note);
} catch (e) {
// 対象が4xxならスキップ
if (e.statusCode >= 400 && e.statusCode < 500) {
logger.warn(`Ignored announce target ${note.inReplyTo} - ${e.statusCode}`);
return;
}
logger.warn(`Error in announce target ${note.inReplyTo} - ${e.statusCode || e}`);
throw e;
}
logger.info(`Creating the (Re)Note: ${uri}`);
//#region Visibility
const to = getApIds(activity.to);
const cc = getApIds(activity.cc);
const visibility = getVisibility(to, cc, actor);
let visibleUsers: User[] = [];
if (visibility == 'specified') {
visibleUsers = await Promise.all(to.map(uri => resolvePerson(uri)));
}
//#endergion
await post(actor, {
createdAt: activity.published ? new Date(activity.published) : null,
renote,
visibility,
visibleUsers,
uri
});
} finally {
unlock();
} }
logger.info(`Creating the (Re)Note: ${uri}`);
//#region Visibility
const to = getApIds(activity.to);
const cc = getApIds(activity.cc);
const visibility = getVisibility(to, cc, actor);
let visibleUsers: User[] = [];
if (visibility == 'specified') {
visibleUsers = await Promise.all(to.map(uri => resolvePerson(uri)));
}
//#endergion
await post(actor, {
createdAt: activity.published ? new Date(activity.published) : null,
renote,
visibility,
visibleUsers,
uri
});
} }
type visibility = 'public' | 'home' | 'followers' | 'specified'; type visibility = 'public' | 'home' | 'followers' | 'specified';

View file

@ -1,13 +1,23 @@
import Resolver from '../../resolver'; import Resolver from '../../resolver';
import { IRemoteUser } from '../../../../models/entities/user'; import { IRemoteUser } from '../../../../models/entities/user';
import { createNote, fetchNote } from '../../models/note'; import { createNote, fetchNote } from '../../models/note';
import { getApId } from '../../type';
import { getApLock } from '../../../../misc/app-lock';
/** /**
* 稿 * 稿
*/ */
export default async function(resolver: Resolver, actor: IRemoteUser, note: any, silent = false): Promise<void> { export default async function(resolver: Resolver, actor: IRemoteUser, note: any, silent = false): Promise<void> {
const exist = await fetchNote(note); const uri = getApId(note);
if (exist == null) {
await createNote(note); const unlock = await getApLock(uri);
try {
const exist = await fetchNote(note);
if (exist == null) {
await createNote(note);
}
} finally {
unlock();
} }
} }

View file

@ -22,6 +22,7 @@ import { Emoji } from '../../../models/entities/emoji';
import { genId } from '../../../misc/gen-id'; import { genId } from '../../../misc/gen-id';
import { fetchMeta } from '../../../misc/fetch-meta'; import { fetchMeta } from '../../../misc/fetch-meta';
import { ensure } from '../../../prelude/ensure'; import { ensure } from '../../../prelude/ensure';
import { getApLock } from '../../../misc/app-lock';
const logger = apLogger; const logger = apLogger;
@ -257,30 +258,24 @@ export async function resolveNote(value: string | IObject, resolver?: Resolver):
const meta = await fetchMeta(); const meta = await fetchMeta();
if (meta.blockedHosts.includes(extractDbHost(uri))) throw { statusCode: 451 }; if (meta.blockedHosts.includes(extractDbHost(uri))) throw { statusCode: 451 };
//#region このサーバーに既に登録されていたらそれを返す const unlock = await getApLock(uri);
const exist = await fetchNote(uri);
if (exist) { try {
return exist; //#region このサーバーに既に登録されていたらそれを返す
} const exist = await fetchNote(uri);
//#endregion
// リモートサーバーからフェッチしてきて登録 if (exist) {
// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにートが生成されるが return exist;
// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
return await createNote(uri, resolver, true).catch(e => {
if (e.name === 'duplicated') {
return fetchNote(uri).then(note => {
if (note == null) {
throw new Error('something happened');
} else {
return note;
}
});
} else {
throw e;
} }
}); //#endregion
// リモートサーバーからフェッチしてきて登録
// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにートが生成されるが
// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
return await createNote(uri, resolver, true);
} finally {
unlock();
}
} }
export async function extractEmojis(tags: ITag[], host: string): Promise<Emoji[]> { export async function extractEmojis(tags: ITag[], host: string): Promise<Emoji[]> {

View file

@ -9561,6 +9561,11 @@ redis-errors@^1.0.0, redis-errors@^1.2.0:
resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad"
integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60= integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=
redis-lock@0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/redis-lock/-/redis-lock-0.1.4.tgz#e83590bee22b5f01cdb65bfbd88d988045356272"
integrity sha512-7/+zu86XVQfJVx1nHTzux5reglDiyUCDwmW7TSlvVezfhH2YLc/Rc8NE0ejQG+8/0lwKzm29/u/4+ogKeLosiA==
redis-parser@^2.6.0: redis-parser@^2.6.0:
version "2.6.0" version "2.6.0"
resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-2.6.0.tgz#52ed09dacac108f1a631c07e9b69941e7a19504b" resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-2.6.0.tgz#52ed09dacac108f1a631c07e9b69941e7a19504b"