diff options
| author | Akihiko Odaki <nekomanma@pixiv.co.jp> | 2018-03-27 16:51:12 +0900 |
|---|---|---|
| committer | Akihiko Odaki <nekomanma@pixiv.co.jp> | 2018-03-27 23:51:21 +0900 |
| commit | 68ce6d574882c1badbb4a3d2772451534014dd01 (patch) | |
| tree | 3b468556c25dd5b63e3774aca1869b71dd9b1919 /src/api/endpoints/users | |
| parent | Merge pull request #1316 from akihikodaki/host (diff) | |
| download | sharkey-68ce6d574882c1badbb4a3d2772451534014dd01.tar.gz sharkey-68ce6d574882c1badbb4a3d2772451534014dd01.tar.bz2 sharkey-68ce6d574882c1badbb4a3d2772451534014dd01.zip | |
Implement remote account resolution
Diffstat (limited to 'src/api/endpoints/users')
| -rw-r--r-- | src/api/endpoints/users/posts.ts | 13 | ||||
| -rw-r--r-- | src/api/endpoints/users/recommendation.ts | 12 | ||||
| -rw-r--r-- | src/api/endpoints/users/show.ts | 189 |
3 files changed, 196 insertions, 18 deletions
diff --git a/src/api/endpoints/users/posts.ts b/src/api/endpoints/users/posts.ts index 0c8bceee3d..3c84bf0d80 100644 --- a/src/api/endpoints/users/posts.ts +++ b/src/api/endpoints/users/posts.ts @@ -2,6 +2,7 @@ * Module dependencies */ import $ from 'cafy'; +import getHostLower from '../../common/get-host-lower'; import Post, { pack } from '../../models/post'; import User from '../../models/user'; @@ -22,7 +23,15 @@ module.exports = (params, me) => new Promise(async (res, rej) => { if (usernameErr) return rej('invalid username param'); if (userId === undefined && username === undefined) { - return rej('user_id or username is required'); + return rej('user_id or pair of username and host is required'); + } + + // Get 'host' parameter + const [host, hostErr] = $(params.host).optional.string().$; + if (hostErr) return rej('invalid host param'); + + if (userId === undefined && host === undefined) { + return rej('user_id or pair of username and host is required'); } // Get 'include_replies' parameter @@ -60,7 +69,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { const q = userId !== undefined ? { _id: userId } - : { username_lower: username.toLowerCase() } ; + : { username_lower: username.toLowerCase(), host_lower: getHostLower(host) } ; // Lookup user const user = await User.findOne(q, { diff --git a/src/api/endpoints/users/recommendation.ts b/src/api/endpoints/users/recommendation.ts index f1f5bcd0ac..45d90f422b 100644 --- a/src/api/endpoints/users/recommendation.ts +++ b/src/api/endpoints/users/recommendation.ts @@ -30,9 +30,15 @@ module.exports = (params, me) => new Promise(async (res, rej) => { _id: { $nin: followingIds }, - 'account.last_used_at': { - $gte: new Date(Date.now() - ms('7days')) - } + $or: [ + { + 'account.last_used_at': { + $gte: new Date(Date.now() - ms('7days')) + } + }, { + host: { $not: null } + } + ] }, { limit: limit, skip: offset, diff --git a/src/api/endpoints/users/show.ts b/src/api/endpoints/users/show.ts index 7aea59296a..78df23f339 100644 --- a/src/api/endpoints/users/show.ts +++ b/src/api/endpoints/users/show.ts @@ -2,7 +2,49 @@ * Module dependencies */ import $ from 'cafy'; -import User, { pack } from '../../models/user'; +import { JSDOM } from 'jsdom'; +import { toUnicode, toASCII } from 'punycode'; +import uploadFromUrl from '../../common/drive/upload_from_url'; +import User, { pack, validateUsername, isValidName, isValidDescription } from '../../models/user'; +const request = require('request-promise-native'); +const WebFinger = require('webfinger.js'); + +const webFinger = new WebFinger({}); + +async function getCollectionCount(url) { + if (!url) { + return null; + } + + try { + const collection = await request({ url, json: true }); + return collection ? collection.totalItems : null; + } catch (exception) { + return null; + } +} + +function findUser(q) { + return User.findOne(q, { + fields: { + data: false + } + }); +} + +function webFingerAndVerify(query, verifier) { + return new Promise((res, rej) => webFinger.lookup(query, (error, result) => { + if (error) { + return rej(error); + } + + if (result.object.subject.toLowerCase().replace(/^acct:/, '') !== verifier) { + return rej('WebFinger verfification failed'); + } + + res(result.object); + })); +} /** * Show a user @@ -12,6 +54,8 @@ import User, { pack } from '../../models/user'; * @return {Promise<any>} */ module.exports = (params, me) => new Promise(async (res, rej) => { + let user; + // Get 'user_id' parameter const [userId, userIdErr] = $(params.user_id).optional.id().$; if (userIdErr) return rej('invalid user_id param'); @@ -20,23 +64,142 @@ module.exports = (params, me) => new Promise(async (res, rej) => { const [username, usernameErr] = $(params.username).optional.string().$; if (usernameErr) return rej('invalid username param'); - if (userId === undefined && username === undefined) { - return rej('user_id or username is required'); - } + // Get 'host' parameter + const [host, hostErr] = $(params.host).optional.string().$; + if (hostErr) return rej('invalid username param'); - const q = userId !== undefined - ? { _id: userId } - : { username_lower: username.toLowerCase() }; + if (userId === undefined && typeof username !== 'string') { + return rej('user_id or pair of username and host is required'); + } // Lookup user - const user = await User.findOne(q, { - fields: { - data: false + if (typeof host === 'string') { + const username_lower = username.toLowerCase(); + const host_lower_ascii = toASCII(host).toLowerCase(); + const host_lower = toUnicode(host_lower_ascii); + + user = await findUser({ username_lower, host_lower }); + + if (user === null) { + const acct_lower = `${username_lower}@${host_lower_ascii}`; + let activityStreams; + let finger; + let followers_count; + let following_count; + let likes_count; + let posts_count; + + if (!validateUsername(username)) { + return rej('username validation failed'); + } + + try { + finger = await webFingerAndVerify(acct_lower, acct_lower); + } catch (exception) { + return rej('WebFinger lookup failed'); + } + + const self = finger.links.find(link => link.rel && link.rel.toLowerCase() === 'self'); + if (!self) { + return rej('WebFinger has no reference to self representation'); + } + + try { + activityStreams = await request({ + url: self.href, + headers: { + Accept: 'application/activity+json, application/ld+json' + }, + json: true + }); + } catch (exception) { + return rej('failed to retrieve ActivityStreams representation'); + } + + if (!(activityStreams && + (Array.isArray(activityStreams['@context']) ? + activityStreams['@context'].includes('https://www.w3.org/ns/activitystreams') : + activityStreams['@context'] === 'https://www.w3.org/ns/activitystreams') && + activityStreams.type === 'Person' && + typeof activityStreams.preferredUsername === 'string' && + activityStreams.preferredUsername.toLowerCase() === username_lower && + isValidName(activityStreams.name) && + isValidDescription(activityStreams.summary) + )) { + return rej('failed ActivityStreams validation'); + } + + try { + [followers_count, following_count, likes_count, posts_count] = await Promise.all([ + getCollectionCount(activityStreams.followers), + getCollectionCount(activityStreams.following), + getCollectionCount(activityStreams.liked), + getCollectionCount(activityStreams.outbox), + webFingerAndVerify(activityStreams.id, acct_lower), + ]); + } catch (exception) { + return rej('failed to fetch assets'); + } + + const summaryDOM = JSDOM.fragment(activityStreams.summary); + + // Create user + user = await User.insert({ + avatar_id: null, + banner_id: null, + created_at: new Date(), + description: summaryDOM.textContent, + followers_count, + following_count, + name: activityStreams.name, + posts_count, + likes_count, + liked_count: 0, + drive_capacity: 1073741824, // 1GB + username: username, + username_lower, + host: toUnicode(finger.subject.replace(/^.*?@/, '')), + host_lower, + account: { + uri: activityStreams.id, + }, + }); + + const [icon, image] = await Promise.all([ + activityStreams.icon, + activityStreams.image, + ].map(async image => { + if (!image || image.type !== 'Image') { + return { _id: null }; + } + + try { + return await uploadFromUrl(image.url, user); + } catch (exception) { + return { _id: null }; + } + })); + + User.update({ _id: user._id }, { + $set: { + avatar_id: icon._id, + banner_id: image._id, + }, + }); + + user.avatar_id = icon._id; + user.banner_id = icon._id; } - }); + } else { + const q = userId !== undefined + ? { _id: userId } + : { username_lower: username.toLowerCase(), host: null }; - if (user === null) { - return rej('user not found'); + user = await findUser(q); + + if (user === null) { + return rej('user not found'); + } } // Send response |