diff --git a/src/checkredis.ts b/src/checkredis.ts index 9e734ee..02c052e 100644 --- a/src/checkredis.ts +++ b/src/checkredis.ts @@ -51,7 +51,7 @@ const updateRedis2 = (key, vals) => { if (score != totalScore) { if (Math.abs(score - totalScore) > 1) { console.log(`user: ${records[i]._id}, score: ${score}, redis: ${totalScore}`) - await new ZRedisClient().zincrby(totalKey, score - totalScore, records[i]._id) + // await new ZRedisClient().zincrby(totalKey, score - totalScore, records[i]._id) } } if (i % 1000 === 0) { diff --git a/src/controllers/nft.controller.ts b/src/controllers/nft.controller.ts index 9ed35ae..9ad6a3a 100644 --- a/src/controllers/nft.controller.ts +++ b/src/controllers/nft.controller.ts @@ -33,6 +33,7 @@ if (process.env.NODE_ENV !== 'production') { link: 'https://x.com/sparky-chain', contract: '0x50a8e60041a206acaa5f844a1104896224be6f38', collection: 'Test Group01', + group: 'test_group01', guild: '1222509817411665920', role: '1230421511735738409', tier: 2, @@ -40,9 +41,10 @@ if (process.env.NODE_ENV !== 'production') { nftList.unshift({ projectName: 'Test', link: 'https://x.com/sparky-chain', - contract: '0x50a8e60041a206acaa5f844a1104896224be6f38', + contract: '0x50a8e60041a206acaa5f844a1104896224be6f38_1', collection: 'Test Group02', guild: '1222509817411665920', + group: 'test_group01', role: '1230421511735738409', tier: 2, }) diff --git a/src/controllers/tasks.controller.ts b/src/controllers/tasks.controller.ts index 8abaf09..6be6d4f 100644 --- a/src/controllers/tasks.controller.ts +++ b/src/controllers/tasks.controller.ts @@ -210,10 +210,6 @@ export default class TasksController extends BaseController { if (!chainRecord) { throw new ZError(14, 'waiting for chain confirm') } - // let result = await fetchClaimStatus(user.address.toLowerCase(), task) - // if (!result) { - // throw new ZError(15, 'waiting for chain confirm') - // } } const Task = allTasks.get(currentTask.task) @@ -221,8 +217,8 @@ export default class TasksController extends BaseController { const { score } = await taskInstance.claimReward(currentTask) const baseTaskSet = new Set(activity.baseTasks) let count = 0 - for (let task of user.taskProgress) { - if (baseTaskSet.has(task.id) && task.status === TaskStatusEnum.CLAIMED) { + for (let _task of user.taskProgress) { + if (baseTaskSet.has(_task.id) && _task.status === TaskStatusEnum.CLAIMED) { count++ } } diff --git a/src/models/oauth/AuthRecord.ts b/src/models/oauth/AuthRecord.ts new file mode 100644 index 0000000..c2a8052 --- /dev/null +++ b/src/models/oauth/AuthRecord.ts @@ -0,0 +1,74 @@ +import { getModelForClass, index, modelOptions, mongoose, prop, ReturnModelType, Severity } from '@typegoose/typegoose' +import { dbconn } from 'decorators/dbconn' +import { BaseModule } from '../Base' + +export enum PlatEnum { + GOOGLE = 0, + APPLE = 1, + TIKTOK = 2, + FACEBOOK = 3, + TWITTER = 4, + TELEGRAM = 5, + EMAIL = 6, + DISCORD = 7, +} + +@dbconn('oauth') +@modelOptions({ + schemaOptions: { collection: 'auth_record', timestamps: true }, + options: { allowMixed: Severity.ALLOW }, +}) +export class AuthRecordClass extends BaseModule { + @prop({ required: true }) + public address: string + + @prop({ enum: PlatEnum, default: PlatEnum.DISCORD }) + public platform: PlatEnum + + @prop() + public nickname?: string + + @prop() + public username?: string + @prop() + public avatar?: string + // 对应discord上的discriminator + @prop() + public discriminator?: string + + @prop() + public openId?: string + + @prop() + public email?: string + + @prop({ default: 0 }) + public condition: number + + @prop({ default: 0 }) + public version: number + + @prop() + public tokenType: string + @prop() + public accessToken: string + @prop() + public refreshToken: string + @prop() + public expiresIn: number + @prop() + public scope: string + + @prop({ type: mongoose.Schema.Types.Mixed }) + public outData: any + + public static async findByAddress( + this: ReturnModelType, + address: string, + platform: PlatEnum, + ) { + return this.findOne({ address, platform }).exec() + } +} + +export const AuthRecord = getModelForClass(AuthRecordClass, { existingConnection: AuthRecordClass.db }) diff --git a/src/services/chain.svr.ts b/src/services/chain.svr.ts index 4967da0..5885623 100644 --- a/src/services/chain.svr.ts +++ b/src/services/chain.svr.ts @@ -117,7 +117,7 @@ export const fetchChainStatus = async (address: string, data: string) => { } export const fetchCheckInStatus = async (address: string) => { - const days = ((Date.now() / 1000 / 60 / 60 / 24) | 0) - 1 + const days = (Date.now() / 1000 / 60 / 60 / 24) | 0 const valStr = days.toString(16).padStart(64, '0') const addressStr = address.replace('0x', '').padStart(64, '0') const method = '86cd4926' diff --git a/src/services/discord.svr.ts b/src/services/discord.svr.ts index 8989334..f4c46ef 100644 --- a/src/services/discord.svr.ts +++ b/src/services/discord.svr.ts @@ -1,4 +1,11 @@ +import { DocumentType } from '@typegoose/typegoose' +import { AuthRecordClass } from 'models/oauth/AuthRecord' +import { timeoutFetch } from 'zutils/utils/net.util' +import { ZError } from 'zutils' const DISCORD_API_HOST = 'https://discord.com/api/v10' +const DISCORD_APP_HOST = 'https://discordapp.com/api' +const DEFAULT_TIMEOUT = 30000 + /** * 直接通过discord接口, 获取工会中某用户信息 * 返回的数据结构: @@ -10,11 +17,15 @@ const DISCORD_API_HOST = 'https://discord.com/api/v10' export async function getGuildMember(guid: string, uid: string) { const url = `${DISCORD_API_HOST}/guilds/${guid}/members/${uid}` const token = process.env.DISCORD_BOT_TOKEN - const response = await fetch(url, { - headers: { - authorization: `Bot ${token}`, + const response = await timeoutFetch( + url, + { + headers: { + authorization: `Bot ${token}`, + }, }, - }) + DEFAULT_TIMEOUT, + ) const data = await response.json() return data } @@ -69,3 +80,63 @@ export async function checkGotRole(guid: string, uid: string, roleId: string) { return false } } + +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(30, '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 timeoutFetch( + '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, + }), + }, + DEFAULT_TIMEOUT, + ) + 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 timeoutFetch( + url, + { + headers: { + authorization: `Bearer ${token}`, + }, + }, + DEFAULT_TIMEOUT, + ) + const data = await response.json() + return data +} diff --git a/src/services/oauth.svr.ts b/src/services/oauth.svr.ts index 77f01b6..175b5a0 100644 --- a/src/services/oauth.svr.ts +++ b/src/services/oauth.svr.ts @@ -1,5 +1,8 @@ import { hmacSha256 } from 'zutils/utils/security.util' import { handleFetch, timeoutFetch } from 'zutils/utils/net.util' +import { AuthRecord, PlatEnum } from 'models/oauth/AuthRecord' +import { ZError, ZRedisClient } from 'zutils' +import { getAvableAccessToken, userGuildMember } from './discord.svr' const SECRET_KEY = process.env.HASH_SALT const DEFAULT_TIMEOUT = 30000 @@ -11,15 +14,36 @@ function createSign(address: string) { } export async function checkTwitter(address: string) { - let sign = createSign(address) - const url = `${process.env.OAUTH_SVR_URL}/activity/twitter/${address}?sign=${sign}` - return handleFetch(url) + let record = await AuthRecord.findOne({ address: address.toLowerCase(), platform: PlatEnum.TWITTER }) + let result: any = {} + let errcode = 0 + let errmsg = '' + if (!record) { + errcode = 20 + errmsg = 'not bind' + } else { + result.username = record.username + result.userid = record.openId + result.avatar = record.avatar + } + return { data: result, errcode, errmsg } } export async function checkDiscord(address: string) { - let sign = createSign(address) - const url = `${process.env.OAUTH_SVR_URL}/activity/discord/${address}?sign=${sign}` - return handleFetch(url) + let record = await AuthRecord.findOne({ address: address.toLowerCase(), platform: PlatEnum.DISCORD }) + let result: any = {} + let errcode = 0 + let errmsg = '' + if (!record) { + errcode = 20 + errmsg = 'not bind' + } else { + result.verified = record.condition + result.username = record.username + result.userid = record.openId + result.discriminator = record.discriminator + } + return { data: result, errcode, errmsg } } export async function checkDiscordGuid(address: string, gid: string) { @@ -41,20 +65,30 @@ export async function checkDiscordGuid(address: string, gid: string) { } export async function checkDiscordRole(address: string, gid: string, rid: string) { - let sign = createSign(address) - const data = { - gid, - address, - rid, - sign, + let record = await AuthRecord.findOne({ address, platform: PlatEnum.DISCORD }) + if (!record) { + throw new ZError(20, 'need connect discord first') } - const url = `${process.env.OAUTH_SVR_URL}/activity/discord/role` - const options: any = { - method: 'POST', - headers: { - 'Content-Type': 'application/json; charset=utf-8', - }, - body: JSON.stringify(data), + const key = `oauth:${gid}:${address}` + const cache = await new ZRedisClient().get(key) + let roleSet = new Set() + if (cache) { + try { + let roleArr = JSON.parse(cache) + roleSet = new Set(roleArr) + if (roleSet.has(rid)) { + return { data: { result: true }, errcode: 0, errmsg: '' } + } + } catch (e) { + console.error(e) + } } - return timeoutFetch(url, options, DEFAULT_TIMEOUT).then(res => res.json()) + let accessToken = await getAvableAccessToken(record) + let data = await userGuildMember(accessToken, gid) + roleSet = new Set(data.roles) + let result = roleSet.has(rid) + new ZRedisClient().pub.set(key, JSON.stringify([...roleSet]), 'EX', 300, () => { + console.log('cache set success: ', key) + }) + return { data: { result }, errcode: 0, errmsg: '' } }