GitHub / Twitter連携の設定をDBに保存するように
This commit is contained in:
		
							parent
							
								
									5675ecead9
								
							
						
					
					
						commit
						cb6f390fb6
					
				
					 12 changed files with 632 additions and 476 deletions
				
			
		|  | @ -57,13 +57,6 @@ npm install web-push -g | ||||||
| web-push generate-vapid-keys | 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 | *5.* Make configuration file | ||||||
| ---------------------------------------------------------------- | ---------------------------------------------------------------- | ||||||
| 1. `cp .config/example.yml .config/default.yml` Copy the `.config/example.yml` and rename it to `default.yml`. | 1. `cp .config/example.yml .config/default.yml` Copy the `.config/example.yml` and rename it to `default.yml`. | ||||||
|  |  | ||||||
|  | @ -1095,6 +1095,16 @@ admin/views/instance.vue: | ||||||
|   enable-recaptcha: "reCAPTCHAを有効にする" |   enable-recaptcha: "reCAPTCHAを有効にする" | ||||||
|   recaptcha-site-key: "reCAPTCHA site key" |   recaptcha-site-key: "reCAPTCHA site key" | ||||||
|   recaptcha-secret-key: "reCAPTCHA secret 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-config: "プロキシアカウントの設定" | ||||||
|   proxy-account-info: "プロキシアカウントは、特定の条件下でユーザーのリモートフォローを代行するアカウントです。例えば、ユーザーがリモートユーザーをリストに入れたとき、リストに入れられたユーザーを誰もフォローしていないとアクティビティがサーバーに配達されないため、代わりにプロキシアカウントがフォローするようにします。" |   proxy-account-info: "プロキシアカウントは、特定の条件下でユーザーのリモートフォローを代行するアカウントです。例えば、ユーザーがリモートユーザーをリストに入れたとき、リストに入れられたユーザーを誰もフォローしていないとアクティビティがサーバーに配達されないため、代わりにプロキシアカウントがフォローするようにします。" | ||||||
|   proxy-account-username: "プロキシアカウントのユーザー名" |   proxy-account-username: "プロキシアカウントのユーザー名" | ||||||
|  |  | ||||||
|  | @ -53,6 +53,28 @@ | ||||||
| 			<p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p> | 			<p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p> | ||||||
| 		</section> | 		</section> | ||||||
| 	</ui-card> | 	</ui-card> | ||||||
|  | 
 | ||||||
|  | 	<ui-card> | ||||||
|  | 		<div slot="title"><fa :icon="['fab', 'twitter']"/> %i18n:@twitter-integration-config%</div> | ||||||
|  | 		<section> | ||||||
|  | 			<ui-switch v-model="enableTwitterIntegration">%i18n:@enable-twitter-integration%</ui-switch> | ||||||
|  | 			<ui-info>%i18n:@twitter-integration-info%</ui-info> | ||||||
|  | 			<ui-input v-model="twitterConsumerKey" :disabled="!enableTwitterIntegration"><i slot="icon"><fa icon="key"/></i>%i18n:@twitter-integration-consumer-key%</ui-input> | ||||||
|  | 			<ui-input v-model="twitterConsumerSecret" :disabled="!enableTwitterIntegration"><i slot="icon"><fa icon="key"/></i>%i18n:@twitter-integration-consumer-secret%</ui-input> | ||||||
|  | 			<ui-button @click="updateMeta">%i18n:@save%</ui-button> | ||||||
|  | 		</section> | ||||||
|  | 	</ui-card> | ||||||
|  | 
 | ||||||
|  | 	<ui-card> | ||||||
|  | 		<div slot="title"><fa :icon="['fab', 'github']"/> %i18n:@github-integration-config%</div> | ||||||
|  | 		<section> | ||||||
|  | 			<ui-switch v-model="enableGithubIntegration">%i18n:@enable-github-integration%</ui-switch> | ||||||
|  | 			<ui-info>%i18n:@github-integration-info%</ui-info> | ||||||
|  | 			<ui-input v-model="githubClientId" :disabled="!enableGithubIntegration"><i slot="icon"><fa icon="key"/></i>%i18n:@github-integration-client-id%</ui-input> | ||||||
|  | 			<ui-input v-model="githubClientSecret" :disabled="!enableGithubIntegration"><i slot="icon"><fa icon="key"/></i>%i18n:@github-integration-client-secret%</ui-input> | ||||||
|  | 			<ui-button @click="updateMeta">%i18n:@save%</ui-button> | ||||||
|  | 		</section> | ||||||
|  | 	</ui-card> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  | @ -77,6 +99,12 @@ export default Vue.extend({ | ||||||
| 			enableRecaptcha: false, | 			enableRecaptcha: false, | ||||||
| 			recaptchaSiteKey: null, | 			recaptchaSiteKey: null, | ||||||
| 			recaptchaSecretKey: null, | 			recaptchaSecretKey: null, | ||||||
|  | 			enableTwitterIntegration: false, | ||||||
|  | 			twitterConsumerKey: null, | ||||||
|  | 			twitterConsumerSecret: null, | ||||||
|  | 			enableGithubIntegration: false, | ||||||
|  | 			githubClientId: null, | ||||||
|  | 			githubClientSecret: null, | ||||||
| 			proxyAccount: null, | 			proxyAccount: null, | ||||||
| 			inviteCode: null, | 			inviteCode: null, | ||||||
| 		}; | 		}; | ||||||
|  | @ -98,6 +126,12 @@ export default Vue.extend({ | ||||||
| 			this.recaptchaSiteKey = meta.recaptchaSiteKey; | 			this.recaptchaSiteKey = meta.recaptchaSiteKey; | ||||||
| 			this.recaptchaSecretKey = meta.recaptchaSecretKey; | 			this.recaptchaSecretKey = meta.recaptchaSecretKey; | ||||||
| 			this.proxyAccount = meta.proxyAccount; | 			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, | 				recaptchaSiteKey: this.recaptchaSiteKey, | ||||||
| 				recaptchaSecretKey: this.recaptchaSecretKey, | 				recaptchaSecretKey: this.recaptchaSecretKey, | ||||||
| 				proxyAccount: this.proxyAccount, | 				proxyAccount: this.proxyAccount, | ||||||
|  | 				enableTwitterIntegration: this.enableTwitterIntegration, | ||||||
|  | 				twitterConsumerKey: this.twitterConsumerKey, | ||||||
|  | 				twitterConsumerSecret: this.twitterConsumerSecret, | ||||||
|  | 				enableGithubIntegration: this.enableGithubIntegration, | ||||||
|  | 				githubClientId: this.githubClientId, | ||||||
|  | 				githubClientSecret: this.githubClientSecret, | ||||||
| 			}).then(() => { | 			}).then(() => { | ||||||
| 				this.$swal({ | 				this.$swal({ | ||||||
| 					type: 'success', | 					type: 'success', | ||||||
|  |  | ||||||
|  | @ -40,14 +40,7 @@ export type Source = { | ||||||
| 	summalyProxy?: string; | 	summalyProxy?: string; | ||||||
| 
 | 
 | ||||||
| 	accesslog?: string; | 	accesslog?: string; | ||||||
| 	twitter?: { | 
 | ||||||
| 		consumer_key: string; |  | ||||||
| 		consumer_secret: string; |  | ||||||
| 	}; |  | ||||||
| 	github?: { |  | ||||||
| 		client_id: string; |  | ||||||
| 		client_secret: string; |  | ||||||
| 	}; |  | ||||||
| 	github_bot?: { | 	github_bot?: { | ||||||
| 		hook_secret: string; | 		hook_secret: string; | ||||||
| 		username: string; | 		username: string; | ||||||
|  |  | ||||||
|  | @ -11,7 +11,9 @@ const defaultMeta: any = { | ||||||
| 		originalNotesCount: 0, | 		originalNotesCount: 0, | ||||||
| 		originalUsersCount: 0 | 		originalUsersCount: 0 | ||||||
| 	}, | 	}, | ||||||
| 	maxNoteTextLength: 1000 | 	maxNoteTextLength: 1000, | ||||||
|  | 	enableTwitterIntegration: false, | ||||||
|  | 	enableGithubIntegration: false, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export default async function(): Promise<IMeta> { | export default async function(): Promise<IMeta> { | ||||||
|  |  | ||||||
|  | @ -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 = { | export type IMeta = { | ||||||
| 	name?: string; | 	name?: string; | ||||||
|  | @ -157,4 +183,12 @@ export type IMeta = { | ||||||
| 	 * Max allowed note text length in charactors | 	 * Max allowed note text length in charactors | ||||||
| 	 */ | 	 */ | ||||||
| 	maxNoteTextLength?: number; | 	maxNoteTextLength?: number; | ||||||
|  | 
 | ||||||
|  | 	enableTwitterIntegration?: boolean; | ||||||
|  | 	twitterConsumerKey?: string; | ||||||
|  | 	twitterConsumerSecret?: string; | ||||||
|  | 
 | ||||||
|  | 	enableGithubIntegration?: boolean; | ||||||
|  | 	githubClientId?: string; | ||||||
|  | 	githubClientSecret?: string; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -137,7 +137,49 @@ export const meta = { | ||||||
| 			desc: { | 			desc: { | ||||||
| 				'ja-JP': 'インスタンスの対象言語' | 				'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; | 		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({}, { | 	await Meta.update({}, { | ||||||
| 		$set: set | 		$set: set | ||||||
| 	}, { upsert: true }); | 	}, { upsert: true }); | ||||||
|  |  | ||||||
|  | @ -77,8 +77,8 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { | ||||||
| 			elasticsearch: config.elasticsearch ? true : false, | 			elasticsearch: config.elasticsearch ? true : false, | ||||||
| 			recaptcha: instance.enableRecaptcha, | 			recaptcha: instance.enableRecaptcha, | ||||||
| 			objectStorage: config.drive && config.drive.storage === 'minio', | 			objectStorage: config.drive && config.drive.storage === 'minio', | ||||||
| 			twitter: config.twitter ? true : false, | 			twitter: instance.enableTwitterIntegration, | ||||||
| 			github: config.github ? true : false, | 			github: instance.enableGithubIntegration, | ||||||
| 			serviceWorker: config.sw ? true : false, | 			serviceWorker: config.sw ? true : false, | ||||||
| 			userRecommendation: config.user_recommendation ? config.user_recommendation : {} | 			userRecommendation: config.user_recommendation ? config.user_recommendation : {} | ||||||
| 		}; | 		}; | ||||||
|  |  | ||||||
|  | @ -44,6 +44,7 @@ router.post('/signup', require('./private/signup').default); | ||||||
| router.post('/signin', require('./private/signin').default); | router.post('/signin', require('./private/signin').default); | ||||||
| 
 | 
 | ||||||
| router.use(require('./service/github').routes()); | router.use(require('./service/github').routes()); | ||||||
|  | router.use(require('./service/github-bot').routes()); | ||||||
| router.use(require('./service/twitter').routes()); | router.use(require('./service/twitter').routes()); | ||||||
| 
 | 
 | ||||||
| router.use(require('./mastodon').routes()); | router.use(require('./mastodon').routes()); | ||||||
|  |  | ||||||
							
								
								
									
										156
									
								
								src/server/api/service/github-bot.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								src/server/api/service/github-bot.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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); | ||||||
|  | }); | ||||||
|  | @ -1,37 +1,14 @@ | ||||||
| import * as EventEmitter from 'events'; |  | ||||||
| import * as Koa from 'koa'; | import * as Koa from 'koa'; | ||||||
| import * as Router from 'koa-router'; | import * as Router from 'koa-router'; | ||||||
| import * as request from 'request'; | import * as request from 'request'; | ||||||
| import { OAuth2 } from 'oauth'; | import { OAuth2 } from 'oauth'; | ||||||
| import User, { IUser, pack, ILocalUser } from '../../../models/user'; | import User, { pack, ILocalUser } from '../../../models/user'; | ||||||
| import createNote from '../../../services/note/create'; |  | ||||||
| import config from '../../../config'; | import config from '../../../config'; | ||||||
| import { publishMainStream } from '../../../stream'; | import { publishMainStream } from '../../../stream'; | ||||||
| import redis from '../../../db/redis'; | import redis from '../../../db/redis'; | ||||||
| import uuid = require('uuid'); | import uuid = require('uuid'); | ||||||
| import signin from '../common/signin'; | import signin from '../common/signin'; | ||||||
| const crypto = require('crypto'); | import fetchMeta from '../../../misc/fetch-meta'; | ||||||
| 
 |  | ||||||
| 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' }); |  | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| function getUserToken(ctx: Koa.Context) { | function getUserToken(ctx: Koa.Context) { | ||||||
| 	return ((ctx.headers['cookie'] || '').match(/i=(!\w+)/) || [null, null])[1]; | 	return ((ctx.headers['cookie'] || '').match(/i=(!\w+)/) || [null, null])[1]; | ||||||
|  | @ -80,337 +57,218 @@ router.get('/disconnect/github', async ctx => { | ||||||
| 	})); | 	})); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| if (!config.github || !redis) { | async function getOath2() { | ||||||
| 	router.get('/connect/github', ctx => { | 	const meta = await fetchMeta(); | ||||||
| 		ctx.body = '現在GitHubへ接続できません (このインスタンスではGitHubはサポートされていません)'; | 
 | ||||||
|  | 	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 => { | 	redis.set(sessid, JSON.stringify(params)); | ||||||
| 		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'); |  | ||||||
| 
 | 
 | ||||||
| 	router.get('/connect/github', async ctx => { | 	const oauth2 = await getOath2(); | ||||||
| 		if (!compareOrigin(ctx)) { | 	ctx.redirect(oauth2.getAuthorizeUrl(params)); | ||||||
| 			ctx.throw(400, 'invalid origin'); | }); | ||||||
|  | 
 | ||||||
|  | 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; | 			return; | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		const userToken = getUserToken(ctx); | 		const code = ctx.query.code; | ||||||
| 		if (!userToken) { | 
 | ||||||
| 			ctx.throw(400, 'signin required'); | 		if (!code) { | ||||||
|  | 			ctx.throw(400, 'invalid session'); | ||||||
| 			return; | 			return; | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		const params = { | 		const { redirect_uri, state } = await new Promise<any>((res, rej) => { | ||||||
| 			redirect_uri: `${config.url}/api/gh/cb`, | 			redis.get(sessid, async (_, state) => { | ||||||
| 			scope: ['read:user'], | 				res(JSON.parse(state)); | ||||||
| 			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 |  | ||||||
| 		}); | 		}); | ||||||
| 
 | 
 | ||||||
| 		redis.set(sessid, JSON.stringify(params)); | 		if (ctx.query.state !== state) { | ||||||
| 		ctx.redirect(oauth2.getAuthorizeUrl(params)); | 			ctx.throw(400, 'invalid session'); | ||||||
| 	}); | 			return; | ||||||
|  | 		} | ||||||
| 
 | 
 | ||||||
| 	router.get('/gh/cb', async ctx => { | 		const { accessToken } = await new Promise<any>((res, rej) => | ||||||
| 		const userToken = getUserToken(ctx); | 			oauth2.getOAuthAccessToken( | ||||||
| 
 | 				code, | ||||||
| 		if (!userToken) { | 				{ redirect_uri }, | ||||||
| 			const sessid = ctx.cookies.get('signin_with_github_session_id'); | 				(err, accessToken, refresh, result) => { | ||||||
| 
 |  | ||||||
| 			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<any>((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<any>((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<any>((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) | 					if (err) | ||||||
| 						rej(err); | 						rej(err); | ||||||
|  | 					else if (result.error) | ||||||
|  | 						rej(result.error); | ||||||
| 					else | 					else | ||||||
| 						res(JSON.parse(body)); | 						res({ accessToken }); | ||||||
| 				})); | 				})); | ||||||
| 
 | 
 | ||||||
| 			if (!login || !id) { | 		const { login, id } = await new Promise<any>((res, rej) => | ||||||
| 				ctx.throw(400, 'invalid session'); | 			request({ | ||||||
| 				return; | 				url: 'https://api.github.com/user', | ||||||
| 			} | 				headers: { | ||||||
| 
 | 					'Accept': 'application/vnd.github.v3+json', | ||||||
| 			const user = await User.findOne({ | 					'Authorization': `bearer ${accessToken}`, | ||||||
| 				host: null, | 					'User-Agent': config.user_agent | ||||||
| 				'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<any>((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<any>((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<any>((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 |  | ||||||
| 					} |  | ||||||
| 				} | 				} | ||||||
| 			}); | 			}, (err, response, body) => { | ||||||
| 
 | 				if (err) | ||||||
| 			ctx.body = `GitHub: @${login} を、Misskey: @${user.username} に接続しました!`; | 					rej(err); | ||||||
| 
 | 				else | ||||||
| 			// Publish i updated event
 | 					res(JSON.parse(body)); | ||||||
| 			publishMainStream(user._id, 'meUpdated', await pack(user, user, { |  | ||||||
| 				detail: true, |  | ||||||
| 				includeSecrets: true |  | ||||||
| 			})); | 			})); | ||||||
|  | 
 | ||||||
|  | 		if (!login || !id) { | ||||||
|  | 			ctx.throw(400, 'invalid session'); | ||||||
|  | 			return; | ||||||
| 		} | 		} | ||||||
| 	}); |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| if (config.github_bot) { | 		const user = await User.findOne({ | ||||||
| 	const secret = config.github_bot.hook_secret; | 			host: null, | ||||||
|  | 			'github.id': id | ||||||
|  | 		}) as ILocalUser; | ||||||
| 
 | 
 | ||||||
| 	router.post('/hooks/github', ctx => { | 		if (!user) { | ||||||
| 		const body = JSON.stringify(ctx.request.body); | 			ctx.throw(404, `@${login}と連携しているMisskeyアカウントはありませんでした...`); | ||||||
| 		const hash = crypto.createHmac('sha1', secret).update(body).digest('hex'); | 			return; | ||||||
| 		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; |  | ||||||
| 		} | 		} | ||||||
| 	}); | 
 | ||||||
| } | 		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<any>((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<any>((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<any>((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; | 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); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
|  | @ -7,6 +7,7 @@ import User, { pack, ILocalUser } from '../../../models/user'; | ||||||
| import { publishMainStream } from '../../../stream'; | import { publishMainStream } from '../../../stream'; | ||||||
| import config from '../../../config'; | import config from '../../../config'; | ||||||
| import signin from '../common/signin'; | import signin from '../common/signin'; | ||||||
|  | import fetchMeta from '../../../misc/fetch-meta'; | ||||||
| 
 | 
 | ||||||
| function getUserToken(ctx: Koa.Context) { | function getUserToken(ctx: Koa.Context) { | ||||||
| 	return ((ctx.headers['cookie'] || '').match(/i=(!\w+)/) || [null, null])[1]; | 	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) { | async function getTwAuth() { | ||||||
| 	router.get('/connect/twitter', ctx => { | 	const meta = await fetchMeta(); | ||||||
| 		ctx.body = '現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)'; |  | ||||||
| 	}); |  | ||||||
| 
 | 
 | ||||||
| 	router.get('/signin/twitter', ctx => { | 	if (meta.enableTwitterIntegration) { | ||||||
| 		ctx.body = '現在Twitterへ接続できません (このインスタンスではTwitterはサポートされていません)'; | 		return autwh({ | ||||||
| 	}); | 			consumerKey: meta.twitterConsumerKey, | ||||||
| } else { | 			consumerSecret: meta.twitterConsumerSecret, | ||||||
| 	const twAuth = autwh({ | 			callbackUrl: `${config.url}/api/tw/cb` | ||||||
| 		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 |  | ||||||
| 		}); | 		}); | ||||||
| 
 | 	} else { | ||||||
| 		ctx.redirect(twCtx.url); | 		return null; | ||||||
| 	}); | 	} | ||||||
| 
 |  | ||||||
| 	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<any>((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<any>((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 |  | ||||||
| 			})); |  | ||||||
| 		} |  | ||||||
| 	}); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | 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<any>((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<any>((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; | module.exports = router; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue