diff options
| author | tamaina <tamaina@hotmail.co.jp> | 2018-04-11 20:27:09 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-04-11 20:27:09 +0900 |
| commit | d43fe853c3605696e2e57e240845d0fc9c284f61 (patch) | |
| tree | 838914e262c0fca5737588a7bba64e2b9f3d8e5f /src/server/api/service | |
| parent | Update README.md (diff) | |
| parent | wip #1443 (diff) | |
| download | misskey-d43fe853c3605696e2e57e240845d0fc9c284f61.tar.gz misskey-d43fe853c3605696e2e57e240845d0fc9c284f61.tar.bz2 misskey-d43fe853c3605696e2e57e240845d0fc9c284f61.zip | |
Merge pull request #1 from syuilo/master
追従
Diffstat (limited to 'src/server/api/service')
| -rw-r--r-- | src/server/api/service/github.ts | 140 | ||||
| -rw-r--r-- | src/server/api/service/twitter.ts | 176 |
2 files changed, 316 insertions, 0 deletions
diff --git a/src/server/api/service/github.ts b/src/server/api/service/github.ts new file mode 100644 index 0000000000..bc8d3c6a7d --- /dev/null +++ b/src/server/api/service/github.ts @@ -0,0 +1,140 @@ +import * as EventEmitter from 'events'; +import * as express from 'express'; +import * as request from 'request'; +const crypto = require('crypto'); + +import User from '../../../models/user'; +import createNote from '../../../services/note/create'; +import config from '../../../config'; + +module.exports = async (app: express.Application) => { + if (config.github_bot == null) return; + + const bot = await User.findOne({ + usernameLower: config.github_bot.username.toLowerCase() + }); + + if (bot == null) { + console.warn(`GitHub hook bot specified, but not found: @${config.github_bot.username}`); + return; + } + + const post = text => createNote(bot, { text }); + + const handler = new EventEmitter(); + + app.post('/hooks/github', (req, res, next) => { + // req.headers['x-hub-signature'] および + // req.headers['x-github-event'] は常に string ですが、型定義の都合上 + // string | string[] になっているので string を明示しています + if ((new Buffer(req.headers['x-hub-signature'] as string)).equals(new Buffer(`sha1=${crypto.createHmac('sha1', config.github_bot.hook_secret).update(JSON.stringify(req.body)).digest('hex')}`))) { + handler.emit(req.headers['x-github-event'] as string, req.body); + res.sendStatus(200); + } else { + res.sendStatus(400); + } + }); + + 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`, + 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 = 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}** ⭐️`); + }); + + 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 new file mode 100644 index 0000000000..e5239fa171 --- /dev/null +++ b/src/server/api/service/twitter.ts @@ -0,0 +1,176 @@ +import * as express from 'express'; +import * as cookie from 'cookie'; +import * as uuid from 'uuid'; +// import * as Twitter from 'twitter'; +// const Twitter = require('twitter'); +import autwh from 'autwh'; +import redis from '../../../db/redis'; +import User, { pack } from '../../../models/user'; +import event from '../../../publishers/stream'; +import config from '../../../config'; +import signin from '../common/signin'; + +module.exports = (app: express.Application) => { + function getUserToken(req: express.Request) { + // req.headers['cookie'] は常に string ですが、型定義の都合上 + // string | string[] になっているので string を明示しています + return ((req.headers['cookie'] as string || '').match(/i=(!\w+)/) || [null, null])[1]; + } + + function compareOrigin(req: express.Request) { + function normalizeUrl(url: string) { + return url[url.length - 1] === '/' ? url.substr(0, url.length - 1) : url; + } + + // req.headers['referer'] は常に string ですが、型定義の都合上 + // string | string[] になっているので string を明示しています + const referer = req.headers['referer'] as string; + + return (normalizeUrl(referer) == normalizeUrl(config.url)); + } + + app.get('/disconnect/twitter', async (req, res): Promise<any> => { + if (!compareOrigin(req)) { + res.status(400).send('invalid origin'); + return; + } + + const userToken = getUserToken(req); + if (userToken == null) return res.send('plz signin'); + + const user = await User.findOneAndUpdate({ + host: null, + 'token': userToken + }, { + $set: { + 'twitter': null + } + }); + + res.send(`Twitterの連携を解除しました :v:`); + + // Publish i updated event + event(user._id, 'i_updated', await pack(user, user, { + detail: true, + includeSecrets: true + })); + }); + + if (config.twitter == null) { + app.get('/connect/twitter', (req, res) => { + res.send('現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)'); + }); + + app.get('/signin/twitter', (req, res) => { + res.send('現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)'); + }); + + return; + } + + const twAuth = autwh({ + consumerKey: config.twitter.consumer_key, + consumerSecret: config.twitter.consumer_secret, + callbackUrl: `${config.url}/api/tw/cb` + }); + + app.get('/connect/twitter', async (req, res): Promise<any> => { + if (!compareOrigin(req)) { + res.status(400).send('invalid origin'); + return; + } + + const userToken = getUserToken(req); + if (userToken == null) return res.send('plz signin'); + + const ctx = await twAuth.begin(); + redis.set(userToken, JSON.stringify(ctx)); + res.redirect(ctx.url); + }); + + app.get('/signin/twitter', async (req, res): Promise<any> => { + const ctx = await twAuth.begin(); + + const sessid = uuid(); + + redis.set(sessid, JSON.stringify(ctx)); + + const expires = 1000 * 60 * 60; // 1h + res.cookie('signin_with_twitter_session_id', sessid, { + path: '/', + domain: `.${config.host}`, + secure: config.url.substr(0, 5) === 'https', + httpOnly: true, + expires: new Date(Date.now() + expires), + maxAge: expires + }); + + res.redirect(ctx.url); + }); + + app.get('/tw/cb', (req, res): any => { + const userToken = getUserToken(req); + + if (userToken == null) { + // req.headers['cookie'] は常に string ですが、型定義の都合上 + // string | string[] になっているので string を明示しています + const cookies = cookie.parse((req.headers['cookie'] as string || '')); + + const sessid = cookies['signin_with_twitter_session_id']; + + if (sessid == undefined) { + res.status(400).send('invalid session'); + return; + } + + redis.get(sessid, async (_, ctx) => { + const result = await twAuth.done(JSON.parse(ctx), req.query.oauth_verifier); + + const user = await User.findOne({ + host: null, + 'twitter.userId': result.userId + }); + + if (user == null) { + res.status(404).send(`@${result.screenName}と連携しているMisskeyアカウントはありませんでした...`); + return; + } + + signin(res, user, true); + }); + } else { + const verifier = req.query.oauth_verifier; + + if (verifier == null) { + res.status(400).send('invalid session'); + return; + } + + redis.get(userToken, async (_, ctx) => { + const result = await twAuth.done(JSON.parse(ctx), verifier); + + const user = await User.findOneAndUpdate({ + host: null, + 'token': userToken + }, { + $set: { + 'twitter': { + accessToken: result.accessToken, + accessTokenSecret: result.accessTokenSecret, + userId: result.userId, + screenName: result.screenName + } + } + }); + + res.send(`Twitter: @${result.screenName} を、Misskey: @${user.username} に接続しました!`); + + // Publish i updated event + event(user._id, 'i_updated', await pack(user, user, { + detail: true, + includeSecrets: true + })); + }); + } + }); +}; |