diff --git a/.gitignore copy b/.gitignore copy deleted file mode 100644 index 9dacc57..0000000 --- a/.gitignore copy +++ /dev/null @@ -1,8 +0,0 @@ -.idea -node_modules -build -dist -.DS_Store -tmp -target -boundle.log diff --git a/src/controllers/discord.controller.ts b/src/controllers/discord.controller.ts index 38d6a23..3a01aa1 100644 --- a/src/controllers/discord.controller.ts +++ b/src/controllers/discord.controller.ts @@ -23,7 +23,7 @@ class DiscordController extends BaseController { record.refreshToken = tokenResponse.refresh_token record.scope = tokenResponse.scope record.tokenType = tokenResponse.token_type - record.expiresIn = tokenResponse.expires_in || 0 + Date.now() + record.expiresIn = (Date.now() / 1000 + tokenResponse.expires_in) | 0 await record.save() if (tokenResponse && tokenResponse.access_token) { let uinfo = await userInfo(tokenResponse.access_token) diff --git a/src/controllers/main.controller.ts b/src/controllers/main.controller.ts index b30d485..9cbec36 100644 --- a/src/controllers/main.controller.ts +++ b/src/controllers/main.controller.ts @@ -3,11 +3,12 @@ import { ZError } from 'common/ZError' import { role, router } from 'decorators/router' import logger from 'logger/logger' import { AuthRecord, PlatEnum } from 'modules/AuthRecord' -import { DiscordSvr } from 'services/discord.svr' +import { DiscordSvr, getAvableAccessToken, userGuildMember } from 'services/discord.svr' import { hmacsha256 } from 'utils/security.util' const checkSign = (params: { address?: string; sign?: string }) => { - const { address, sign } = params + let { address, sign } = params + address = address.toLowerCase() if (!address || !sign) { throw new ZError(10, 'invalid params') } @@ -119,21 +120,49 @@ class MainController extends BaseController { return result } + /** + * 检查是否加入某guid + */ @role(ROLE_ANON) - @router('get /activity/discord/svr/:id') + @router('post /activity/discord/guild') async checkDiscordJoin(req) { - let { id } = req.params + let { gid, address } = req.params checkSign(req.params) - let verified = new DiscordSvr().checkUser(id) - return { verified } + address = address.toLowerCase() + let record = await AuthRecord.findOne({ address, platform: PlatEnum.DISCORD }) + if (!record) { + throw new ZError(12, 'not found discord record') + } + let accessToken = await getAvableAccessToken(record) + // TODO:: check local cache + let data = await userGuildMember(accessToken, gid) + if (!data || data.code) { + throw new ZError(13, 'not found guild') + } + return { result: true } } - + /** + * 检查是否有某个role + */ @role(ROLE_ANON) - @router('get /activity/discord/role/:id') + @router('post /activity/discord/role') async checkDiscordRole(req) { - let { id } = req.params + let { rid, gid, address } = req.params checkSign(req.params) - let verified = new DiscordSvr().checkUserRole(id) - return { verified } + address = address.toLowerCase() + let record = await AuthRecord.findOne({ address, platform: PlatEnum.DISCORD }) + if (!record) { + throw new ZError(12, 'not found discord record') + } + let accessToken = await getAvableAccessToken(record) + console.log('discord access token:', accessToken) + // TODO:: check local cache + let data = await userGuildMember(accessToken, gid) + console.log(data?.roles) + if (!data || data.code) { + throw new ZError(13, 'not found guild') + } + let roleSet = new Set(data.roles) + return { result: roleSet.has(rid) } } } diff --git a/src/controllers/twitter.controller.ts b/src/controllers/twitter.controller.ts index 3c396e5..2097a4b 100644 --- a/src/controllers/twitter.controller.ts +++ b/src/controllers/twitter.controller.ts @@ -24,7 +24,7 @@ class TwitterController extends BaseController { record.refreshToken = tokenResponse.refresh_token record.scope = tokenResponse.scope record.tokenType = tokenResponse.token_type - record.expiresIn = tokenResponse.expires_in || 0 + Date.now() + record.expiresIn = (Date.now() / 1000 + tokenResponse.expires_in || 0) | 0 await record.save() if (tokenResponse && tokenResponse.access_token) { const uinfo = await getTwitterUserInfo(tokenResponse.access_token) diff --git a/src/services/discord.svr.ts b/src/services/discord.svr.ts index 6e359dc..5f09e25 100644 --- a/src/services/discord.svr.ts +++ b/src/services/discord.svr.ts @@ -2,6 +2,11 @@ import { ZError } from 'common/ZError' import { singleton } from 'decorators/singleton' import { Client, Events, GatewayIntentBits, Guild } from 'discord.js' +import { AuthRecordClass } from 'modules/AuthRecord' +import { DocumentType } from '@typegoose/typegoose' + +const DISCORD_API_HOST = 'https://discord.com/api/v10' +const DISCORD_APP_HOST = 'https://discordapp.com/api' export async function exchangeDiscrodCodeForToken(code: string) { const clientId = process.env.DISCORD_CLIENT_ID @@ -19,7 +24,6 @@ export async function exchangeDiscrodCodeForToken(code: string) { client_secret: clientSecret, redirect_uri: redirectUri, code, - scope: 'identify email', }), }) @@ -27,15 +31,102 @@ export async function exchangeDiscrodCodeForToken(code: string) { return data } +export async function getAvableAccessToken(record: DocumentType) { + if (record.expiresIn < Date.now() / 1000 + 60 * 5) { + console.log('discord access token expired') + const data = await refreshDiscordToken(record.refreshToken) + if (data.code) { + throw new ZError(10, 'refresh token error') + } + record.accessToken = data.access_token + record.refreshToken = data.refresh_token + record.expiresIn = (Date.now() / 1000 + data.expires_in || 0) | 0 + await record.save() + } + return record.accessToken +} +export async function refreshDiscordToken(rtoken: string) { + const clientId = process.env.DISCORD_CLIENT_ID + const clientSecret = process.env.DISCORD_CLIENT_SECRET + const response = await fetch('https://discord.com/api/oauth2/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + client_id: clientId, + client_secret: clientSecret, + refresh_token: rtoken, + }), + }) + const data = await response.json() + return data +} +/** + * 用户基本信息 + * @param token + * @returns + */ export async function userInfo(token: string) { - const response = await fetch('https://discord.com/api/users/@me', { + const response = await fetch(`${DISCORD_APP_HOST}/users/@me`, { headers: { authorization: `Bearer ${token}`, }, }) const data = await response.json() - console.log(data) + return data +} +/** + * 用户所在的工会列表, oauth2方式 + * @param token + * @returns + */ +export async function userGuildList(token: string) { + const url = `${DISCORD_APP_HOST}/users/@me/guilds` + const response = await fetch(url, { + headers: { + authorization: `Bearer ${token}`, + }, + }) + const data = await response.json() + return data +} + +/** + * 用户在某个工会的信息, oauth2方式 + * @param token + * @param guildId + * @returns + * 返回的数据结构: + * https://discord.com/developers/docs/resources/guild#guild-member-object + */ +export async function userGuildMember(token: string, guildId: string) { + const url = `${DISCORD_APP_HOST}/users/@me/guilds/${guildId}/member` + const response = await fetch(url, { + headers: { + authorization: `Bearer ${token}`, + }, + }) + const data = await response.json() + return data +} + +/** + * 直接通过discord接口, 获取工会中某用户信息 + * 返回的数据结构: + * https://discord.com/developers/docs/resources/guild#guild-member-object + */ +export async function getGuildMember(uid: string) { + const url = `${DISCORD_API_HOST}/guilds/${process.env.DISCROD_GUILD_ID}/members/${uid}` + const token = process.env.DISCORD_BOT_TOKEN + const response = await fetch(url, { + headers: { + authorization: `Bot ${token}`, + }, + }) + const data = await response.json() return data }