WIP: Add Discord auth (#3239)
* Add Discord auth * Apply review 175263424
This commit is contained in:
		
							parent
							
								
									a34fdc2068
								
							
						
					
					
						commit
						9d8f7b081d
					
				
					 17 changed files with 522 additions and 4 deletions
				
			
		|  | @ -399,6 +399,7 @@ common/views/components/signin.vue: | |||
|   or: "または" | ||||
|   signin-with-twitter: "Twitterでログイン" | ||||
|   signin-with-github: "GitHubでログイン" | ||||
|   signin-with-discord: "Discordでログイン" | ||||
|   login-failed: "ログインできませんでした。ユーザー名とパスワードを確認してください。" | ||||
| 
 | ||||
| common/views/components/signup.vue: | ||||
|  | @ -450,6 +451,14 @@ common/views/components/github-setting.vue: | |||
|   connect: "GitHubと接続する" | ||||
|   disconnect: "切断する" | ||||
| 
 | ||||
| common/views/components/discord-setting.vue: | ||||
|   description: "お使いのDiscordアカウントをお使いのMisskeyアカウントに接続しておくと、プロフィールでDiscordアカウント情報が表示されるようになったり、Discordを用いた便利なサインインを利用できるようになります。" | ||||
|   connected-to: "次のDiscordアカウントに接続されています" | ||||
|   detail: "詳細..." | ||||
|   reconnect: "再接続する" | ||||
|   connect: "Discordと接続する" | ||||
|   disconnect: "切断する" | ||||
| 
 | ||||
| common/views/components/uploader.vue: | ||||
|   waiting: "待機中" | ||||
| 
 | ||||
|  | @ -1081,7 +1090,12 @@ admin/views/instance.vue: | |||
|   github-integration-info: "コールバックURLは /api/gh/cb に設定します。" | ||||
|   enable-github-integration: "GitHub連携を有効にする" | ||||
|   github-integration-client-id: "Client ID" | ||||
|   github-integration-client-secret: "Client secret" | ||||
|   github-integration-client-secret: "Client Secret" | ||||
|   discord-integration-config: "Discord連携の設定" | ||||
|   discord-integration-info: "コールバックURLは /api/dc/cb に設定します。" | ||||
|   enable-discord-integration: "Discord連携を有効にする" | ||||
|   discord-integration-client-id: "Client ID" | ||||
|   discord-integration-client-secret: "Client Secret" | ||||
|   proxy-account-config: "プロキシアカウントの設定" | ||||
|   proxy-account-info: "プロキシアカウントは、特定の条件下でユーザーのリモートフォローを代行するアカウントです。例えば、ユーザーがリモートユーザーをリストに入れたとき、リストに入れられたユーザーを誰もフォローしていないとアクティビティがサーバーに配達されないため、代わりにプロキシアカウントがフォローするようにします。" | ||||
|   proxy-account-username: "プロキシアカウントのユーザー名" | ||||
|  | @ -1530,6 +1544,10 @@ mobile/views/pages/settings.vue: | |||
|   github-connect: "GitHubアカウントに接続する" | ||||
|   github-reconnect: "再接続する" | ||||
|   github-disconnect: "切断する" | ||||
|   discord: "Discord連携" | ||||
|   discord-connect: "Discordアカウントに接続する" | ||||
|   discord-reconnect: "再接続する" | ||||
|   discord-disconnect: "切断する" | ||||
|   update: "Misskey Update" | ||||
|   version: "バージョン:" | ||||
|   latest-version: "最新のバージョン:" | ||||
|  |  | |||
|  | @ -76,6 +76,17 @@ | |||
| 			<ui-button @click="updateMeta">{{ $t('save') }}</ui-button> | ||||
| 		</section> | ||||
| 	</ui-card> | ||||
| 
 | ||||
| 	<ui-card> | ||||
| 		<div slot="title"><fa :icon="['fab', 'discord']"/> {{ $t('discord-integration-config') }}</div> | ||||
| 		<section> | ||||
| 			<ui-switch v-model="enableDiscordIntegration">{{ $t('enable-discord-integration') }}</ui-switch> | ||||
| 			<ui-info>{{ $t('discord-integration-info') }}</ui-info> | ||||
| 			<ui-input v-model="discordClientId" :disabled="!enableDiscordIntegration"><i slot="icon"><fa icon="key"/></i>{{ $t('discord-integration-client-id') }}</ui-input> | ||||
| 			<ui-input v-model="discordClientSecret" :disabled="!enableDiscordIntegration"><i slot="icon"><fa icon="key"/></i>{{ $t('discord-integration-client-secret') }}</ui-input> | ||||
| 			<ui-button @click="updateMeta">{{ $t('save') }}</ui-button> | ||||
| 		</section> | ||||
| 	</ui-card> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -113,6 +124,9 @@ export default Vue.extend({ | |||
| 			enableGithubIntegration: false, | ||||
| 			githubClientId: null, | ||||
| 			githubClientSecret: null, | ||||
| 			enableDiscordIntegration: false, | ||||
| 			discordClientId: null, | ||||
| 			discordClientSecret: null, | ||||
| 			proxyAccount: null, | ||||
| 			inviteCode: null, | ||||
| 			faHeadset, faShieldAlt, faGhost | ||||
|  | @ -141,6 +155,9 @@ export default Vue.extend({ | |||
| 			this.enableGithubIntegration = meta.enableGithubIntegration; | ||||
| 			this.githubClientId = meta.githubClientId; | ||||
| 			this.githubClientSecret = meta.githubClientSecret; | ||||
| 			this.enableDiscordIntegration = meta.enableDiscordIntegration; | ||||
| 			this.discordClientId = meta.discordClientId; | ||||
| 			this.discordClientSecret = meta.discordClientSecret; | ||||
| 		}); | ||||
| 	}, | ||||
| 
 | ||||
|  | @ -180,6 +197,9 @@ export default Vue.extend({ | |||
| 				enableGithubIntegration: this.enableGithubIntegration, | ||||
| 				githubClientId: this.githubClientId, | ||||
| 				githubClientSecret: this.githubClientSecret, | ||||
| 				enableDiscordIntegration: this.enableDiscordIntegration, | ||||
| 				discordClientId: this.discordClientId, | ||||
| 				discordClientSecret: this.discordClientSecret | ||||
| 			}).then(() => { | ||||
| 				this.$root.alert({ | ||||
| 					type: 'success', | ||||
|  |  | |||
							
								
								
									
										64
									
								
								src/client/app/common/views/components/discord-setting.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/client/app/common/views/components/discord-setting.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,64 @@ | |||
| <template> | ||||
| <div class="mk-discord-setting"> | ||||
| 	<p>{{ $t('description') }}</p> | ||||
| 	<p class="account" v-if="$store.state.i.discord" :title="`Discord ID: ${$store.state.i.discord.id}`">{{ $t('connected-to') }}: <a :href="`https://discordapp.com/users/${$store.state.i.discord.id}`" target="_blank">@{{ $store.state.i.discord.username }}#{{ $store.state.i.discord.discriminator }}</a></p> | ||||
| 	<p> | ||||
| 		<a :href="`${apiUrl}/connect/discord`" target="_blank" @click.prevent="connect">{{ $store.state.i.discord ? this.$t('reconnect') : this.$t('connect') }}</a> | ||||
| 		<span v-if="$store.state.i.discord"> or </span> | ||||
| 		<a :href="`${apiUrl}/disconnect/discord`" target="_blank" v-if="$store.state.i.discord" @click.prevent="disconnect">{{ $t('disconnect') }}</a> | ||||
| 	</p> | ||||
| 	<p class="id" v-if="$store.state.i.discord">Discord ID: {{ $store.state.i.discord.id }}</p> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../../i18n'; | ||||
| import { apiUrl } from '../../../config'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('common/views/components/discord-setting.vue'), | ||||
| 	data() { | ||||
| 		return { | ||||
| 			form: null, | ||||
| 			apiUrl | ||||
| 		}; | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		this.$watch('$store.state.i', () => { | ||||
| 			if (this.$store.state.i.discord && this.form) | ||||
| 				this.form.close(); | ||||
| 		}, { | ||||
| 			deep: true | ||||
| 		}); | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		connect() { | ||||
| 			this.form = window.open(apiUrl + '/connect/discord', | ||||
| 				'discord_connect_window', | ||||
| 				'height=570, width=520'); | ||||
| 		}, | ||||
| 
 | ||||
| 		disconnect() { | ||||
| 			window.open(apiUrl + '/disconnect/discord', | ||||
| 				'discord_disconnect_window', | ||||
| 				'height=570, width=520'); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="stylus" scoped> | ||||
| .mk-discord-setting | ||||
| 	.account | ||||
| 		border solid 1px #e1e8ed | ||||
| 		border-radius 4px | ||||
| 		padding 16px | ||||
| 
 | ||||
| 		a | ||||
| 			font-weight bold | ||||
| 			color inherit | ||||
| 
 | ||||
| 	.id | ||||
| 		color #8899a6 | ||||
| </style> | ||||
|  | @ -29,6 +29,7 @@ import ellipsis from './ellipsis.vue'; | |||
| import urlPreview from './url-preview.vue'; | ||||
| import twitterSetting from './twitter-setting.vue'; | ||||
| import githubSetting from './github-setting.vue'; | ||||
| import discordSetting from './discord-setting.vue'; | ||||
| import fileTypeIcon from './file-type-icon.vue'; | ||||
| import emoji from './emoji.vue'; | ||||
| import welcomeTimeline from './welcome-timeline.vue'; | ||||
|  | @ -74,6 +75,7 @@ Vue.component('mk-ellipsis', ellipsis); | |||
| Vue.component('mk-url-preview', urlPreview); | ||||
| Vue.component('mk-twitter-setting', twitterSetting); | ||||
| Vue.component('mk-github-setting', githubSetting); | ||||
| Vue.component('mk-discord-setting', discordSetting); | ||||
| Vue.component('mk-file-type-icon', fileTypeIcon); | ||||
| Vue.component('mk-emoji', emoji); | ||||
| Vue.component('mk-welcome-timeline', welcomeTimeline); | ||||
|  |  | |||
|  | @ -14,6 +14,7 @@ | |||
| 	<ui-button type="submit" :disabled="signing">{{ signing ? $t('signing-in') : $t('signin') }}</ui-button> | ||||
| 	<p style="margin: 8px 0;"><a :href="`${apiUrl}/signin/twitter`">{{ $t('signin-with-twitter') }}</a></p> | ||||
| 	<p style="margin: 8px 0;"><a :href="`${apiUrl}/signin/github`">{{ $t('signin-with-github') }}</a></p> | ||||
| 	<p style="margin: 8px 0;"><a :href="`${apiUrl}/signin/discord`">{{ $t('signin-with-discord') /* TODO: Make these layouts better */ }}</a></p> | ||||
| </form> | ||||
| </template> | ||||
| 
 | ||||
|  |  | |||
|  | @ -30,6 +30,13 @@ | |||
| 					<mk-github-setting/> | ||||
| 				</section> | ||||
| 			</ui-card> | ||||
| 
 | ||||
| 			<ui-card> | ||||
| 				<div slot="title"><fa :icon="['fab', 'discord']"/> {{ $t('discord') }}</div> | ||||
| 				<section> | ||||
| 					<mk-discord-setting/> | ||||
| 				</section> | ||||
| 			</ui-card> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<ui-card class="theme" v-show="page == 'theme'"> | ||||
|  |  | |||
							
								
								
									
										26
									
								
								src/client/app/desktop/views/pages/user/user.discord.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/client/app/desktop/views/pages/user/user.discord.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,26 @@ | |||
| <template> | ||||
| <div class="lkafjvabenanajk17kwqpsatoushincb"> | ||||
| 	<span><fa :icon="['fab', 'discord']"/><a :href="`https://discordapp.com/users/${user.discord.id}`" target="_blank">@{{ user.discord.username }}#{{ user.discord.discriminator }}</a></span> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	props: ['user'] | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="stylus" scoped> | ||||
| .lkafjvabenanajk17kwqpsatoushincb | ||||
| 	padding 32px | ||||
| 	background #7289da | ||||
| 	border-radius 6px | ||||
| 	color #fff | ||||
| 
 | ||||
| 	a | ||||
| 		margin-left 8px | ||||
| 		color #fff | ||||
| 
 | ||||
| </style> | ||||
|  | @ -14,6 +14,7 @@ | |||
| 				<x-profile :user="user"/> | ||||
| 				<x-twitter :user="user" v-if="!user.host && user.twitter"/> | ||||
| 				<x-github :user="user" v-if="!user.host && user.github"/> | ||||
| 				<x-discord :user="user" v-if="!user.host && user.discord"/> | ||||
| 				<mk-calendar @chosen="warp" :start="new Date(user.createdAt)"/> | ||||
| 				<mk-activity :user="user"/> | ||||
| 				<x-photos :user="user"/> | ||||
|  | @ -39,6 +40,7 @@ import XFollowersYouKnow from './user.followers-you-know.vue'; | |||
| import XFriends from './user.friends.vue'; | ||||
| import XTwitter from './user.twitter.vue'; | ||||
| import XGithub from './user.github.vue'; // ?MEM: Don't fix the intentional typo. (XGitHub -> `<x-git-hub>`) | ||||
| import XDiscord from './user.discord.vue'; | ||||
| 
 | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n(), | ||||
|  | @ -50,7 +52,8 @@ export default Vue.extend({ | |||
| 		XFollowersYouKnow, | ||||
| 		XFriends, | ||||
| 		XTwitter, | ||||
| 		XGithub // ?MEM: Don't fix the intentional typo. (see L41) | ||||
| 		XGithub, // ?MEM: Don't fix the intentional typo. (see L41) | ||||
| 		XDiscord | ||||
| 	}, | ||||
| 	data() { | ||||
| 		return { | ||||
|  |  | |||
|  | @ -143,6 +143,7 @@ import { | |||
| import { | ||||
| 	faTwitter as fabTwitter, | ||||
| 	faGithub as fabGithub, | ||||
| 	faDiscord as fabDiscord | ||||
| } from '@fortawesome/free-brands-svg-icons'; | ||||
| import i18n from './i18n'; | ||||
| 
 | ||||
|  | @ -259,7 +260,8 @@ library.add( | |||
| 	farHdd, | ||||
| 
 | ||||
| 	fabTwitter, | ||||
| 	fabGithub | ||||
| 	fabGithub, | ||||
| 	fabDiscord | ||||
| ); | ||||
| //#endregion
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -140,6 +140,19 @@ | |||
| 				</section> | ||||
| 			</ui-card> | ||||
| 
 | ||||
| 			<ui-card> | ||||
| 				<div slot="title"><fa :icon="['fab', 'discord']"/> {{ $t('discord') }}</div> | ||||
| 
 | ||||
| 				<section> | ||||
| 					<p class="account" v-if="$store.state.i.discord"><a :href="`https://discordapp.com/users/${$store.state.i.discord.id}`" target="_blank">@{{ $store.state.i.discord.username }}#{{ $store.state.i.discord.discriminator }}</a></p> | ||||
| 					<p> | ||||
| 						<a :href="`${apiUrl}/connect/discord`" target="_blank">{{ $store.state.i.discord ? this.$t('discord-reconnect') : this.$t('discord-connect') }}</a> | ||||
| 						<span v-if="$store.state.i.discord"> or </span> | ||||
| 						<a :href="`${apiUrl}/disconnect/discord`" target="_blank" v-if="$store.state.i.discord">{{ $t('discord-disconnect') }}</a> | ||||
| 					</p> | ||||
| 				</section> | ||||
| 			</ui-card> | ||||
| 
 | ||||
| 			<x-api-settings /> | ||||
| 
 | ||||
| 			<ui-card> | ||||
|  |  | |||
|  | @ -15,6 +15,7 @@ const defaultMeta: any = { | |||
| 	maxNoteTextLength: 1000, | ||||
| 	enableTwitterIntegration: false, | ||||
| 	enableGithubIntegration: false, | ||||
| 	enableDiscordIntegration: false | ||||
| }; | ||||
| 
 | ||||
| export default async function(): Promise<IMeta> { | ||||
|  |  | |||
|  | @ -191,4 +191,8 @@ export type IMeta = { | |||
| 	enableGithubIntegration?: boolean; | ||||
| 	githubClientId?: string; | ||||
| 	githubClientSecret?: string; | ||||
| 
 | ||||
| 	enableDiscordIntegration?: boolean; | ||||
| 	discordClientId?: string; | ||||
| 	discordClientSecret?: string; | ||||
| }; | ||||
|  |  | |||
|  | @ -88,6 +88,14 @@ export interface ILocalUser extends IUserBase { | |||
| 		id: string; | ||||
| 		login: string; | ||||
| 	}; | ||||
| 	discord: { | ||||
| 		accessToken: string; | ||||
| 		refreshToken: string; | ||||
| 		expiresDate: number; | ||||
| 		id: string; | ||||
| 		username: string; | ||||
| 		discriminator: string; | ||||
| 	}; | ||||
| 	line: { | ||||
| 		userId: string; | ||||
| 	}; | ||||
|  | @ -291,6 +299,11 @@ export const pack = ( | |||
| 		if (_user.github) { | ||||
| 			delete _user.github.accessToken; | ||||
| 		} | ||||
| 		if (_user.discord) { | ||||
| 			delete _user.discord.accessToken; | ||||
| 			delete _user.discord.refreshToken; | ||||
| 			delete _user.discord.expiresDate; | ||||
| 		} | ||||
| 		delete _user.line; | ||||
| 
 | ||||
| 		// Visible via only the official client
 | ||||
|  |  | |||
|  | @ -177,9 +177,30 @@ export const meta = { | |||
| 		githubClientSecret: { | ||||
| 			validator: $.str.optional.nullable, | ||||
| 			desc: { | ||||
| 				'ja-JP': 'GitHubアプリのClient secret' | ||||
| 				'ja-JP': 'GitHubアプリのClient Secret' | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		enableDiscordIntegration: { | ||||
| 			validator: $.bool.optional, | ||||
| 			desc: { | ||||
| 				'ja-JP': 'Discord連携機能を有効にするか否か' | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		discordClientId: { | ||||
| 			validator: $.str.optional.nullable, | ||||
| 			desc: { | ||||
| 				'ja-JP': 'DiscordアプリのClient ID' | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		discordClientSecret: { | ||||
| 			validator: $.str.optional.nullable, | ||||
| 			desc: { | ||||
| 				'ja-JP': 'DiscordアプリのClient Secret' | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
|  | @ -282,6 +303,18 @@ export default define(meta, (ps) => new Promise(async (res, rej) => { | |||
| 		set.githubClientSecret = ps.githubClientSecret; | ||||
| 	} | ||||
| 
 | ||||
| 	if (ps.enableDiscordIntegration !== undefined) { | ||||
| 		set.enableDiscordIntegration = ps.enableDiscordIntegration; | ||||
| 	} | ||||
| 
 | ||||
| 	if (ps.discordClientId !== undefined) { | ||||
| 		set.discordClientId = ps.discordClientId; | ||||
| 	} | ||||
| 
 | ||||
| 	if (ps.discordClientSecret !== undefined) { | ||||
| 		set.discordClientSecret = ps.discordClientSecret; | ||||
| 	} | ||||
| 
 | ||||
| 	await Meta.update({}, { | ||||
| 		$set: set | ||||
| 	}, { upsert: true }); | ||||
|  |  | |||
|  | @ -79,6 +79,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { | |||
| 			objectStorage: config.drive && config.drive.storage === 'minio', | ||||
| 			twitter: instance.enableTwitterIntegration, | ||||
| 			github: instance.enableGithubIntegration, | ||||
| 			discord: instance.enableDiscordIntegration, | ||||
| 			serviceWorker: config.sw ? true : false, | ||||
| 			userRecommendation: config.user_recommendation ? config.user_recommendation : {} | ||||
| 		}; | ||||
|  | @ -94,6 +95,9 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { | |||
| 		response.enableGithubIntegration = instance.enableGithubIntegration; | ||||
| 		response.githubClientId = instance.githubClientId; | ||||
| 		response.githubClientSecret = instance.githubClientSecret; | ||||
| 		response.enableDiscordIntegration = instance.enableDiscordIntegration; | ||||
| 		response.discordClientId = instance.discordClientId; | ||||
| 		response.discordClientSecret = instance.discordClientSecret; | ||||
| 	} | ||||
| 
 | ||||
| 	res(response); | ||||
|  |  | |||
|  | @ -43,6 +43,7 @@ endpoints.forEach(endpoint => endpoint.meta.requireFile | |||
| router.post('/signup', require('./private/signup').default); | ||||
| router.post('/signin', require('./private/signin').default); | ||||
| 
 | ||||
| router.use(require('./service/discord').routes()); | ||||
| router.use(require('./service/github').routes()); | ||||
| router.use(require('./service/github-bot').routes()); | ||||
| router.use(require('./service/twitter').routes()); | ||||
|  |  | |||
							
								
								
									
										306
									
								
								src/server/api/service/discord.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										306
									
								
								src/server/api/service/discord.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,306 @@ | |||
| import * as Koa from 'koa'; | ||||
| import * as Router from 'koa-router'; | ||||
| import * as request from 'request'; | ||||
| import { OAuth2 } from 'oauth'; | ||||
| 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'; | ||||
| import fetchMeta from '../../../misc/fetch-meta'; | ||||
| 
 | ||||
| function getUserToken(ctx: Koa.Context) { | ||||
| 	return ((ctx.headers['cookie'] || '').match(/i=(!\w+)/) || [null, null])[1]; | ||||
| } | ||||
| 
 | ||||
| function compareOrigin(ctx: Koa.Context) { | ||||
| 	function normalizeUrl(url: string) { | ||||
| 		return url ? url.endsWith('/') ? url.substr(0, url.length - 1) : url : ''; | ||||
| 	} | ||||
| 
 | ||||
| 	const referer = ctx.headers['referer']; | ||||
| 
 | ||||
| 	return (normalizeUrl(referer) == normalizeUrl(config.url)); | ||||
| } | ||||
| 
 | ||||
| // Init router
 | ||||
| const router = new Router(); | ||||
| 
 | ||||
| router.get('/disconnect/discord', async ctx => { | ||||
| 	if (!compareOrigin(ctx)) { | ||||
| 		ctx.throw(400, 'invalid origin'); | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	const userToken = getUserToken(ctx); | ||||
| 	if (!userToken) { | ||||
| 		ctx.throw(400, 'signin required'); | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	const user = await User.findOneAndUpdate({ | ||||
| 		host: null, | ||||
| 		'token': userToken | ||||
| 	}, { | ||||
| 		$set: { | ||||
| 			'discord': null | ||||
| 		} | ||||
| 	}); | ||||
| 
 | ||||
| 	ctx.body = `Discordの連携を解除しました :v:`; | ||||
| 
 | ||||
| 	// Publish i updated event
 | ||||
| 	publishMainStream(user._id, 'meUpdated', await pack(user, user, { | ||||
| 		detail: true, | ||||
| 		includeSecrets: true | ||||
| 	})); | ||||
| }); | ||||
| 
 | ||||
| async function getOAuth2() { | ||||
| 	const meta = await fetchMeta(); | ||||
| 
 | ||||
| 	if (meta.enableDiscordIntegration) { | ||||
| 		return new OAuth2( | ||||
| 			meta.discordClientId, | ||||
| 			meta.discordClientSecret, | ||||
| 			'https://discordapp.com/', | ||||
| 			'api/oauth2/authorize', | ||||
| 			'api/oauth2/token'); | ||||
| 	} else { | ||||
| 		return null; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| router.get('/connect/discord', 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/dc/cb`, | ||||
| 		scope: ['identify'], | ||||
| 		state: uuid(), | ||||
| 		response_type: 'code' | ||||
| 	}; | ||||
| 
 | ||||
| 	redis.set(userToken, JSON.stringify(params)); | ||||
| 
 | ||||
| 	const oauth2 = await getOAuth2(); | ||||
| 	ctx.redirect(oauth2.getAuthorizeUrl(params)); | ||||
| }); | ||||
| 
 | ||||
| router.get('/signin/discord', async ctx => { | ||||
| 	const sessid = uuid(); | ||||
| 
 | ||||
| 	const params = { | ||||
| 		redirect_uri: `${config.url}/api/dc/cb`, | ||||
| 		scope: ['identify'], | ||||
| 		state: uuid(), | ||||
| 		response_type: 'code' | ||||
| 	}; | ||||
| 
 | ||||
| 	const expires = 1000 * 60 * 60; // 1h
 | ||||
| 	ctx.cookies.set('signin_with_discord_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)); | ||||
| 
 | ||||
| 	const oauth2 = await getOAuth2(); | ||||
| 	ctx.redirect(oauth2.getAuthorizeUrl(params)); | ||||
| }); | ||||
| 
 | ||||
| router.get('/dc/cb', async ctx => { | ||||
| 	const userToken = getUserToken(ctx); | ||||
| 
 | ||||
| 	const oauth2 = await getOAuth2(); | ||||
| 
 | ||||
| 	if (!userToken) { | ||||
| 		const sessid = ctx.cookies.get('signin_with_discord_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<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, refreshToken, expiresDate } = await new Promise<any>((res, rej) => | ||||
| 			oauth2.getOAuthAccessToken( | ||||
| 				code, | ||||
| 				{ | ||||
| 					grant_type: 'authorization_code', | ||||
| 					redirect_uri | ||||
| 				}, | ||||
| 				(err, accessToken, refreshToken, result) => { | ||||
| 					if (err) | ||||
| 						rej(err); | ||||
| 					else if (result.error) | ||||
| 						rej(result.error); | ||||
| 					else | ||||
| 					res({ | ||||
| 						accessToken, | ||||
| 						refreshToken, | ||||
| 						expiresDate: Date.now() + Number(result.expires_in) * 1000 | ||||
| 					}); | ||||
| 				})); | ||||
| 
 | ||||
| 		const { id, username, discriminator } = await new Promise<any>((res, rej) => | ||||
| 			request({ | ||||
| 				url: 'https://discordapp.com/api/users/@me', | ||||
| 				headers: { | ||||
| 					'Authorization': `Bearer ${accessToken}`, | ||||
| 					'User-Agent': config.user_agent | ||||
| 				} | ||||
| 			}, (err, response, body) => { | ||||
| 				if (err) | ||||
| 					rej(err); | ||||
| 				else | ||||
| 					res(JSON.parse(body)); | ||||
| 			})); | ||||
| 
 | ||||
| 		if (!id || !username || !discriminator) { | ||||
| 			ctx.throw(400, 'invalid session'); | ||||
| 			return; | ||||
| 		} | ||||
| 
 | ||||
| 		let user = await User.findOne({ | ||||
| 			host: null, | ||||
| 			'discord.id': id | ||||
| 		}) as ILocalUser; | ||||
| 
 | ||||
| 		if (!user) { | ||||
| 			ctx.throw(404, `@${username}#${discriminator}と連携しているMisskeyアカウントはありませんでした...`); | ||||
| 			return; | ||||
| 		} | ||||
| 
 | ||||
| 		user = await User.findOneAndUpdate({ | ||||
| 			host: null, | ||||
| 			'discord.id': id | ||||
| 		}, { | ||||
| 			$set: { | ||||
| 				discord: { | ||||
| 					accessToken, | ||||
| 					refreshToken, | ||||
| 					expiresDate, | ||||
| 					username, | ||||
| 					discriminator | ||||
| 				} | ||||
| 			} | ||||
| 		}) as ILocalUser; | ||||
| 
 | ||||
| 		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, refreshToken, expiresDate } = await new Promise<any>((res, rej) => | ||||
| 			oauth2.getOAuthAccessToken( | ||||
| 				code, | ||||
| 				{ | ||||
| 					grant_type: 'authorization_code', | ||||
| 					redirect_uri | ||||
| 				}, | ||||
| 				(err, accessToken, refreshToken, result) => { | ||||
| 					if (err) | ||||
| 						rej(err); | ||||
| 					else if (result.error) | ||||
| 						rej(result.error); | ||||
| 					else | ||||
| 						res({ | ||||
| 							accessToken, | ||||
| 							refreshToken, | ||||
| 							expiresDate: Date.now() + Number(result.expires_in) * 1000 | ||||
| 						}); | ||||
| 				})); | ||||
| 
 | ||||
| 		const { id, username, discriminator } = await new Promise<any>((res, rej) => | ||||
| 			request({ | ||||
| 				url: 'https://discordapp.com/api/users/@me', | ||||
| 				headers: { | ||||
| 					'Authorization': `Bearer ${accessToken}`, | ||||
| 					'User-Agent': config.user_agent | ||||
| 				} | ||||
| 			}, (err, response, body) => { | ||||
| 				if (err) | ||||
| 					rej(err); | ||||
| 				else | ||||
| 					res(JSON.parse(body)); | ||||
| 			})); | ||||
| 
 | ||||
| 		if (!id || !username || !discriminator) { | ||||
| 			ctx.throw(400, 'invalid session'); | ||||
| 			return; | ||||
| 		} | ||||
| 
 | ||||
| 		const user = await User.findOneAndUpdate({ | ||||
| 			host: null, | ||||
| 			token: userToken | ||||
| 		}, { | ||||
| 			$set: { | ||||
| 				discord: { | ||||
| 					accessToken, | ||||
| 					refreshToken, | ||||
| 					expiresDate, | ||||
| 					id, | ||||
| 					username, | ||||
| 					discriminator | ||||
| 				} | ||||
| 			} | ||||
| 		}); | ||||
| 
 | ||||
| 		ctx.body = `Discord: @${username}#${discriminator} を、Misskey: @${user.username} に接続しました!`; | ||||
| 
 | ||||
| 		// Publish i updated event
 | ||||
| 		publishMainStream(user._id, 'meUpdated', await pack(user, user, { | ||||
| 			detail: true, | ||||
| 			includeSecrets: true | ||||
| 		})); | ||||
| 	} | ||||
| }); | ||||
| 
 | ||||
| module.exports = router; | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue