mirror of
				https://codeberg.org/yeentown/barkey.git
				synced 2025-10-31 05:24:13 +00:00 
			
		
		
		
	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: "または" |   or: "または" | ||||||
|   signin-with-twitter: "Twitterでログイン" |   signin-with-twitter: "Twitterでログイン" | ||||||
|   signin-with-github: "GitHubでログイン" |   signin-with-github: "GitHubでログイン" | ||||||
|  |   signin-with-discord: "Discordでログイン" | ||||||
|   login-failed: "ログインできませんでした。ユーザー名とパスワードを確認してください。" |   login-failed: "ログインできませんでした。ユーザー名とパスワードを確認してください。" | ||||||
| 
 | 
 | ||||||
| common/views/components/signup.vue: | common/views/components/signup.vue: | ||||||
|  | @ -450,6 +451,14 @@ common/views/components/github-setting.vue: | ||||||
|   connect: "GitHubと接続する" |   connect: "GitHubと接続する" | ||||||
|   disconnect: "切断する" |   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: | common/views/components/uploader.vue: | ||||||
|   waiting: "待機中" |   waiting: "待機中" | ||||||
| 
 | 
 | ||||||
|  | @ -1081,7 +1090,12 @@ admin/views/instance.vue: | ||||||
|   github-integration-info: "コールバックURLは /api/gh/cb に設定します。" |   github-integration-info: "コールバックURLは /api/gh/cb に設定します。" | ||||||
|   enable-github-integration: "GitHub連携を有効にする" |   enable-github-integration: "GitHub連携を有効にする" | ||||||
|   github-integration-client-id: "Client ID" |   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-config: "プロキシアカウントの設定" | ||||||
|   proxy-account-info: "プロキシアカウントは、特定の条件下でユーザーのリモートフォローを代行するアカウントです。例えば、ユーザーがリモートユーザーをリストに入れたとき、リストに入れられたユーザーを誰もフォローしていないとアクティビティがサーバーに配達されないため、代わりにプロキシアカウントがフォローするようにします。" |   proxy-account-info: "プロキシアカウントは、特定の条件下でユーザーのリモートフォローを代行するアカウントです。例えば、ユーザーがリモートユーザーをリストに入れたとき、リストに入れられたユーザーを誰もフォローしていないとアクティビティがサーバーに配達されないため、代わりにプロキシアカウントがフォローするようにします。" | ||||||
|   proxy-account-username: "プロキシアカウントのユーザー名" |   proxy-account-username: "プロキシアカウントのユーザー名" | ||||||
|  | @ -1530,6 +1544,10 @@ mobile/views/pages/settings.vue: | ||||||
|   github-connect: "GitHubアカウントに接続する" |   github-connect: "GitHubアカウントに接続する" | ||||||
|   github-reconnect: "再接続する" |   github-reconnect: "再接続する" | ||||||
|   github-disconnect: "切断する" |   github-disconnect: "切断する" | ||||||
|  |   discord: "Discord連携" | ||||||
|  |   discord-connect: "Discordアカウントに接続する" | ||||||
|  |   discord-reconnect: "再接続する" | ||||||
|  |   discord-disconnect: "切断する" | ||||||
|   update: "Misskey Update" |   update: "Misskey Update" | ||||||
|   version: "バージョン:" |   version: "バージョン:" | ||||||
|   latest-version: "最新のバージョン:" |   latest-version: "最新のバージョン:" | ||||||
|  |  | ||||||
|  | @ -76,6 +76,17 @@ | ||||||
| 			<ui-button @click="updateMeta">{{ $t('save') }}</ui-button> | 			<ui-button @click="updateMeta">{{ $t('save') }}</ui-button> | ||||||
| 		</section> | 		</section> | ||||||
| 	</ui-card> | 	</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> | </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  | @ -113,6 +124,9 @@ export default Vue.extend({ | ||||||
| 			enableGithubIntegration: false, | 			enableGithubIntegration: false, | ||||||
| 			githubClientId: null, | 			githubClientId: null, | ||||||
| 			githubClientSecret: null, | 			githubClientSecret: null, | ||||||
|  | 			enableDiscordIntegration: false, | ||||||
|  | 			discordClientId: null, | ||||||
|  | 			discordClientSecret: null, | ||||||
| 			proxyAccount: null, | 			proxyAccount: null, | ||||||
| 			inviteCode: null, | 			inviteCode: null, | ||||||
| 			faHeadset, faShieldAlt, faGhost | 			faHeadset, faShieldAlt, faGhost | ||||||
|  | @ -141,6 +155,9 @@ export default Vue.extend({ | ||||||
| 			this.enableGithubIntegration = meta.enableGithubIntegration; | 			this.enableGithubIntegration = meta.enableGithubIntegration; | ||||||
| 			this.githubClientId = meta.githubClientId; | 			this.githubClientId = meta.githubClientId; | ||||||
| 			this.githubClientSecret = meta.githubClientSecret; | 			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, | 				enableGithubIntegration: this.enableGithubIntegration, | ||||||
| 				githubClientId: this.githubClientId, | 				githubClientId: this.githubClientId, | ||||||
| 				githubClientSecret: this.githubClientSecret, | 				githubClientSecret: this.githubClientSecret, | ||||||
|  | 				enableDiscordIntegration: this.enableDiscordIntegration, | ||||||
|  | 				discordClientId: this.discordClientId, | ||||||
|  | 				discordClientSecret: this.discordClientSecret | ||||||
| 			}).then(() => { | 			}).then(() => { | ||||||
| 				this.$root.alert({ | 				this.$root.alert({ | ||||||
| 					type: 'success', | 					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 urlPreview from './url-preview.vue'; | ||||||
| import twitterSetting from './twitter-setting.vue'; | import twitterSetting from './twitter-setting.vue'; | ||||||
| import githubSetting from './github-setting.vue'; | import githubSetting from './github-setting.vue'; | ||||||
|  | import discordSetting from './discord-setting.vue'; | ||||||
| import fileTypeIcon from './file-type-icon.vue'; | import fileTypeIcon from './file-type-icon.vue'; | ||||||
| import emoji from './emoji.vue'; | import emoji from './emoji.vue'; | ||||||
| import welcomeTimeline from './welcome-timeline.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-url-preview', urlPreview); | ||||||
| Vue.component('mk-twitter-setting', twitterSetting); | Vue.component('mk-twitter-setting', twitterSetting); | ||||||
| Vue.component('mk-github-setting', githubSetting); | Vue.component('mk-github-setting', githubSetting); | ||||||
|  | Vue.component('mk-discord-setting', discordSetting); | ||||||
| Vue.component('mk-file-type-icon', fileTypeIcon); | Vue.component('mk-file-type-icon', fileTypeIcon); | ||||||
| Vue.component('mk-emoji', emoji); | Vue.component('mk-emoji', emoji); | ||||||
| Vue.component('mk-welcome-timeline', welcomeTimeline); | Vue.component('mk-welcome-timeline', welcomeTimeline); | ||||||
|  |  | ||||||
|  | @ -14,6 +14,7 @@ | ||||||
| 	<ui-button type="submit" :disabled="signing">{{ signing ? $t('signing-in') : $t('signin') }}</ui-button> | 	<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/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/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> | </form> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -30,6 +30,13 @@ | ||||||
| 					<mk-github-setting/> | 					<mk-github-setting/> | ||||||
| 				</section> | 				</section> | ||||||
| 			</ui-card> | 			</ui-card> | ||||||
|  | 
 | ||||||
|  | 			<ui-card> | ||||||
|  | 				<div slot="title"><fa :icon="['fab', 'discord']"/> {{ $t('discord') }}</div> | ||||||
|  | 				<section> | ||||||
|  | 					<mk-discord-setting/> | ||||||
|  | 				</section> | ||||||
|  | 			</ui-card> | ||||||
| 		</div> | 		</div> | ||||||
| 
 | 
 | ||||||
| 		<ui-card class="theme" v-show="page == 'theme'"> | 		<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-profile :user="user"/> | ||||||
| 				<x-twitter :user="user" v-if="!user.host && user.twitter"/> | 				<x-twitter :user="user" v-if="!user.host && user.twitter"/> | ||||||
| 				<x-github :user="user" v-if="!user.host && user.github"/> | 				<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-calendar @chosen="warp" :start="new Date(user.createdAt)"/> | ||||||
| 				<mk-activity :user="user"/> | 				<mk-activity :user="user"/> | ||||||
| 				<x-photos :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 XFriends from './user.friends.vue'; | ||||||
| import XTwitter from './user.twitter.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 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({ | export default Vue.extend({ | ||||||
| 	i18n: i18n(), | 	i18n: i18n(), | ||||||
|  | @ -50,7 +52,8 @@ export default Vue.extend({ | ||||||
| 		XFollowersYouKnow, | 		XFollowersYouKnow, | ||||||
| 		XFriends, | 		XFriends, | ||||||
| 		XTwitter, | 		XTwitter, | ||||||
| 		XGithub // ?MEM: Don't fix the intentional typo. (see L41) | 		XGithub, // ?MEM: Don't fix the intentional typo. (see L41) | ||||||
|  | 		XDiscord | ||||||
| 	}, | 	}, | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
|  |  | ||||||
|  | @ -143,6 +143,7 @@ import { | ||||||
| import { | import { | ||||||
| 	faTwitter as fabTwitter, | 	faTwitter as fabTwitter, | ||||||
| 	faGithub as fabGithub, | 	faGithub as fabGithub, | ||||||
|  | 	faDiscord as fabDiscord | ||||||
| } from '@fortawesome/free-brands-svg-icons'; | } from '@fortawesome/free-brands-svg-icons'; | ||||||
| import i18n from './i18n'; | import i18n from './i18n'; | ||||||
| 
 | 
 | ||||||
|  | @ -259,7 +260,8 @@ library.add( | ||||||
| 	farHdd, | 	farHdd, | ||||||
| 
 | 
 | ||||||
| 	fabTwitter, | 	fabTwitter, | ||||||
| 	fabGithub | 	fabGithub, | ||||||
|  | 	fabDiscord | ||||||
| ); | ); | ||||||
| //#endregion
 | //#endregion
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -140,6 +140,19 @@ | ||||||
| 				</section> | 				</section> | ||||||
| 			</ui-card> | 			</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 /> | 			<x-api-settings /> | ||||||
| 
 | 
 | ||||||
| 			<ui-card> | 			<ui-card> | ||||||
|  |  | ||||||
|  | @ -15,6 +15,7 @@ const defaultMeta: any = { | ||||||
| 	maxNoteTextLength: 1000, | 	maxNoteTextLength: 1000, | ||||||
| 	enableTwitterIntegration: false, | 	enableTwitterIntegration: false, | ||||||
| 	enableGithubIntegration: false, | 	enableGithubIntegration: false, | ||||||
|  | 	enableDiscordIntegration: false | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export default async function(): Promise<IMeta> { | export default async function(): Promise<IMeta> { | ||||||
|  |  | ||||||
|  | @ -191,4 +191,8 @@ export type IMeta = { | ||||||
| 	enableGithubIntegration?: boolean; | 	enableGithubIntegration?: boolean; | ||||||
| 	githubClientId?: string; | 	githubClientId?: string; | ||||||
| 	githubClientSecret?: string; | 	githubClientSecret?: string; | ||||||
|  | 
 | ||||||
|  | 	enableDiscordIntegration?: boolean; | ||||||
|  | 	discordClientId?: string; | ||||||
|  | 	discordClientSecret?: string; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -88,6 +88,14 @@ export interface ILocalUser extends IUserBase { | ||||||
| 		id: string; | 		id: string; | ||||||
| 		login: string; | 		login: string; | ||||||
| 	}; | 	}; | ||||||
|  | 	discord: { | ||||||
|  | 		accessToken: string; | ||||||
|  | 		refreshToken: string; | ||||||
|  | 		expiresDate: number; | ||||||
|  | 		id: string; | ||||||
|  | 		username: string; | ||||||
|  | 		discriminator: string; | ||||||
|  | 	}; | ||||||
| 	line: { | 	line: { | ||||||
| 		userId: string; | 		userId: string; | ||||||
| 	}; | 	}; | ||||||
|  | @ -291,6 +299,11 @@ export const pack = ( | ||||||
| 		if (_user.github) { | 		if (_user.github) { | ||||||
| 			delete _user.github.accessToken; | 			delete _user.github.accessToken; | ||||||
| 		} | 		} | ||||||
|  | 		if (_user.discord) { | ||||||
|  | 			delete _user.discord.accessToken; | ||||||
|  | 			delete _user.discord.refreshToken; | ||||||
|  | 			delete _user.discord.expiresDate; | ||||||
|  | 		} | ||||||
| 		delete _user.line; | 		delete _user.line; | ||||||
| 
 | 
 | ||||||
| 		// Visible via only the official client
 | 		// Visible via only the official client
 | ||||||
|  |  | ||||||
|  | @ -177,9 +177,30 @@ export const meta = { | ||||||
| 		githubClientSecret: { | 		githubClientSecret: { | ||||||
| 			validator: $.str.optional.nullable, | 			validator: $.str.optional.nullable, | ||||||
| 			desc: { | 			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; | 		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({}, { | 	await Meta.update({}, { | ||||||
| 		$set: set | 		$set: set | ||||||
| 	}, { upsert: true }); | 	}, { upsert: true }); | ||||||
|  |  | ||||||
|  | @ -79,6 +79,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { | ||||||
| 			objectStorage: config.drive && config.drive.storage === 'minio', | 			objectStorage: config.drive && config.drive.storage === 'minio', | ||||||
| 			twitter: instance.enableTwitterIntegration, | 			twitter: instance.enableTwitterIntegration, | ||||||
| 			github: instance.enableGithubIntegration, | 			github: instance.enableGithubIntegration, | ||||||
|  | 			discord: instance.enableDiscordIntegration, | ||||||
| 			serviceWorker: config.sw ? true : false, | 			serviceWorker: config.sw ? true : false, | ||||||
| 			userRecommendation: config.user_recommendation ? config.user_recommendation : {} | 			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.enableGithubIntegration = instance.enableGithubIntegration; | ||||||
| 		response.githubClientId = instance.githubClientId; | 		response.githubClientId = instance.githubClientId; | ||||||
| 		response.githubClientSecret = instance.githubClientSecret; | 		response.githubClientSecret = instance.githubClientSecret; | ||||||
|  | 		response.enableDiscordIntegration = instance.enableDiscordIntegration; | ||||||
|  | 		response.discordClientId = instance.discordClientId; | ||||||
|  | 		response.discordClientSecret = instance.discordClientSecret; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	res(response); | 	res(response); | ||||||
|  |  | ||||||
|  | @ -43,6 +43,7 @@ endpoints.forEach(endpoint => endpoint.meta.requireFile | ||||||
| router.post('/signup', require('./private/signup').default); | 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/discord').routes()); | ||||||
| router.use(require('./service/github').routes()); | router.use(require('./service/github').routes()); | ||||||
| router.use(require('./service/github-bot').routes()); | router.use(require('./service/github-bot').routes()); | ||||||
| router.use(require('./service/twitter').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
		
		Reference in a new issue