From cb6f390fb6964a032f15c6885d686d07c945ad38 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 7 Nov 2018 13:14:52 +0900 Subject: [PATCH] =?UTF-8?q?GitHub=20/=20Twitter=E9=80=A3=E6=90=BA=E3=81=AE?= =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E3=82=92DB=E3=81=AB=E4=BF=9D=E5=AD=98?= =?UTF-8?q?=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/setup.en.md | 7 - locales/ja-JP.yml | 10 + src/client/app/admin/views/instance.vue | 40 ++ src/config/types.ts | 9 +- src/misc/fetch-meta.ts | 4 +- src/models/meta.ts | 34 ++ src/server/api/endpoints/admin/update-meta.ts | 68 ++- src/server/api/endpoints/meta.ts | 4 +- src/server/api/index.ts | 1 + src/server/api/service/github-bot.ts | 156 ++++++ src/server/api/service/github.ts | 526 +++++++----------- src/server/api/service/twitter.ts | 249 +++++---- 12 files changed, 632 insertions(+), 476 deletions(-) create mode 100644 src/server/api/service/github-bot.ts diff --git a/docs/setup.en.md b/docs/setup.en.md index 83392d0d9..3a1c7bff9 100644 --- a/docs/setup.en.md +++ b/docs/setup.en.md @@ -57,13 +57,6 @@ npm install web-push -g web-push generate-vapid-keys ``` -*(optional)* Create a twitter application ----------------------------------------------------------------- -If you want to enable the twitter integration, you need to create a twitter app at [https://developer.twitter.com/en/apply/user](https://developer.twitter.com/en/apply/user). - -In the app you need to set the oauth callback url as : https://misskey-instance/api/tw/cb - - *5.* Make configuration file ---------------------------------------------------------------- 1. `cp .config/example.yml .config/default.yml` Copy the `.config/example.yml` and rename it to `default.yml`. diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index c3a9848b7..572c8ccdf 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1095,6 +1095,16 @@ admin/views/instance.vue: enable-recaptcha: "reCAPTCHAを有効にする" recaptcha-site-key: "reCAPTCHA site key" recaptcha-secret-key: "reCAPTCHA secret key" + twitter-integration-config: "Twitter連携の設定" + twitter-integration-info: "コールバックURLは /api/tw/cb に設定します。" + enable-twitter-integration: "Twitter連携を有効にする" + twitter-integration-consumer-key: "Consumer key" + twitter-integration-consumer-secret: "Consumer secret" + github-integration-config: "GitHub連携の設定" + github-integration-info: "コールバックURLは /api/gh/cb に設定します。" + enable-github-integration: "GitHub連携を有効にする" + github-integration-client-id: "Client ID" + github-integration-client-secret: "Client secret" proxy-account-config: "プロキシアカウントの設定" proxy-account-info: "プロキシアカウントは、特定の条件下でユーザーのリモートフォローを代行するアカウントです。例えば、ユーザーがリモートユーザーをリストに入れたとき、リストに入れられたユーザーを誰もフォローしていないとアクティビティがサーバーに配達されないため、代わりにプロキシアカウントがフォローするようにします。" proxy-account-username: "プロキシアカウントのユーザー名" diff --git a/src/client/app/admin/views/instance.vue b/src/client/app/admin/views/instance.vue index 815cea631..a463bdc73 100644 --- a/src/client/app/admin/views/instance.vue +++ b/src/client/app/admin/views/instance.vue @@ -53,6 +53,28 @@

Code: {{ inviteCode }}

+ + +
%i18n:@twitter-integration-config%
+
+ %i18n:@enable-twitter-integration% + %i18n:@twitter-integration-info% + %i18n:@twitter-integration-consumer-key% + %i18n:@twitter-integration-consumer-secret% + %i18n:@save% +
+
+ + +
%i18n:@github-integration-config%
+
+ %i18n:@enable-github-integration% + %i18n:@github-integration-info% + %i18n:@github-integration-client-id% + %i18n:@github-integration-client-secret% + %i18n:@save% +
+
@@ -77,6 +99,12 @@ export default Vue.extend({ enableRecaptcha: false, recaptchaSiteKey: null, recaptchaSecretKey: null, + enableTwitterIntegration: false, + twitterConsumerKey: null, + twitterConsumerSecret: null, + enableGithubIntegration: false, + githubClientId: null, + githubClientSecret: null, proxyAccount: null, inviteCode: null, }; @@ -98,6 +126,12 @@ export default Vue.extend({ this.recaptchaSiteKey = meta.recaptchaSiteKey; this.recaptchaSecretKey = meta.recaptchaSecretKey; this.proxyAccount = meta.proxyAccount; + this.enableTwitterIntegration = meta.enableTwitterIntegration; + this.twitterConsumerKey = meta.twitterConsumerKey; + this.twitterConsumerSecret = meta.twitterConsumerSecret; + this.enableGithubIntegration = meta.enableGithubIntegration; + this.githubClientId = meta.githubClientId; + this.githubClientSecret = meta.githubClientSecret; }); }, @@ -131,6 +165,12 @@ export default Vue.extend({ recaptchaSiteKey: this.recaptchaSiteKey, recaptchaSecretKey: this.recaptchaSecretKey, proxyAccount: this.proxyAccount, + enableTwitterIntegration: this.enableTwitterIntegration, + twitterConsumerKey: this.twitterConsumerKey, + twitterConsumerSecret: this.twitterConsumerSecret, + enableGithubIntegration: this.enableGithubIntegration, + githubClientId: this.githubClientId, + githubClientSecret: this.githubClientSecret, }).then(() => { this.$swal({ type: 'success', diff --git a/src/config/types.ts b/src/config/types.ts index 98fa2660f..f9cb9d865 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -40,14 +40,7 @@ export type Source = { summalyProxy?: string; accesslog?: string; - twitter?: { - consumer_key: string; - consumer_secret: string; - }; - github?: { - client_id: string; - client_secret: string; - }; + github_bot?: { hook_secret: string; username: string; diff --git a/src/misc/fetch-meta.ts b/src/misc/fetch-meta.ts index e8ff1aca7..622ad5439 100644 --- a/src/misc/fetch-meta.ts +++ b/src/misc/fetch-meta.ts @@ -11,7 +11,9 @@ const defaultMeta: any = { originalNotesCount: 0, originalUsersCount: 0 }, - maxNoteTextLength: 1000 + maxNoteTextLength: 1000, + enableTwitterIntegration: false, + enableGithubIntegration: false, }; export default async function(): Promise { diff --git a/src/models/meta.ts b/src/models/meta.ts index f62117a0b..a12747ea3 100644 --- a/src/models/meta.ts +++ b/src/models/meta.ts @@ -99,6 +99,32 @@ if ((config as any).maintainer) { } }); } +if ((config as any).twitter) { + Meta.findOne({}).then(m => { + if (m != null && m.enableTwitterIntegration == null) { + Meta.update({}, { + $set: { + enableTwitterIntegration: true, + twitterConsumerKey: (config as any).twitter.consumer_key, + twitterConsumerSecret: (config as any).twitter.consumer_secret + } + }); + } + }); +} +if ((config as any).github) { + Meta.findOne({}).then(m => { + if (m != null && m.enableGithubIntegration == null) { + Meta.update({}, { + $set: { + enableGithubIntegration: true, + githubClientId: (config as any).github.client_id, + githubClientSecret: (config as any).github.client_secret + } + }); + } + }); +} export type IMeta = { name?: string; @@ -157,4 +183,12 @@ export type IMeta = { * Max allowed note text length in charactors */ maxNoteTextLength?: number; + + enableTwitterIntegration?: boolean; + twitterConsumerKey?: string; + twitterConsumerSecret?: string; + + enableGithubIntegration?: boolean; + githubClientId?: string; + githubClientSecret?: string; }; diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts index d45a8759f..39d7ef86b 100644 --- a/src/server/api/endpoints/admin/update-meta.ts +++ b/src/server/api/endpoints/admin/update-meta.ts @@ -137,7 +137,49 @@ export const meta = { desc: { 'ja-JP': 'インスタンスの対象言語' } - } + }, + + enableTwitterIntegration: { + validator: $.bool.optional, + desc: { + 'ja-JP': 'Twitter連携機能を有効にするか否か' + } + }, + + twitterConsumerKey: { + validator: $.str.optional.nullable, + desc: { + 'ja-JP': 'TwitterアプリのConsumer key' + } + }, + + twitterConsumerSecret: { + validator: $.str.optional.nullable, + desc: { + 'ja-JP': 'TwitterアプリのConsumer secret' + } + }, + + enableGithubIntegration: { + validator: $.bool.optional, + desc: { + 'ja-JP': 'GitHub連携機能を有効にするか否か' + } + }, + + githubClientId: { + validator: $.str.optional.nullable, + desc: { + 'ja-JP': 'GitHubアプリのClient ID' + } + }, + + githubClientSecret: { + validator: $.str.optional.nullable, + desc: { + 'ja-JP': 'GitHubアプリのClient secret' + } + }, } }; @@ -216,6 +258,30 @@ export default define(meta, (ps) => new Promise(async (res, rej) => { set.langs = ps.langs; } + if (ps.enableTwitterIntegration !== undefined) { + set.enableTwitterIntegration = ps.enableTwitterIntegration; + } + + if (ps.twitterConsumerKey !== undefined) { + set.twitterConsumerKey = ps.twitterConsumerKey; + } + + if (ps.twitterConsumerSecret !== undefined) { + set.twitterConsumerSecret = ps.twitterConsumerSecret; + } + + if (ps.enableGithubIntegration !== undefined) { + set.enableGithubIntegration = ps.enableGithubIntegration; + } + + if (ps.githubClientId !== undefined) { + set.githubClientId = ps.githubClientId; + } + + if (ps.githubClientSecret !== undefined) { + set.githubClientSecret = ps.githubClientSecret; + } + await Meta.update({}, { $set: set }, { upsert: true }); diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts index 625a9519d..7cd72d3cc 100644 --- a/src/server/api/endpoints/meta.ts +++ b/src/server/api/endpoints/meta.ts @@ -77,8 +77,8 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { elasticsearch: config.elasticsearch ? true : false, recaptcha: instance.enableRecaptcha, objectStorage: config.drive && config.drive.storage === 'minio', - twitter: config.twitter ? true : false, - github: config.github ? true : false, + twitter: instance.enableTwitterIntegration, + github: instance.enableGithubIntegration, serviceWorker: config.sw ? true : false, userRecommendation: config.user_recommendation ? config.user_recommendation : {} }; diff --git a/src/server/api/index.ts b/src/server/api/index.ts index 33e98f650..bb8bad8bb 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -44,6 +44,7 @@ router.post('/signup', require('./private/signup').default); router.post('/signin', require('./private/signin').default); router.use(require('./service/github').routes()); +router.use(require('./service/github-bot').routes()); router.use(require('./service/twitter').routes()); router.use(require('./mastodon').routes()); diff --git a/src/server/api/service/github-bot.ts b/src/server/api/service/github-bot.ts new file mode 100644 index 000000000..cb038363f --- /dev/null +++ b/src/server/api/service/github-bot.ts @@ -0,0 +1,156 @@ +import * as EventEmitter from 'events'; +import * as Router from 'koa-router'; +import * as request from 'request'; +import User, { IUser } from '../../../models/user'; +import createNote from '../../../services/note/create'; +import config from '../../../config'; +const crypto = require('crypto'); + +const handler = new EventEmitter(); + +let bot: IUser; + +const post = async (text: string, home = true) => { + if (bot == null) { + const account = await User.findOne({ + usernameLower: config.github_bot.username.toLowerCase() + }); + + if (account == null) { + console.warn(`GitHub hook bot specified, but not found: @${config.github_bot.username}`); + return; + } else { + bot = account; + } + } + + createNote(bot, { text, visibility: home ? 'home' : 'public' }); +}; + +// Init router +const router = new Router(); + +if (config.github_bot) { + const secret = config.github_bot.hook_secret; + + router.post('/hooks/github', ctx => { + const body = JSON.stringify(ctx.request.body); + const hash = crypto.createHmac('sha1', secret).update(body).digest('hex'); + const sig1 = new Buffer(ctx.headers['x-hub-signature']); + const sig2 = new Buffer(`sha1=${hash}`); + + // シグネチャ比較 + if (sig1.equals(sig2)) { + handler.emit(ctx.headers['x-github-event'], ctx.request.body); + ctx.status = 204; + } else { + ctx.status = 400; + } + }); +} + +module.exports = router; + +handler.on('status', event => { + const state = event.state; + switch (state) { + case 'error': + case 'failure': + const commit = event.commit; + const parent = commit.parents[0]; + + // Fetch parent status + request({ + url: `${parent.url}/statuses`, + proxy: config.proxy, + headers: { + 'User-Agent': 'misskey' + } + }, (err, res, body) => { + if (err) { + console.error(err); + return; + } + const parentStatuses = JSON.parse(body); + const parentState = parentStatuses[0].state; + const stillFailed = parentState == 'failure' || parentState == 'error'; + if (stillFailed) { + post(`**⚠️BUILD STILL FAILED⚠️**: ?[${commit.commit.message}](${commit.html_url})`); + } else { + post(`**🚨BUILD FAILED🚨**: →→→?[${commit.commit.message}](${commit.html_url})←←←`); + } + }); + break; + } +}); + +handler.on('push', event => { + const ref = event.ref; + switch (ref) { + case 'refs/heads/master': + const pusher = event.pusher; + const compare = event.compare; + const commits: any[] = event.commits; + post([ + `Pushed by **${pusher.name}** with ?[${commits.length} commit${commits.length > 1 ? 's' : ''}](${compare}):`, + commits.reverse().map(commit => `・[?[${commit.id.substr(0, 7)}](${commit.url})] ${commit.message.split('\n')[0]}`).join('\n'), + ].join('\n')); + break; + case 'refs/heads/release': + const commit = event.commits[0]; + post(`RELEASED: ${commit.message}`); + break; + } +}); + +handler.on('issues', event => { + const issue = event.issue; + const action = event.action; + let title: string; + switch (action) { + case 'opened': title = 'Issue opened'; break; + case 'closed': title = 'Issue closed'; break; + case 'reopened': title = 'Issue reopened'; break; + default: return; + } + post(`${title}: <${issue.number}>「${issue.title}」\n${issue.html_url}`); +}); + +handler.on('issue_comment', event => { + const issue = event.issue; + const comment = event.comment; + const action = event.action; + let text: string; + switch (action) { + case 'created': text = `Commented to「${issue.title}」:${comment.user.login}「${comment.body}」\n${comment.html_url}`; break; + default: return; + } + post(text); +}); + +handler.on('watch', event => { + const sender = event.sender; + post(`(((⭐️))) Starred by **${sender.login}** (((⭐️)))`, false); +}); + +handler.on('fork', event => { + const repo = event.forkee; + post(`🍴 Forked:\n${repo.html_url} 🍴`); +}); + +handler.on('pull_request', event => { + const pr = event.pull_request; + const action = event.action; + let text: string; + switch (action) { + case 'opened': text = `New Pull Request:「${pr.title}」\n${pr.html_url}`; break; + case 'reopened': text = `Pull Request Reopened:「${pr.title}」\n${pr.html_url}`; break; + case 'closed': + text = pr.merged + ? `Pull Request Merged!:「${pr.title}」\n${pr.html_url}` + : `Pull Request Closed:「${pr.title}」\n${pr.html_url}`; + break; + default: return; + } + post(text); +}); diff --git a/src/server/api/service/github.ts b/src/server/api/service/github.ts index 617bd7d08..4dce856c2 100644 --- a/src/server/api/service/github.ts +++ b/src/server/api/service/github.ts @@ -1,37 +1,14 @@ -import * as EventEmitter from 'events'; import * as Koa from 'koa'; import * as Router from 'koa-router'; import * as request from 'request'; import { OAuth2 } from 'oauth'; -import User, { IUser, pack, ILocalUser } from '../../../models/user'; -import createNote from '../../../services/note/create'; +import User, { pack, ILocalUser } from '../../../models/user'; import config from '../../../config'; import { publishMainStream } from '../../../stream'; import redis from '../../../db/redis'; import uuid = require('uuid'); import signin from '../common/signin'; -const crypto = require('crypto'); - -const handler = new EventEmitter(); - -let bot: IUser; - -const post = async (text: string, home = true) => { - if (bot == null) { - const account = await User.findOne({ - usernameLower: config.github_bot.username.toLowerCase() - }); - - if (account == null) { - console.warn(`GitHub hook bot specified, but not found: @${config.github_bot.username}`); - return; - } else { - bot = account; - } - } - - createNote(bot, { text, visibility: home ? 'home' : 'public' }); -}; +import fetchMeta from '../../../misc/fetch-meta'; function getUserToken(ctx: Koa.Context) { return ((ctx.headers['cookie'] || '').match(/i=(!\w+)/) || [null, null])[1]; @@ -80,337 +57,218 @@ router.get('/disconnect/github', async ctx => { })); }); -if (!config.github || !redis) { - router.get('/connect/github', ctx => { - ctx.body = '現在GitHubへ接続できません (このインスタンスではGitHubはサポートされていません)'; +async function getOath2() { + const meta = await fetchMeta(); + + if (meta.enableGithubIntegration) { + return new OAuth2( + meta.githubClientId, + meta.githubClientSecret, + 'https://github.com/', + 'login/oauth/authorize', + 'login/oauth/access_token'); + } else { + return null; + } +} + +router.get('/connect/github', async ctx => { + if (!compareOrigin(ctx)) { + ctx.throw(400, 'invalid origin'); + return; + } + + const userToken = getUserToken(ctx); + if (!userToken) { + ctx.throw(400, 'signin required'); + return; + } + + const params = { + redirect_uri: `${config.url}/api/gh/cb`, + scope: ['read:user'], + state: uuid() + }; + + redis.set(userToken, JSON.stringify(params)); + + const oauth2 = await getOath2(); + ctx.redirect(oauth2.getAuthorizeUrl(params)); +}); + +router.get('/signin/github', async ctx => { + const sessid = uuid(); + + const params = { + redirect_uri: `${config.url}/api/gh/cb`, + scope: ['read:user'], + state: uuid() + }; + + const expires = 1000 * 60 * 60; // 1h + ctx.cookies.set('signin_with_github_session_id', sessid, { + path: '/', + domain: config.host, + secure: config.url.startsWith('https'), + httpOnly: true, + expires: new Date(Date.now() + expires), + maxAge: expires }); - router.get('/signin/github', ctx => { - ctx.body = '現在GitHubへ接続できません (このインスタンスではGitHubはサポートされていません)'; - }); -} else { - const oauth2 = new OAuth2( - config.github.client_id, - config.github.client_secret, - 'https://github.com/', - 'login/oauth/authorize', - 'login/oauth/access_token'); + redis.set(sessid, JSON.stringify(params)); - router.get('/connect/github', async ctx => { - if (!compareOrigin(ctx)) { - ctx.throw(400, 'invalid origin'); + const oauth2 = await getOath2(); + ctx.redirect(oauth2.getAuthorizeUrl(params)); +}); + +router.get('/gh/cb', async ctx => { + const userToken = getUserToken(ctx); + + const oauth2 = await getOath2(); + + if (!userToken) { + const sessid = ctx.cookies.get('signin_with_github_session_id'); + + if (!sessid) { + ctx.throw(400, 'invalid session'); return; } - const userToken = getUserToken(ctx); - if (!userToken) { - ctx.throw(400, 'signin required'); + const code = ctx.query.code; + + if (!code) { + ctx.throw(400, 'invalid session'); return; } - const params = { - redirect_uri: `${config.url}/api/gh/cb`, - scope: ['read:user'], - state: uuid() - }; - - redis.set(userToken, JSON.stringify(params)); - ctx.redirect(oauth2.getAuthorizeUrl(params)); - }); - - router.get('/signin/github', async ctx => { - const sessid = uuid(); - - const params = { - redirect_uri: `${config.url}/api/gh/cb`, - scope: ['read:user'], - state: uuid() - }; - - const expires = 1000 * 60 * 60; // 1h - ctx.cookies.set('signin_with_github_session_id', sessid, { - path: '/', - domain: config.host, - secure: config.url.startsWith('https'), - httpOnly: true, - expires: new Date(Date.now() + expires), - maxAge: expires + const { redirect_uri, state } = await new Promise((res, rej) => { + redis.get(sessid, async (_, state) => { + res(JSON.parse(state)); + }); }); - redis.set(sessid, JSON.stringify(params)); - ctx.redirect(oauth2.getAuthorizeUrl(params)); - }); + if (ctx.query.state !== state) { + ctx.throw(400, 'invalid session'); + return; + } - router.get('/gh/cb', async ctx => { - const userToken = getUserToken(ctx); - - if (!userToken) { - const sessid = ctx.cookies.get('signin_with_github_session_id'); - - if (!sessid) { - ctx.throw(400, 'invalid session'); - return; - } - - const code = ctx.query.code; - - if (!code) { - ctx.throw(400, 'invalid session'); - return; - } - - const { redirect_uri, state } = await new Promise((res, rej) => { - redis.get(sessid, async (_, state) => { - res(JSON.parse(state)); - }); - }); - - if (ctx.query.state !== state) { - ctx.throw(400, 'invalid session'); - return; - } - - const { accessToken } = await new Promise((res, rej) => - oauth2.getOAuthAccessToken( - code, - { redirect_uri }, - (err, accessToken, refresh, result) => { - if (err) - rej(err); - else if (result.error) - rej(result.error); - else - res({ accessToken }); - })); - - const { login, id } = await new Promise((res, rej) => - request({ - url: 'https://api.github.com/user', - headers: { - 'Accept': 'application/vnd.github.v3+json', - 'Authorization': `bearer ${accessToken}`, - 'User-Agent': config.user_agent - } - }, (err, response, body) => { + const { accessToken } = await new Promise((res, rej) => + oauth2.getOAuthAccessToken( + code, + { redirect_uri }, + (err, accessToken, refresh, result) => { if (err) rej(err); + else if (result.error) + rej(result.error); else - res(JSON.parse(body)); + res({ accessToken }); })); - if (!login || !id) { - ctx.throw(400, 'invalid session'); - return; - } - - const user = await User.findOne({ - host: null, - 'github.id': id - }) as ILocalUser; - - if (!user) { - ctx.throw(404, `@${login}と連携しているMisskeyアカウントはありませんでした...`); - return; - } - - signin(ctx, user, true); - } else { - const code = ctx.query.code; - - if (!code) { - ctx.throw(400, 'invalid session'); - return; - } - - const { redirect_uri, state } = await new Promise((res, rej) => { - redis.get(userToken, async (_, state) => { - res(JSON.parse(state)); - }); - }); - - if (ctx.query.state !== state) { - ctx.throw(400, 'invalid session'); - return; - } - - const { accessToken } = await new Promise((res, rej) => - oauth2.getOAuthAccessToken( - code, - { redirect_uri }, - (err, accessToken, refresh, result) => { - if (err) - rej(err); - else if (result.error) - rej(result.error); - else - res({ accessToken }); - })); - - const { login, id } = await new Promise((res, rej) => - request({ - url: 'https://api.github.com/user', - headers: { - 'Accept': 'application/vnd.github.v3+json', - 'Authorization': `bearer ${accessToken}`, - 'User-Agent': config.user_agent - } - }, (err, response, body) => { - if (err) - rej(err); - else - res(JSON.parse(body)); - })); - - if (!login || !id) { - ctx.throw(400, 'invalid session'); - return; - } - - const user = await User.findOneAndUpdate({ - host: null, - token: userToken - }, { - $set: { - github: { - accessToken, - id, - login - } + const { login, id } = await new Promise((res, rej) => + request({ + url: 'https://api.github.com/user', + headers: { + 'Accept': 'application/vnd.github.v3+json', + 'Authorization': `bearer ${accessToken}`, + 'User-Agent': config.user_agent } - }); - - ctx.body = `GitHub: @${login} を、Misskey: @${user.username} に接続しました!`; - - // Publish i updated event - publishMainStream(user._id, 'meUpdated', await pack(user, user, { - detail: true, - includeSecrets: true + }, (err, response, body) => { + if (err) + rej(err); + else + res(JSON.parse(body)); })); + + if (!login || !id) { + ctx.throw(400, 'invalid session'); + return; } - }); -} -if (config.github_bot) { - const secret = config.github_bot.hook_secret; + const user = await User.findOne({ + host: null, + 'github.id': id + }) as ILocalUser; - router.post('/hooks/github', ctx => { - const body = JSON.stringify(ctx.request.body); - const hash = crypto.createHmac('sha1', secret).update(body).digest('hex'); - const sig1 = new Buffer(ctx.headers['x-hub-signature']); - const sig2 = new Buffer(`sha1=${hash}`); - - // シグネチャ比較 - if (sig1.equals(sig2)) { - handler.emit(ctx.headers['x-github-event'], ctx.request.body); - ctx.status = 204; - } else { - ctx.status = 400; + if (!user) { + ctx.throw(404, `@${login}と連携しているMisskeyアカウントはありませんでした...`); + return; } - }); -} + + signin(ctx, user, true); + } else { + const code = ctx.query.code; + + if (!code) { + ctx.throw(400, 'invalid session'); + return; + } + + const { redirect_uri, state } = await new Promise((res, rej) => { + redis.get(userToken, async (_, state) => { + res(JSON.parse(state)); + }); + }); + + if (ctx.query.state !== state) { + ctx.throw(400, 'invalid session'); + return; + } + + const { accessToken } = await new Promise((res, rej) => + oauth2.getOAuthAccessToken( + code, + { redirect_uri }, + (err, accessToken, refresh, result) => { + if (err) + rej(err); + else if (result.error) + rej(result.error); + else + res({ accessToken }); + })); + + const { login, id } = await new Promise((res, rej) => + request({ + url: 'https://api.github.com/user', + headers: { + 'Accept': 'application/vnd.github.v3+json', + 'Authorization': `bearer ${accessToken}`, + 'User-Agent': config.user_agent + } + }, (err, response, body) => { + if (err) + rej(err); + else + res(JSON.parse(body)); + })); + + if (!login || !id) { + ctx.throw(400, 'invalid session'); + return; + } + + const user = await User.findOneAndUpdate({ + host: null, + token: userToken + }, { + $set: { + github: { + accessToken, + id, + login + } + } + }); + + ctx.body = `GitHub: @${login} を、Misskey: @${user.username} に接続しました!`; + + // Publish i updated event + publishMainStream(user._id, 'meUpdated', await pack(user, user, { + detail: true, + includeSecrets: true + })); + } +}); module.exports = router; - -handler.on('status', event => { - const state = event.state; - switch (state) { - case 'error': - case 'failure': - const commit = event.commit; - const parent = commit.parents[0]; - - // Fetch parent status - request({ - url: `${parent.url}/statuses`, - proxy: config.proxy, - headers: { - 'User-Agent': 'misskey' - } - }, (err, res, body) => { - if (err) { - console.error(err); - return; - } - const parentStatuses = JSON.parse(body); - const parentState = parentStatuses[0].state; - const stillFailed = parentState == 'failure' || parentState == 'error'; - if (stillFailed) { - post(`**⚠️BUILD STILL FAILED⚠️**: ?[${commit.commit.message}](${commit.html_url})`); - } else { - post(`**🚨BUILD FAILED🚨**: →→→?[${commit.commit.message}](${commit.html_url})←←←`); - } - }); - break; - } -}); - -handler.on('push', event => { - const ref = event.ref; - switch (ref) { - case 'refs/heads/master': - const pusher = event.pusher; - const compare = event.compare; - const commits: any[] = event.commits; - post([ - `Pushed by **${pusher.name}** with ?[${commits.length} commit${commits.length > 1 ? 's' : ''}](${compare}):`, - commits.reverse().map(commit => `・[?[${commit.id.substr(0, 7)}](${commit.url})] ${commit.message.split('\n')[0]}`).join('\n'), - ].join('\n')); - break; - case 'refs/heads/release': - const commit = event.commits[0]; - post(`RELEASED: ${commit.message}`); - break; - } -}); - -handler.on('issues', event => { - const issue = event.issue; - const action = event.action; - let title: string; - switch (action) { - case 'opened': title = 'Issue opened'; break; - case 'closed': title = 'Issue closed'; break; - case 'reopened': title = 'Issue reopened'; break; - default: return; - } - post(`${title}: <${issue.number}>「${issue.title}」\n${issue.html_url}`); -}); - -handler.on('issue_comment', event => { - const issue = event.issue; - const comment = event.comment; - const action = event.action; - let text: string; - switch (action) { - case 'created': text = `Commented to「${issue.title}」:${comment.user.login}「${comment.body}」\n${comment.html_url}`; break; - default: return; - } - post(text); -}); - -handler.on('watch', event => { - const sender = event.sender; - post(`(((⭐️))) Starred by **${sender.login}** (((⭐️)))`, false); -}); - -handler.on('fork', event => { - const repo = event.forkee; - post(`🍴 Forked:\n${repo.html_url} 🍴`); -}); - -handler.on('pull_request', event => { - const pr = event.pull_request; - const action = event.action; - let text: string; - switch (action) { - case 'opened': text = `New Pull Request:「${pr.title}」\n${pr.html_url}`; break; - case 'reopened': text = `Pull Request Reopened:「${pr.title}」\n${pr.html_url}`; break; - case 'closed': - text = pr.merged - ? `Pull Request Merged!:「${pr.title}」\n${pr.html_url}` - : `Pull Request Closed:「${pr.title}」\n${pr.html_url}`; - break; - default: return; - } - post(text); -}); diff --git a/src/server/api/service/twitter.ts b/src/server/api/service/twitter.ts index 6c3cdaa13..ced3e8acc 100644 --- a/src/server/api/service/twitter.ts +++ b/src/server/api/service/twitter.ts @@ -7,6 +7,7 @@ import User, { pack, ILocalUser } from '../../../models/user'; import { publishMainStream } from '../../../stream'; import config from '../../../config'; import signin from '../common/signin'; +import fetchMeta from '../../../misc/fetch-meta'; function getUserToken(ctx: Koa.Context) { return ((ctx.headers['cookie'] || '').match(/i=(!\w+)/) || [null, null])[1]; @@ -55,131 +56,133 @@ router.get('/disconnect/twitter', async ctx => { })); }); -if (config.twitter == null || redis == null) { - router.get('/connect/twitter', ctx => { - ctx.body = '現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)'; - }); +async function getTwAuth() { + const meta = await fetchMeta(); - router.get('/signin/twitter', ctx => { - ctx.body = '現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)'; - }); -} else { - const twAuth = autwh({ - consumerKey: config.twitter.consumer_key, - consumerSecret: config.twitter.consumer_secret, - callbackUrl: `${config.url}/api/tw/cb` - }); - - router.get('/connect/twitter', async ctx => { - if (!compareOrigin(ctx)) { - ctx.throw(400, 'invalid origin'); - return; - } - - const userToken = getUserToken(ctx); - if (userToken == null) { - ctx.throw(400, 'signin required'); - return; - } - - const twCtx = await twAuth.begin(); - redis.set(userToken, JSON.stringify(twCtx)); - ctx.redirect(twCtx.url); - }); - - router.get('/signin/twitter', async ctx => { - const twCtx = await twAuth.begin(); - - const sessid = uuid(); - - redis.set(sessid, JSON.stringify(twCtx)); - - const expires = 1000 * 60 * 60; // 1h - ctx.cookies.set('signin_with_twitter_session_id', sessid, { - path: '/', - domain: config.host, - secure: config.url.startsWith('https'), - httpOnly: true, - expires: new Date(Date.now() + expires), - maxAge: expires + if (meta.enableTwitterIntegration) { + return autwh({ + consumerKey: meta.twitterConsumerKey, + consumerSecret: meta.twitterConsumerSecret, + callbackUrl: `${config.url}/api/tw/cb` }); - - ctx.redirect(twCtx.url); - }); - - router.get('/tw/cb', async ctx => { - const userToken = getUserToken(ctx); - - if (userToken == null) { - const sessid = ctx.cookies.get('signin_with_twitter_session_id'); - - if (sessid == null) { - ctx.throw(400, 'invalid session'); - return; - } - - const get = new Promise((res, rej) => { - redis.get(sessid, async (_, twCtx) => { - res(twCtx); - }); - }); - - const twCtx = await get; - - const result = await twAuth.done(JSON.parse(twCtx), ctx.query.oauth_verifier); - - const user = await User.findOne({ - host: null, - 'twitter.userId': result.userId - }) as ILocalUser; - - if (user == null) { - ctx.throw(404, `@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`); - return; - } - - signin(ctx, user, true); - } else { - const verifier = ctx.query.oauth_verifier; - - if (verifier == null) { - ctx.throw(400, 'invalid session'); - return; - } - - const get = new Promise((res, rej) => { - redis.get(userToken, async (_, twCtx) => { - res(twCtx); - }); - }); - - const twCtx = await get; - - const result = await twAuth.done(JSON.parse(twCtx), verifier); - - const user = await User.findOneAndUpdate({ - host: null, - token: userToken - }, { - $set: { - twitter: { - accessToken: result.accessToken, - accessTokenSecret: result.accessTokenSecret, - userId: result.userId, - screenName: result.screenName - } - } - }); - - ctx.body = `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`; - - // Publish i updated event - publishMainStream(user._id, 'meUpdated', await pack(user, user, { - detail: true, - includeSecrets: true - })); - } - }); + } else { + return null; + } } +router.get('/connect/twitter', async ctx => { + if (!compareOrigin(ctx)) { + ctx.throw(400, 'invalid origin'); + return; + } + + const userToken = getUserToken(ctx); + if (userToken == null) { + ctx.throw(400, 'signin required'); + return; + } + + const twAuth = await getTwAuth(); + const twCtx = await twAuth.begin(); + redis.set(userToken, JSON.stringify(twCtx)); + ctx.redirect(twCtx.url); +}); + +router.get('/signin/twitter', async ctx => { + const twAuth = await getTwAuth(); + const twCtx = await twAuth.begin(); + + const sessid = uuid(); + + redis.set(sessid, JSON.stringify(twCtx)); + + const expires = 1000 * 60 * 60; // 1h + ctx.cookies.set('signin_with_twitter_session_id', sessid, { + path: '/', + domain: config.host, + secure: config.url.startsWith('https'), + httpOnly: true, + expires: new Date(Date.now() + expires), + maxAge: expires + }); + + ctx.redirect(twCtx.url); +}); + +router.get('/tw/cb', async ctx => { + const userToken = getUserToken(ctx); + + const twAuth = await getTwAuth(); + + if (userToken == null) { + const sessid = ctx.cookies.get('signin_with_twitter_session_id'); + + if (sessid == null) { + ctx.throw(400, 'invalid session'); + return; + } + + const get = new Promise((res, rej) => { + redis.get(sessid, async (_, twCtx) => { + res(twCtx); + }); + }); + + const twCtx = await get; + + const result = await twAuth.done(JSON.parse(twCtx), ctx.query.oauth_verifier); + + const user = await User.findOne({ + host: null, + 'twitter.userId': result.userId + }) as ILocalUser; + + if (user == null) { + ctx.throw(404, `@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`); + return; + } + + signin(ctx, user, true); + } else { + const verifier = ctx.query.oauth_verifier; + + if (verifier == null) { + ctx.throw(400, 'invalid session'); + return; + } + + const get = new Promise((res, rej) => { + redis.get(userToken, async (_, twCtx) => { + res(twCtx); + }); + }); + + const twCtx = await get; + + const result = await twAuth.done(JSON.parse(twCtx), verifier); + + const user = await User.findOneAndUpdate({ + host: null, + token: userToken + }, { + $set: { + twitter: { + accessToken: result.accessToken, + accessTokenSecret: result.accessTokenSecret, + userId: result.userId, + screenName: result.screenName + } + } + }); + + ctx.body = `Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`; + + // Publish i updated event + publishMainStream(user._id, 'meUpdated', await pack(user, user, { + detail: true, + includeSecrets: true + })); + } +}); + module.exports = router;