diff options
| author | Acid Chicken (硫酸鶏) <root@acid-chicken.com> | 2018-11-15 19:15:04 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2018-11-15 19:15:04 +0900 |
| commit | 9d8f7b081d8c47027583b3085923e2128f622b98 (patch) | |
| tree | badb8b660018fd4967b946cbe65d48650b88110f /src | |
| parent | 10.51.2 (diff) | |
| download | misskey-9d8f7b081d8c47027583b3085923e2128f622b98.tar.gz misskey-9d8f7b081d8c47027583b3085923e2128f622b98.tar.bz2 misskey-9d8f7b081d8c47027583b3085923e2128f622b98.zip | |
WIP: Add Discord auth (#3239)
* Add Discord auth
* Apply review 175263424
Diffstat (limited to 'src')
| -rw-r--r-- | src/client/app/admin/views/instance.vue | 20 | ||||
| -rw-r--r-- | src/client/app/common/views/components/discord-setting.vue | 64 | ||||
| -rw-r--r-- | src/client/app/common/views/components/index.ts | 2 | ||||
| -rw-r--r-- | src/client/app/common/views/components/signin.vue | 1 | ||||
| -rw-r--r-- | src/client/app/desktop/views/components/settings.vue | 7 | ||||
| -rw-r--r-- | src/client/app/desktop/views/pages/user/user.discord.vue | 26 | ||||
| -rw-r--r-- | src/client/app/desktop/views/pages/user/user.vue | 5 | ||||
| -rw-r--r-- | src/client/app/init.ts | 4 | ||||
| -rw-r--r-- | src/client/app/mobile/views/pages/settings.vue | 13 | ||||
| -rw-r--r-- | src/misc/fetch-meta.ts | 1 | ||||
| -rw-r--r-- | src/models/meta.ts | 4 | ||||
| -rw-r--r-- | src/models/user.ts | 13 | ||||
| -rw-r--r-- | src/server/api/endpoints/admin/update-meta.ts | 35 | ||||
| -rw-r--r-- | src/server/api/endpoints/meta.ts | 4 | ||||
| -rw-r--r-- | src/server/api/index.ts | 1 | ||||
| -rw-r--r-- | src/server/api/service/discord.ts | 306 |
16 files changed, 503 insertions, 3 deletions
diff --git a/src/client/app/admin/views/instance.vue b/src/client/app/admin/views/instance.vue index e52a20d708..4c234ec260 100644 --- a/src/client/app/admin/views/instance.vue +++ b/src/client/app/admin/views/instance.vue @@ -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', diff --git a/src/client/app/common/views/components/discord-setting.vue b/src/client/app/common/views/components/discord-setting.vue new file mode 100644 index 0000000000..113df9b0ae --- /dev/null +++ b/src/client/app/common/views/components/discord-setting.vue @@ -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> diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts index eb2bc5aefb..30c38ff6c3 100644 --- a/src/client/app/common/views/components/index.ts +++ b/src/client/app/common/views/components/index.ts @@ -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); diff --git a/src/client/app/common/views/components/signin.vue b/src/client/app/common/views/components/signin.vue index e02e613750..6ea7f652d6 100644 --- a/src/client/app/common/views/components/signin.vue +++ b/src/client/app/common/views/components/signin.vue @@ -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> diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue index 99e8064cea..d7d20583e6 100644 --- a/src/client/app/desktop/views/components/settings.vue +++ b/src/client/app/desktop/views/components/settings.vue @@ -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'"> diff --git a/src/client/app/desktop/views/pages/user/user.discord.vue b/src/client/app/desktop/views/pages/user/user.discord.vue new file mode 100644 index 0000000000..30db57855a --- /dev/null +++ b/src/client/app/desktop/views/pages/user/user.discord.vue @@ -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> diff --git a/src/client/app/desktop/views/pages/user/user.vue b/src/client/app/desktop/views/pages/user/user.vue index 758a98137b..1333d313fe 100644 --- a/src/client/app/desktop/views/pages/user/user.vue +++ b/src/client/app/desktop/views/pages/user/user.vue @@ -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 { diff --git a/src/client/app/init.ts b/src/client/app/init.ts index 9896cafdd9..cc702950bf 100644 --- a/src/client/app/init.ts +++ b/src/client/app/init.ts @@ -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 diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue index fceaebc66d..08cae29a3f 100644 --- a/src/client/app/mobile/views/pages/settings.vue +++ b/src/client/app/mobile/views/pages/settings.vue @@ -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> diff --git a/src/misc/fetch-meta.ts b/src/misc/fetch-meta.ts index cf7d375229..e855097c42 100644 --- a/src/misc/fetch-meta.ts +++ b/src/misc/fetch-meta.ts @@ -15,6 +15,7 @@ const defaultMeta: any = { maxNoteTextLength: 1000, enableTwitterIntegration: false, enableGithubIntegration: false, + enableDiscordIntegration: false }; export default async function(): Promise<IMeta> { diff --git a/src/models/meta.ts b/src/models/meta.ts index a12747ea3c..34117afd25 100644 --- a/src/models/meta.ts +++ b/src/models/meta.ts @@ -191,4 +191,8 @@ export type IMeta = { enableGithubIntegration?: boolean; githubClientId?: string; githubClientSecret?: string; + + enableDiscordIntegration?: boolean; + discordClientId?: string; + discordClientSecret?: string; }; diff --git a/src/models/user.ts b/src/models/user.ts index a5863de8da..22eecb571b 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -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 diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts index 1e4ff959d9..bbae212bd7 100644 --- a/src/server/api/endpoints/admin/update-meta.ts +++ b/src/server/api/endpoints/admin/update-meta.ts @@ -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 }); diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts index b324b113c8..56386cc1f5 100644 --- a/src/server/api/endpoints/meta.ts +++ b/src/server/api/endpoints/meta.ts @@ -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); diff --git a/src/server/api/index.ts b/src/server/api/index.ts index bb8bad8bbe..1cd0028574 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -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()); diff --git a/src/server/api/service/discord.ts b/src/server/api/service/discord.ts new file mode 100644 index 0000000000..d90f39ffb3 --- /dev/null +++ b/src/server/api/service/discord.ts @@ -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; |