mirror of
https://github.com/byulmaru/quesdon
synced 2024-11-30 15:58:01 +09:00
Misskey API 호환 코드 추가
This commit is contained in:
parent
cdd6f1661a
commit
9132850b16
@ -3,10 +3,13 @@ import Router from 'koa-router';
|
||||
import fetch from 'node-fetch';
|
||||
import parseLinkHeader, { Link, Links } from 'parse-link-header';
|
||||
import { Account } from 'megalodon';
|
||||
import { User as MisskeyUser } from '../../utils/misskey_entities/user';
|
||||
import { Following } from '../../utils/misskey_entities/following';
|
||||
import { oneLineTrim, stripIndents } from 'common-tags';
|
||||
import { QUESTION_TEXT_MAX_LENGTH } from '../../../common/const';
|
||||
import { BASE_URL, NOTICE_ACCESS_TOKEN, PUSHBULLET_CLIENT_ID, PUSHBULLET_CLIENT_SECRET } from '../../config';
|
||||
import { Question, User } from '../../db/index';
|
||||
import detectInstance from '../../utils/detectInstance';
|
||||
|
||||
const router = new Router();
|
||||
|
||||
@ -24,8 +27,6 @@ router.get('/verify_credentials', async (ctx: Koa.ParameterizedContext): Promise
|
||||
|
||||
router.get('/followers', async (ctx: Koa.ParameterizedContext): Promise<never|void|{}> =>
|
||||
{
|
||||
if (null === /^\d+$/.exec(ctx.query.max_id || '0'))
|
||||
return ctx.throw('max_id is num only', 400);
|
||||
if (!ctx.session.user)
|
||||
return ctx.throw('please login', 403);
|
||||
|
||||
@ -37,10 +38,49 @@ router.get('/followers', async (ctx: Koa.ParameterizedContext): Promise<never|vo
|
||||
if (user.hostName === 'twitter.com')
|
||||
return ctx.body = { max_id: undefined, accounts: [] };
|
||||
|
||||
// TODO: add logic for misskey
|
||||
// mastodon
|
||||
const instanceUrl = 'https://' + user.acct.split('@')[1];
|
||||
const myInfo = await fetch(instanceUrl + '/api/v1/accounts/verify_credentials',
|
||||
|
||||
const instanceType = await detectInstance(instanceUrl);
|
||||
|
||||
if (instanceType === 'misskey')
|
||||
{
|
||||
// misskey
|
||||
const fetchOptions =
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
};
|
||||
|
||||
const myInfo: MisskeyUser = await fetch(`${instanceUrl}/api/i`,
|
||||
Object.assign({}, fetchOptions,
|
||||
{
|
||||
body: JSON.stringify( { i: user.accessToken })
|
||||
})).then(r => r.json());
|
||||
const followersRaw: Following[] = await fetch(`${instanceUrl}/api/users/followers`,
|
||||
Object.assign({}, fetchOptions,
|
||||
{
|
||||
body:
|
||||
`{
|
||||
"i": "${user.accessToken}",
|
||||
"userId": "${myInfo.id}",
|
||||
"limit": 80
|
||||
${ctx.query.max_id ? ',"untilId": "' + ctx.query.max_id + '"' : ''}
|
||||
}`
|
||||
})).then(r => r.json());
|
||||
const followers = followersRaw
|
||||
.map(follower => `${follower.follower?.username}@${follower.follower?.host ?? user.acct.split('@')[1]}`.toLowerCase());
|
||||
console.log(followers);
|
||||
const followersObject = await User.find({acctLower: {$in: followers}});
|
||||
const max_id = (followersRaw[followersRaw.length - 1] ?? { id: '' }).id;
|
||||
return ctx.body =
|
||||
{
|
||||
accounts: followersObject,
|
||||
max_id
|
||||
};
|
||||
}
|
||||
|
||||
// mastodon
|
||||
const myInfo = await fetch(`${instanceUrl}/api/v1/accounts/verify_credentials`,
|
||||
{
|
||||
headers: { Authorization: 'Bearer ' + user.accessToken }
|
||||
}).then((r) => r.json());
|
||||
@ -57,7 +97,7 @@ router.get('/followers', async (ctx: Koa.ParameterizedContext): Promise<never|vo
|
||||
|
||||
const followersObject = await User.find({acctLower: {$in: followers}});
|
||||
const max_id = ((parseLinkHeader(followersRes.headers.get('Link') ?? '') || {} as Links).next || {} as Link).max_id;
|
||||
ctx.body =
|
||||
return ctx.body =
|
||||
{
|
||||
accounts: followersObject,
|
||||
max_id
|
||||
@ -185,7 +225,7 @@ router.post('/:acct/question', async (ctx: Koa.ParameterizedContext): Promise<ne
|
||||
if (!ctx.session.user)
|
||||
return ctx.throw('please login', 403);
|
||||
|
||||
const questionUser = await User.findById(ctx.sessions.user);
|
||||
const questionUser = await User.findById(ctx.session.user);
|
||||
if (!questionUser)
|
||||
return ctx.throw('not found', 404);
|
||||
|
||||
|
@ -2,12 +2,16 @@ import Koa from 'koa';
|
||||
import Router from 'koa-router';
|
||||
import fetch from 'node-fetch';
|
||||
import rndstr from 'rndstr';
|
||||
import crypto from 'crypto';
|
||||
import { URL } from 'url';
|
||||
import { BASE_URL } from '../../config';
|
||||
import { MastodonApp, User } from '../../db/index';
|
||||
import QueryStringUtils from '../../utils/queryString';
|
||||
import { requestOAuth } from '../../utils/requestOAuth';
|
||||
import twitterClient from '../../utils/twitterClient';
|
||||
import detectInstance from '../../utils/detectInstance';
|
||||
import { App } from '../../utils/misskey_entities/app';
|
||||
import { User as MisskeyUser } from '../../utils/misskey_entities/user';
|
||||
|
||||
const router = new Router();
|
||||
|
||||
@ -20,47 +24,7 @@ router.post('/get_url', async (ctx: Koa.ParameterizedContext): Promise<never|voi
|
||||
const redirectUri = BASE_URL + '/api/web/oauth/redirect';
|
||||
let url = '';
|
||||
|
||||
if (hostName !== 'twitter.com')
|
||||
{
|
||||
// Mastodon
|
||||
// TODO: misskey support
|
||||
let app = await MastodonApp.findOne( { hostName, appBaseUrl: BASE_URL, redirectUri } );
|
||||
if (!app)
|
||||
{
|
||||
const res = await fetch('https://' + hostName + '/api/v1/apps',
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(
|
||||
{
|
||||
client_name: 'Quesdon',
|
||||
redirect_uris: redirectUri,
|
||||
scopes: 'read write',
|
||||
website: BASE_URL
|
||||
}),
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}).then((r) => r.json());
|
||||
|
||||
app = new MastodonApp();
|
||||
app.clientId = res.client_id;
|
||||
app.clientSecret = res.client_secret;
|
||||
app.hostName = hostName;
|
||||
app.appBaseUrl = BASE_URL;
|
||||
app.redirectUri = redirectUri;
|
||||
|
||||
await app.save();
|
||||
}
|
||||
ctx.session.loginState = `${rndstr()}_${app.id}`;
|
||||
const params: {[key: string]: string} =
|
||||
{
|
||||
client_id: app.clientId,
|
||||
scope: 'read+write',
|
||||
redirect_uri: redirectUri,
|
||||
response_type: 'code',
|
||||
state: ctx.session.loginState
|
||||
};
|
||||
url = `https://${app.hostName}/oauth/authorize?${Object.entries(params).map((v) => v.join('=')).join('&')}`;
|
||||
}
|
||||
else // Twitter
|
||||
if (hostName === 'twitter.com')
|
||||
{
|
||||
ctx.session.loginState = 'twitter';
|
||||
const { TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET } = process.env;
|
||||
@ -80,6 +44,85 @@ router.post('/get_url', async (ctx: Koa.ParameterizedContext): Promise<never|voi
|
||||
ctx.session.twitterOAuth = requestToken;
|
||||
url = `https://twitter.com/oauth/authenticate?oauth_token=${requestToken.token}`;
|
||||
}
|
||||
else
|
||||
{
|
||||
const instanceType = await detectInstance(`https://${hostName}`);
|
||||
if (instanceType === 'misskey')
|
||||
{
|
||||
let app = await MastodonApp.findOne( { hostName, appBaseUrl: BASE_URL, redirectUri } );
|
||||
if (!app) // if it's the first time user from this instance is using quesdon
|
||||
{
|
||||
const res: App = await fetch(`https://${hostName}/api/app/create`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(
|
||||
{
|
||||
name: 'Quesdon',
|
||||
description: BASE_URL,
|
||||
permission: ['read:following', 'write:notes'],
|
||||
callbackUrl: redirectUri
|
||||
}),
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}).then(r => r.json());
|
||||
|
||||
app = new MastodonApp();
|
||||
app.clientId = res.id,
|
||||
app.clientSecret = res.secret as string;
|
||||
app.hostName = hostName;
|
||||
app.appBaseUrl = BASE_URL;
|
||||
app.redirectUri = redirectUri;
|
||||
|
||||
await app.save();
|
||||
}
|
||||
ctx.session.loginState = `misskey_${app.id}`;
|
||||
const res = await fetch(`https://${hostName}/api/auth/session/generate`, // get authentication url from misskey instance
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify( { appSecret: app.clientSecret } )
|
||||
}).then(r => r.json());
|
||||
url = res.url;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Mastodon
|
||||
let app = await MastodonApp.findOne( { hostName, appBaseUrl: BASE_URL, redirectUri } );
|
||||
if (!app)
|
||||
{
|
||||
const res = await fetch('https://' + hostName + '/api/v1/apps',
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(
|
||||
{
|
||||
client_name: 'Quesdon',
|
||||
redirect_uris: redirectUri,
|
||||
scopes: 'read write',
|
||||
website: BASE_URL
|
||||
}),
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}).then((r) => r.json());
|
||||
|
||||
app = new MastodonApp();
|
||||
app.clientId = res.client_id;
|
||||
app.clientSecret = res.client_secret;
|
||||
app.hostName = hostName;
|
||||
app.appBaseUrl = BASE_URL;
|
||||
app.redirectUri = redirectUri;
|
||||
|
||||
await app.save();
|
||||
}
|
||||
ctx.session.loginState = `${rndstr()}_${app.id}`;
|
||||
const params: {[key: string]: string} =
|
||||
{
|
||||
client_id: app.clientId,
|
||||
scope: 'read+write',
|
||||
redirect_uri: redirectUri,
|
||||
response_type: 'code',
|
||||
state: ctx.session.loginState
|
||||
};
|
||||
url = `https://${app.hostName}/oauth/authorize?${Object.entries(params).map((v) => v.join('=')).join('&')}`;
|
||||
}
|
||||
}
|
||||
ctx.body = { url };
|
||||
});
|
||||
|
||||
@ -96,11 +139,40 @@ router.get('/redirect', async (ctx: Koa.ParameterizedContext) =>
|
||||
url: string;
|
||||
acct: string;
|
||||
};
|
||||
|
||||
if (ctx.session.loginState !== 'twitter')
|
||||
|
||||
if ((ctx.session.loginState as string).startsWith('misskey'))
|
||||
{
|
||||
// misskey
|
||||
const app = await MastodonApp.findById(ctx.session.loginState.split('_')[1]);
|
||||
if (app === null)
|
||||
return ctx.redirect('/login?error=app_notfound');
|
||||
|
||||
const res: { accessToken: string; user: MisskeyUser } = await fetch(`https://${app.hostName}/api/auth/session/userkey`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(
|
||||
{
|
||||
appSecret: app.clientSecret,
|
||||
token: ctx.query.token
|
||||
})
|
||||
}).then(r => r.json());
|
||||
|
||||
profile =
|
||||
{
|
||||
id: res.user.id,
|
||||
name: res.user.name ?? res.user.username,
|
||||
screenName: res.user.username,
|
||||
hostName: app.hostName,
|
||||
avatarUrl: res.user.avatarUrl as string,
|
||||
accessToken: crypto.createHash('sha256').update(res.accessToken + app.clientSecret).digest('hex'),
|
||||
url: `https://${res.user.host}/@${res.user.username}`,
|
||||
acct: `${res.user.username}@${app.hostName}`
|
||||
};
|
||||
}
|
||||
else if (ctx.session.loginState !== 'twitter')
|
||||
{
|
||||
// Mastodon
|
||||
// TODO: handle misskey
|
||||
if (ctx.query.state !== ctx.session.loginState)
|
||||
return ctx.redirect('/login?error=invalid_state');
|
||||
|
||||
@ -209,8 +281,7 @@ router.get('/redirect', async (ctx: Koa.ParameterizedContext) =>
|
||||
let user;
|
||||
if (profile.hostName !== 'twitter.com')
|
||||
{
|
||||
// Mastodon
|
||||
// TODO: misskey
|
||||
// Mastodon and misskey
|
||||
user = await User.findOne({acctLower: acct.toLowerCase()});
|
||||
}
|
||||
else
|
||||
|
@ -8,6 +8,7 @@ import { IMastodonApp, IUser, Question, QuestionLike, User } from '../../db/inde
|
||||
import { cutText } from '../../utils/cutText';
|
||||
import { requestOAuth } from '../../utils/requestOAuth';
|
||||
import twitterClient from '../../utils/twitterClient';
|
||||
import detectInstance from '../../utils/detectInstance';
|
||||
|
||||
const router = new Router();
|
||||
|
||||
@ -52,7 +53,7 @@ router.get('/latest', async (ctx) =>
|
||||
ctx.body = questions;
|
||||
});
|
||||
|
||||
router.post('/:id/answer', async (ctx: Koa.ParameterizedContext): Promise<never|void> =>
|
||||
router.post('/:id/answer', async (ctx: Koa.ParameterizedContext): Promise<void|never> =>
|
||||
{
|
||||
if (!ctx.session.user)
|
||||
return ctx.throw('please login', 403);
|
||||
@ -87,57 +88,104 @@ router.post('/:id/answer', async (ctx: Koa.ParameterizedContext): Promise<never|
|
||||
const isTwitter = user.hostName === 'twitter.com';
|
||||
const answerCharMax = isTwitter ? (110 - question.question.length) : 200;
|
||||
const answerUrl = `${BASE_URL}/@${user.acct}/questions/${question.id}`;
|
||||
if (!isTwitter)
|
||||
|
||||
if (isTwitter)
|
||||
{
|
||||
// Mastodon
|
||||
// TODO: misskey
|
||||
const body =
|
||||
{
|
||||
spoiler_text: `Q. ${question.question} #quesdon`,
|
||||
status: `A. ${question.answer.length > 200 ? `${question.answer.substring(0, 200)}...` : question.answer}
|
||||
#quesdon ${answerUrl}`,
|
||||
visibility: ctx.request.body.visibility
|
||||
};
|
||||
if (question.questionUser)
|
||||
{
|
||||
let questionUserAcct = `@${question.questionUser.acct}`;
|
||||
if (question.questionUser.hostName === 'twitter.com')
|
||||
questionUserAcct = `https://twitter.com/${question.questionUser.acct.replace(/:.+/, '')}`;
|
||||
body.status = stripIndents`질문자: ${questionUserAcct}
|
||||
${body.status}`;
|
||||
}
|
||||
if (question.isNSFW)
|
||||
{
|
||||
body.status = `Q. ${question.question}
|
||||
${body.status}`;
|
||||
body.spoiler_text = '⚠ 이 질문은 답변자가 NSFW하다고 했어요. #quesdon';
|
||||
}
|
||||
fetch('https://' + user.acct.split('@')[1] + '/api/v1/statuses',
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
headers:
|
||||
{
|
||||
'Authorization': 'Bearer ' + user.accessToken,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
const strQ = cutText(question.question, 60);
|
||||
const strA = cutText(question.answer, 120 - strQ.length);
|
||||
const [key, secret] = user.accessToken.split(':');
|
||||
const body = `Q. ${strQ}
|
||||
A. ${strA}
|
||||
#quesdon ${answerUrl}`;
|
||||
requestOAuth(twitterClient,
|
||||
await requestOAuth(twitterClient,
|
||||
{
|
||||
url: 'https://api.twitter.com/1.1/statuses/update.json',
|
||||
method: 'POST',
|
||||
data: { status: body }
|
||||
}, { key, secret });
|
||||
return;
|
||||
}
|
||||
|
||||
// misskey
|
||||
const instanceUrl = 'https://' + user.acct.split('@')[1];
|
||||
const instanceType = await detectInstance(instanceUrl);
|
||||
|
||||
if (instanceType === 'misskey')
|
||||
{
|
||||
let visibility;
|
||||
switch(ctx.request.body.visibility)
|
||||
{
|
||||
case 'public': visibility = 'public'; break;
|
||||
case 'unlisted': visibility = 'home'; break;
|
||||
case 'private': visibility = 'followers'; break;
|
||||
default: visibility = 'home'; break;
|
||||
}
|
||||
|
||||
const body =
|
||||
{
|
||||
i: user.accessToken,
|
||||
visibility: visibility,
|
||||
cw: `Q. ${question.question} #quesdon`,
|
||||
text: stripIndents`A. ${question.answer.length > 200 ? `${question.answer.substring(0, 200)}...` : question.answer}
|
||||
#quesdon ${answerUrl}`
|
||||
};
|
||||
if (question.questionUser)
|
||||
{
|
||||
let questionUserAcct = `@${question.questionUser.acct}`;
|
||||
if (question.questionUser.hostName === 'twitter.com')
|
||||
questionUserAcct = `https://twitter.com/${question.questionUser.acct.replace(/:.+/, '')}`;
|
||||
body.text = stripIndents`질문자: ${questionUserAcct}
|
||||
${body.text}`;
|
||||
}
|
||||
if (question.isNSFW)
|
||||
{
|
||||
body.text = stripIndents`Q. ${question.question}
|
||||
${body.text}`;
|
||||
body.cw = '⚠ 이 질문은 답변자가 NSFW하다고 했어요. #quesdon';
|
||||
}
|
||||
|
||||
await fetch(`${instanceUrl}/api/notes/create`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Mastodon
|
||||
const body =
|
||||
{
|
||||
spoiler_text: `Q. ${question.question} #quesdon`,
|
||||
status: stripIndents`A. ${question.answer.length > 200 ? `${question.answer.substring(0, 200)}...` : question.answer}
|
||||
#quesdon ${answerUrl}`,
|
||||
visibility: ctx.request.body.visibility
|
||||
};
|
||||
if (question.questionUser)
|
||||
{
|
||||
let questionUserAcct = `@${question.questionUser.acct}`;
|
||||
if (question.questionUser.hostName === 'twitter.com')
|
||||
questionUserAcct = `https://twitter.com/${question.questionUser.acct.replace(/:.+/, '')}`;
|
||||
body.status = stripIndents`질문자: ${questionUserAcct}
|
||||
${body.status}`;
|
||||
}
|
||||
if (question.isNSFW)
|
||||
{
|
||||
body.status = stripIndents`Q. ${question.question}
|
||||
${body.status}`;
|
||||
body.spoiler_text = '⚠ 이 질문은 답변자가 NSFW하다고 했어요. #quesdon';
|
||||
}
|
||||
await fetch(instanceUrl + '/api/v1/statuses',
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
headers:
|
||||
{
|
||||
'Authorization': 'Bearer ' + user.accessToken,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
return;
|
||||
});
|
||||
|
||||
router.post('/:id/delete', async (ctx: Koa.ParameterizedContext): Promise<never|void> =>
|
||||
|
Loading…
Reference in New Issue
Block a user