diff --git a/initdatas/activity_info.json b/initdatas/activity_info.json index 362d5e7..42e4a39 100644 --- a/initdatas/activity_info.json +++ b/initdatas/activity_info.json @@ -86,6 +86,30 @@ "pretasks": ["e2far3lj30vwcpe0mf8"], "cfg": {"icon": "discord"}, "params": {"time": 6, "failRate": 60} + }, { + "id": "e2feyflj30vwcpe0sjx", + "task": "YoutubeFollow", + "title": "Follow Youtube", + "type": 1, + "desc": "Follow Counter Fire’s official YTB account", + "category": "Social Tasks", + "score": 100, + "autoclaim": false, + "pretasks": [], + "cfg": {"icon": "youtube"}, + "params": {"time": 6, "failRate": 60} + }, { + "id": "e2feyflj30vwcpe0sjz", + "task": "YoutubePost", + "title": "Post Youtube", + "type": 1, + "desc": "Post a video introducing @_CounterFire", + "category": "Social Tasks", + "score": 100, + "autoclaim": false, + "pretasks": [], + "cfg": {"icon": "youtube"}, + "params": {"time": 6, "failRate": 60} }, { "id": "e2f7fplj30vwcpe0l98", "task": "OkxLogin", @@ -99,10 +123,10 @@ "cfg": {"account": "okx", "icon": "okx"}, "params": {} }, { - "id": "e2f7fplj30vwcpe0l98", + "id": "e2f7fplj30vwcpe0l96", "task": "DailyCheckIn", "title": "daily checkin", - "type": 1, + "type": 2, "desc": "", "category": "Special Quests", "score": 20, @@ -110,6 +134,18 @@ "pretasks": [], "cfg": {"score": [0, 15, 20, 20, 40, 40, 60]}, "params": {"days": 1, "score": [0, 15, 20, 20, 40, 40, 60]} + }, { + "id": "e2f7fplj30vwcpe0l97", + "task": "DailyCheckIn", + "title": "daily checkin", + "type": 1, + "desc": "Check-in for 3 consecutive days.", + "category": "Referral to Earn", + "score": 100, + "autoclaim": false, + "pretasks": [], + "cfg": {}, + "params": {"days": 3} }, { "id": "e2f7t4lj30vwcpe0ldr", "task": "ShareCode", @@ -120,6 +156,57 @@ "cfg": {}, "score": 100, "params": {"score": [100, 20]} + }, { + "id": "e2f7t4lj31vwcpe0ldr", + "task": "BurnNft", + "type": 1, + "show": true, + "title": "Hero NFT", + "desc": "Click to burn and redeem Flame", + "category": "CF Pal", + "autoclaim": false, + "pretasks": [], + "cfg": {"address": "0x59e751c2037B710090035B6ea928e0cce80aC03f"}, + "score": 200, + "params": {"address": "0x59e751c2037B710090035B6ea928e0cce80aC03f"} + }, { + "id": "e2f7t4lj32vwcpe0ldr", + "task": "BurnNft", + "type": 1, + "show": true, + "title": "Candy Badge", + "desc": "Click to burn and redeem Flame", + "category": "CF Pal", + "pretasks": [], + "cfg": {"address": "0x6a673D946a976776fd5F163d9d831b2fEB600015"}, + "score": 100, + "params": {"address": "0x6a673D946a976776fd5F163d9d831b2fEB600015"} + }, { + "id": "e2f7t4lj33vwcpe0ldr", + "task": "BurnNft", + "type": 1, + "show": true, + "title": "Explorer Badge", + "desc": "Click to burn and redeem Flame", + "category": "CF Pal", + "autoclaim": false, + "pretasks": [], + "cfg": {"address": "0x7b6399DFbed8Bc46F6A498C6B1040E80c2B5C4bc"}, + "score": 100, + "params": {"address": "0x7b6399DFbed8Bc46F6A498C6B1040E80c2B5C4bc"} + }, { + "id": "e2f7t4lj33vwcpe0ldr", + "task": "BurnNft", + "type": 1, + "show": true, + "title": "Gacha Badge", + "desc": "Click to burn and redeem Flame", + "category": "CF Pal", + "autoclaim": false, + "pretasks": [], + "cfg": {"address": "0xe2E4D5a4045fBFcbCBECAf5b8A94303712d2FA97"}, + "score": 200, + "params": {"address": "0xe2E4D5a4045fBFcbCBECAf5b8A94303712d2FA97"} }], "startTime": 1702628292366, "endTime": 1705220292366 diff --git a/src/models/NFTBrunRecord.ts b/src/models/NFTBrunRecord.ts new file mode 100644 index 0000000..06026fd --- /dev/null +++ b/src/models/NFTBrunRecord.ts @@ -0,0 +1,31 @@ +import { Severity, getModelForClass, index, modelOptions, mongoose, prop } from '@typegoose/typegoose' +import { dbconn } from 'decorators/dbconn' +import { BaseModule } from './Base' + + +@dbconn() +@index({ user: 1, chain: 1, address: 1}, { unique: false }) +@index({ user: 1, chain: 1, address: 1, tokenId: 1 }, { unique: true }) +@modelOptions({ schemaOptions: { collection: 'nft_burn_record', timestamps: true }, options: { allowMixed: Severity.ALLOW } }) +class NftBurnRecordClass extends BaseModule { + @prop({ required: true}) + public user: string + + @prop() + public chain: number + + @prop({ required: true}) + public address: string + + @prop() + public tokenId: number + + @prop() + public activity: string + @prop() + public task: string + +} + +export const NftBurnRecord = getModelForClass(NftBurnRecordClass, { existingConnection: NftBurnRecordClass['db'] }) + diff --git a/src/services/chain.svr.ts b/src/services/chain.svr.ts index f36163d..fb3f217 100644 --- a/src/services/chain.svr.ts +++ b/src/services/chain.svr.ts @@ -1,5 +1,5 @@ -export const queryCheckInList = async (address: string, days: string | number | string[], max: number = 0) => { +export const queryCheckInList = async (address: string, days: string | number | string[], limit: number = 0) => { const url = process.env.CHAIN_SVR + '/task/check_in' return fetch(url, { method: 'POST', @@ -7,7 +7,31 @@ export const queryCheckInList = async (address: string, days: string | number | body: JSON.stringify({ address, days, - max + limit + }) + }).then((res) => res.json()) +} + +export const queryCheckInSeq = async (address: string) =>{ + const url = process.env.CHAIN_SVR + '/task/check_in/max_seq' + return fetch(url, { + method: 'POST', + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({ + address, + }) + }).then((res) => res.json()) +} + +export const queryBurnNftList = async (address: string, user: string, chain: number) => { + const url = process.env.CHAIN_SVR + '/task/nft/checkburn' + return fetch(url, { + method: 'POST', + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({ + address, + user, + chain }) }).then((res) => res.json()) } \ No newline at end of file diff --git a/src/tasks/BurnNft.ts b/src/tasks/BurnNft.ts new file mode 100644 index 0000000..56047ef --- /dev/null +++ b/src/tasks/BurnNft.ts @@ -0,0 +1,64 @@ +import { ITask } from "./base/ITask"; +import { TaskStatusEnum } from "models/ActivityUser"; +import { ZError } from "common/ZError"; +import { TaskCfg } from "models/ActivityInfo"; +import { queryBurnNftList } from "services/chain.svr"; +import { NftBurnRecord } from "models/NFTBrunRecord"; + +export default class BurnNft extends ITask { + static desc = 'Butn NFT' + static show: boolean = true + async execute(data: any) { + const { task } = data + let cfg = this.params.activity.tasks.find((t: TaskCfg) => t.id === task.id) + const address = cfg.params.address.toLowerCase() + const chain = parseInt(process.env.CHAIN) + const res = await queryBurnNftList(address, this.params.user.address, chain) + if (res.errcode) { + throw new ZError(res.errcode, res.errmsg) + } + const nftList = res.data + const localNft = await NftBurnRecord.find({ + user: this.params.user.id, + chain, + address + }) + const localNftSet = new Set(); + for (let nft of localNft) { + localNftSet.add(nft.tokenId) + } + let finishNft + for (let nft of nftList) { + if (!localNftSet.has(nft.tokenId)) { + finishNft = nft.tokenId + break; + } + } + const record = new NftBurnRecord({ + user: this.params.user.id, + chain, + address, + tokenId: finishNft, + activity: this.params.activity.id, + task: task.id + }) + await record.save() + task.status = TaskStatusEnum.SUCCESS + task.timeFinish = Date.now() + task.data = {tokenId: finishNft} + try { + await this.params.user.save() + } catch(err) { + throw new ZError(100, 'save failed') + } + if (cfg.autoclaim) { + try { + await this.claimReward(task); + } catch(err) { + console.log(err) + } + } + return true + } + +} \ No newline at end of file diff --git a/src/tasks/DailyCheckIn.ts b/src/tasks/DailyCheckIn.ts index b713f90..aa580c4 100644 --- a/src/tasks/DailyCheckIn.ts +++ b/src/tasks/DailyCheckIn.ts @@ -1,8 +1,8 @@ import { ITask } from "./base/ITask"; import { ZError } from "common/ZError"; import { TaskStatus, TaskStatusEnum } from "models/ActivityUser"; -import { TaskCfg } from "models/ActivityInfo"; -import { queryCheckInList } from "services/chain.svr"; +import { TaskCfg, TaskTypeEnum } from "models/ActivityInfo"; +import { queryCheckInList, queryCheckInSeq } from "services/chain.svr"; import { updateRankScore } from "services/rank.svr"; // TODO:: test @@ -22,12 +22,17 @@ export default class DailyCheckIn extends ITask { let cfg = this.params.activity.tasks.find((t: TaskCfg) => t.id === task.id) const days = cfg.params.days || 1 const limit = cfg.params.limit || 0 - const res = await queryCheckInList(address, days - 1, limit) + const res = cfg.type === TaskTypeEnum.DAILY ? + await queryCheckInList(address, days - 1, limit) + : await queryCheckInSeq(address) if (res.errcode) { throw new ZError(res.errcode, res.errmsg) } + let success = false + - if (task.status === TaskStatusEnum.RUNNING && res.data.length >= days) { + if ((cfg.type === TaskTypeEnum.DAILY && task.status === TaskStatusEnum.RUNNING && res.data.length >= days) + || (cfg.type === TaskTypeEnum.ONCE && task.status === TaskStatusEnum.RUNNING && res.data.count >= days)) { task.status = TaskStatusEnum.SUCCESS task.timeFinish = Date.now() task.data = res.data @@ -48,33 +53,37 @@ export default class DailyCheckIn extends ITask { } public async claimReward(task: TaskStatus) { - // await super.claimReward(task); // 增加连续签到奖励分 // 请求前7天的签到记录, 往前查找连续签到的记录, - const res = await queryCheckInList(this.params.user.address, 1, 0) - const [taskId, dateTag] = task.id.split(':'); - let list: { day: string, time: number, count: number }[] = res.data; let cfg = this.params.activity.tasks.find((t: TaskCfg) => t.id === task.id) - const countCfg = cfg.params.score.length; - let count = list.length > 0 ? list[0].count : 0; - let seq = count % countCfg; - let score = cfg.params.score[seq] + cfg.score; - const user = this.params.user - if (user.boost > 1 && Date.now() < user.boostExpire) { - score = Math.floor(score * user.boost) - } - await updateRankScore({ - user: this.params.user.id, - score: score, - activity: this.params.user.activity, - scoreType: cfg.task, - scoreParams: { - taskId: task.id, - date: dateTag, - bouns: score, - boost: user.boost, + if (cfg.type === TaskTypeEnum.DAILY) { + const res = await queryCheckInList(this.params.user.address, 1, 0) + const [taskId, dateTag] = task.id.split(':'); + let list: { day: string, time: number, count: number }[] = res.data; + + const countCfg = cfg.params.score.length; + let count = list.length > 0 ? list[0].count : 0; + let seq = count % countCfg; + let score = cfg.params.score[seq] || 0 + cfg.score; + const user = this.params.user + if (user.boost > 1 && Date.now() < user.boostExpire) { + score = Math.floor(score * user.boost) } - }) + await updateRankScore({ + user: this.params.user.id, + score: score, + activity: this.params.user.activity, + scoreType: cfg.task, + scoreParams: { + taskId: task.id, + date: dateTag, + bouns: score, + boost: user.boost, + } + }) + } else { + super.claimReward(task); + } } } \ No newline at end of file diff --git a/src/tasks/YoutubeFollow.ts b/src/tasks/YoutubeFollow.ts new file mode 100644 index 0000000..a7dd3ce --- /dev/null +++ b/src/tasks/YoutubeFollow.ts @@ -0,0 +1,38 @@ +import { ITask } from "./base/ITask"; +import { TaskStatusEnum } from "models/ActivityUser"; +import { ZError } from "common/ZError"; +import { TaskCfg } from "models/ActivityInfo"; + +export default class YoutubeFollow extends ITask { + static desc = 'youtube follow' + static show: boolean = true + async execute(data: any) { + const { task } = data + let cfg = this.params.activity.tasks.find((t: TaskCfg) => t.id === task.id) + let time = cfg.params.time; + if (Date.now() - task.timeStart < time * 1000) { + throw new ZError(11, 'follow failed') + } + let num = Math.random() * 100 + if (num < cfg.params.failRate) { + throw new ZError(12, 'follow failed') + } + task.status = TaskStatusEnum.SUCCESS + task.timeFinish = Date.now() + task.data = {} + try { + await this.params.user.save() + } catch(err) { + throw new ZError(100, 'save failed') + } + if (cfg.autoclaim) { + try { + await this.claimReward(task); + } catch(err) { + console.log(err) + } + } + return true + } + +} \ No newline at end of file diff --git a/src/tasks/YoutubePost.ts b/src/tasks/YoutubePost.ts new file mode 100644 index 0000000..7250c19 --- /dev/null +++ b/src/tasks/YoutubePost.ts @@ -0,0 +1,37 @@ +import { ZError } from "common/ZError"; +import { ITask } from "./base/ITask"; +import { TaskStatusEnum } from "models/ActivityUser"; + +export default class YoutubePost extends ITask { + static desc = 'youtube post' + static show: boolean = true + async execute(data: any) { + const { task } = data + let cfg = this.params.activity.tasks.find(t => t.id === task.id) + let time = cfg.params.time; + if (Date.now() - task.timeStart < time * 1000) { + throw new ZError(11, 'post failed') + } + let num = Math.random() * 100 + if (num < cfg.params.failRate) { + throw new ZError(12, 'post failed') + } + task.status = TaskStatusEnum.SUCCESS + task.timeFinish = Date.now() + task.data = {} + try { + await this.params.user.save() + } catch(err) { + throw new ZError(100, 'save failed') + } + if (cfg.autoclaim) { + try { + await this.claimReward(task); + } catch(err) { + console.log(err) + } + } + return true + } + +} \ No newline at end of file