From 8bdd128fece477ce1979948b14cf976774c08ca7 Mon Sep 17 00:00:00 2001 From: Vincent Date: Sat, 2 Sep 2023 13:25:25 +0800 Subject: [PATCH] fix: new netunion --- README.md | 6 +- lib/coupons/DCwpqO.js | 169 +++++++++++++++++++++++++++++++++++++ lib/coupons/const.js | 8 +- lib/coupons/gundamGrab.js | 56 ++++++++++++ lib/coupons/index.js | 53 ++++++------ lib/coupons/qualityShop.js | 52 ------------ lib/coupons/takeAway.js | 53 ------------ lib/fetch.js | 23 +++-- lib/payload.js | 64 ++++++-------- lib/template.js | 50 +++++++++++ 10 files changed, 355 insertions(+), 179 deletions(-) create mode 100644 lib/coupons/DCwpqO.js create mode 100644 lib/coupons/gundamGrab.js delete mode 100644 lib/coupons/qualityShop.js delete mode 100644 lib/coupons/takeAway.js create mode 100644 lib/template.js diff --git a/README.md b/README.md index f2ef92ac..e7aa29f0 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,11 @@ token=Js3xxxxFyy_Aq-rOnxMte6vKPV4AAAAA6QwAADgqRBSfcmNqyuG8CQ7JDL7xxxxNGbfF7tPNV5 3. 点击右上角 `Fork` 按钮 2. 添加 Actions secrets 1. 导航到 Fork 后的仓库主页面 - 2. 在仓库菜单栏中,点击 `⚙️Settings`(设置) - 3. 点击侧边栏 `Secrets`(密码)条目 + 2. 在仓库菜单栏中,点击 `⚙️Settings` + 3. 点击侧边栏 `Secrets and variables - Actions`条目 4. 点击 `New repository secret` 创建仓库密码 1. 在 `Name` 输入框中填入 `TOKEN` - 2. 在 `Value` 输入框中填入从 cookie 中提取的 token 值(详见下文 TOKEN 配置) + 2. 在 `Secret` 输入框中填入从 cookie 中提取的 token 值(详见下文 TOKEN 配置) 5. 点击 `Add secret` 保存配置 _Fork 后的项目可执行 `npm run sync` 同步上游更新,详细参考【脚本更新】章节_ diff --git a/lib/coupons/DCwpqO.js b/lib/coupons/DCwpqO.js new file mode 100644 index 00000000..9512d82a --- /dev/null +++ b/lib/coupons/DCwpqO.js @@ -0,0 +1,169 @@ +import fetch from '../fetch.js' +import { getRenderList, getTemplateData } from '../template.js' +import { ECODE } from './const.js' + +const gundamId = '1VlhFT' +const fetchStatus = { + CAN_FETCH: 0, + FETCH_ALREADY: 1, + OUT_OF_STOCK: 2, + INVALID_USER_TYPE: 3, + CANNOT_FETCH: 4, + FETCHED_BY_UUID: 5 +} + +function getActUrl(gundamId) { + return new URL( + `https://market.waimai.meituan.com/gd2/single.html?el_biz=waimai&el_page=gundam.loader&tenant=gundam&gundam_id=${gundamId}` + ) +} + +function resolveMetadata(renderList, jsText) { + try { + for (const instanceId of renderList) { + const regRedMod = new RegExp( + `gdc-new-ticket-wall-${instanceId}.+?(?=openUserCheck)` + ) + const res = jsText.match(regRedMod) + + if (res) { + const data = eval(`({moduleId:"${res[0]}})`) + + data.instanceId = instanceId + + return data + } + } + } catch (e) { + return null + } + + return null +} + +async function getTicketConfig(gdId, appJs) { + const renderList = await getRenderList(gdId) + const jsText = await fetch(appJs).then((res) => res.text()) + const data = resolveMetadata(renderList, jsText) + const ticketConfig = data.ticketConfig.makeOptions1.ticketInfo1 + + return { + ...ticketConfig, + instanceId: data.instanceId + } +} + +async function getParams(gdId, channelUrl, instanceId) { + return { + couponReferId: channelUrl, + actualLng: 0, + actualLat: 0, + geoType: 2, + versiom: 1, + isInDpEnv: 0, + gdPageId: gdId, + instanceId: instanceId + } +} + +function formatCoupons(coupons, info) { + return coupons.map((item) => ({ + name: item.couponName, + etime: item.couponEndTime, + amount: item.couponValue, + amountLimit: item.priceLimit, + useCondition: info.useCondition ?? '', + actName: info.actName + })) +} + +async function getLotteryInfo(cookie, couponIds) { + const res = await fetch.get( + `https://promotion.waimai.meituan.com/lottery/couponcomponent/info/v2`, + { + cookie, + params: { + couponReferIds: couponIds.join(','), + actualLng: 0, + actualLat: 0, + geoType: 2, + sceneId: 1, + isInDpEnv: 0, + cType: 'wm_wxapp' + } + } + ) + const couponList = res.data.couponList.filter( + (data) => + data.status === fetchStatus.CAN_FETCH || + data.status === fetchStatus.FETCH_ALREADY + ) + + return couponList +} + +async function grabCoupon(cookie) { + const actUrl = getActUrl(gundamId) + const tmplData = await getTemplateData(cookie, gundamId) + const ticketConfig = await getTicketConfig(tmplData.gdId, tmplData.appJs) + const lotteryCoupons = await getLotteryInfo(cookie, [ + ticketConfig.channelUrl ?? '' + ]) + const results = [] + + for (const coupon of lotteryCoupons) { + if (coupon.status === fetchStatus.FETCH_ALREADY) { + results.push(coupon) + + continue + } + + const res = await fetch.post( + `https://promotion.waimai.meituan.com/lottery/couponcomponent/fetchcomponentcoupon/v2`, + { + appVersion: '', + cType: 'wm_wxapp', + fpPlatform: 3, + mtFingerprint: '', + wxOpenId: '' + }, + { + cookie, + params: { + couponReferId: coupon.couponReferId, + actualLng: 0, + actualLat: 0, + geoType: 2, + version: 1, + isInDpEnv: 0, + gdPageId: tmplData.gdId, + pageId: tmplData.pageId, + instanceId: ticketConfig.instanceId ?? '', + sceneId: 1 + }, + headers: { + mtgsig: '{}', + Origin: actUrl.origin, + Referer: actUrl.origin + '/' + } + } + ) + + if (res.code == 0) { + results.push(coupon) + } else { + // console.log('抢券失败', res) + } + } + + return formatCoupons(results, { + actName: tmplData.actName, + useCondition: ticketConfig.desc + }) +} + +export default { + grabCoupon: grabCoupon, + getActUrl: getActUrl +} +export { getParams } diff --git a/lib/coupons/const.js b/lib/coupons/const.js index f476221e..0ebfce43 100644 --- a/lib/coupons/const.js +++ b/lib/coupons/const.js @@ -7,4 +7,10 @@ const ECODE = { } const HOST = 'https://mediacps.meituan.com' -export { ECODE, HOST } +const couponId = { + main: { gid: '2KAWnD', name: '外卖红包' }, + shop: { gid: '4luWGh', name: '品质优惠天天领' } + // { gid: '1VlhFT', aid: '443084', name: '周末小吃街' } +} + +export { couponId, ECODE, HOST } diff --git a/lib/coupons/gundamGrab.js b/lib/coupons/gundamGrab.js new file mode 100644 index 00000000..0827da60 --- /dev/null +++ b/lib/coupons/gundamGrab.js @@ -0,0 +1,56 @@ +import fetch from '../fetch.js' +import { getTemplateData } from '../template.js' +import getPayload from '../payload.js' +import { ECODE, HOST } from './const.js' + +function getActUrl(gundamId) { + return new URL( + `https://market.waimai.meituan.com/gd/single.html?el_biz=waimai&el_page=gundam.loader&gundam_id=${gundamId}` + ) +} + +function formatCoupons(coupons, actName) { + return coupons.map((item) => ({ + name: item.couponName, + etime: item.etime, + amount: item.couponAmount, + amountLimit: item.amountLimit, + useCondition: item.useCondition, + actName: actName + })) +} + +async function grabCoupon(cookie, gundamId) { + const actUrl = getActUrl(gundamId) + const tmplData = await getTemplateData(cookie, gundamId) + const payload = await getPayload(tmplData.gdId, tmplData.appJs) + const res = await fetch.post(`${HOST}/gundam/gundamGrabV4`, payload, { + cookie, + headers: { + Origin: actUrl.origin, + Referer: actUrl.origin + '/' + } + }) + + if (res.code == 0) { + return formatCoupons(res.data.coupons, tmplData.actName) + } + + const apiInfo = { + api: 'gundamGrabV4', + name: tmplData.actName, + msg: res.msg || res.message + } + + if (res.code == 3) { + throw { code: ECODE.AUTH, ...apiInfo } + } + + throw { code: ECODE.API, ...apiInfo } +} + +export default { + grabCoupon: grabCoupon, + getActUrl: getActUrl +} +export { getPayload } diff --git a/lib/coupons/index.js b/lib/coupons/index.js index 79ac3e5a..aa8983c8 100644 --- a/lib/coupons/index.js +++ b/lib/coupons/index.js @@ -1,7 +1,7 @@ import fetch, { createCookieJar } from '../fetch.js' -import qualityShop from './qualityShop.js' -import takeAwayCoupon from './takeAway.js' -import { ECODE, HOST } from './const.js' +import gundamGrab from './gundamGrab.js' +import DCwpqO from './DCwpqO.js' +import { couponId, ECODE, HOST } from './const.js' function createMTCookie(token) { const cookieJar = createCookieJar(token) @@ -40,47 +40,46 @@ async function getUserInfo(cookie) { throw { code: ECODE.API, api: 'gundamLogin', msg: res.msg || res.message } } -function formatCoupons(coupons) { - return coupons.map((item) => ({ - name: item.couponName, - etime: item.etime, - amount: item.couponAmount, - amountLimit: item.amountLimit, - useCondition: item.useCondition - })) -} - async function runTask(cookie) { try { // 优先检测登录状态 const userInfo = await getUserInfo(cookie) - let grabResult = [] + const grabResult = [] - const takeAwayCouponResult = await takeAwayCoupon.grabCoupon(cookie) + // 主活动,失败时向外抛出异常 + const mainResult = await gundamGrab.grabCoupon(cookie, couponId.main.gid) - grabResult = grabResult.concat(takeAwayCouponResult.coupons) + grabResult.push(...mainResult) try { - const qualityShopCouponResult = await qualityShop.grabCoupon(cookie) - - grabResult = grabResult.concat(qualityShopCouponResult.coupons) - } catch (e) { - // 品质商家红包仅对某些用户群体生效 + const qualityShopResult = await gundamGrab.grabCoupon( + cookie, + couponId.shop.gid + ) + + grabResult.push(...qualityShopResult) + } catch { + // 仅对某些用户群体生效 } + // try { + // const DCwpqOResult = await DCwpqO.grabCoupon(cookie) + + // grabResult.push(...DCwpqOResult) + // } catch (e) { + // console.log('eeee', e) + // // 仅对某些用户群体生效 + // } + return { code: ECODE.SUCC, data: { user: userInfo, - coupons: formatCoupons(grabResult) + coupons: grabResult }, msg: '成功' } } catch (e) { - const data = { - // seems no usage and references - actUrl: takeAwayCoupon.getActUrl().href - } let code, msg // console.log('getCoupons error', e) @@ -104,7 +103,7 @@ async function runTask(cookie) { msg = '程序异常' } - return { code, data, msg, error: e } + return { code, msg, error: e } } } diff --git a/lib/coupons/qualityShop.js b/lib/coupons/qualityShop.js deleted file mode 100644 index ea84f1ac..00000000 --- a/lib/coupons/qualityShop.js +++ /dev/null @@ -1,52 +0,0 @@ -import fetch from '../fetch.js' -import getPayload from '../payload.js' -import { ECODE, HOST } from './const.js' - -const GUNDAM_ID = '4luWGh' -const actUrl = new URL( - `https://market.waimai.meituan.com/gd/single.html?el_biz=waimai&el_page=gundam.loader&gundam_id=${GUNDAM_ID}` -) - -async function getTemplateData() { - const text = await fetch( - `https://market.waimai.meituan.com/api/template/get?env=current&el_biz=waimai&el_page=gundam.loader&gundam_id=${GUNDAM_ID}` - ).then((rep) => rep.text()) - const matchGlobal = text.match(/globalData: ({.+})/) - const matchAppJs = text.match(/https:\/\/[./_-\w]+app.+?\.js(?=")/g) - - try { - const globalData = JSON.parse(matchGlobal[1]) - - return { - gundamId: globalData.gdId, - appJs: matchAppJs[0] - } - } catch (e) { - throw new Error(`活动配置数据获取失败: ${e}`) - } -} - -async function grabCoupon(cookie) { - const tmplData = await getTemplateData(cookie) - const payload = await getPayload(tmplData.gundamId, tmplData.appJs) - const res = await fetch.post(`${HOST}/gundam/gundamGrabV4`, payload, { - cookie, - headers: { - Origin: actUrl.origin, - Referer: actUrl.origin + '/' - } - }) - - if (res.code == 0) return res.data - - if (res.code == 3) { - throw { code: ECODE.AUTH, api: '品质商家', msg: res.msg || res.message } - } - - throw { code: ECODE.API, api: '品质商家', msg: res.msg || res.message } -} - -export default { - grabCoupon: grabCoupon, - getActUrl: () => actUrl -} diff --git a/lib/coupons/takeAway.js b/lib/coupons/takeAway.js deleted file mode 100644 index 7d8efd5d..00000000 --- a/lib/coupons/takeAway.js +++ /dev/null @@ -1,53 +0,0 @@ -import fetch from '../fetch.js' -import getPayload from '../payload.js' -import { ECODE, HOST } from './const.js' - -const GUNDAM_ID = '2KAWnD' -const actUrl = new URL( - `https://market.waimai.meituan.com/gd/single.html?el_biz=waimai&el_page=gundam.loader&gundam_id=${GUNDAM_ID}` -) - -async function getTemplateData(cookie) { - const text = await fetch( - `https://market.waimai.meituan.com/api/template/get?env=current&el_biz=waimai&el_page=gundam.loader&gundam_id=${GUNDAM_ID}`, - { cookie } - ).then((rep) => rep.text()) - const matchGlobal = text.match(/globalData: ({.+})/) - const matchAppJs = text.match(/https:\/\/[./_-\w]+app\.js(?=")/g) - - try { - const globalData = JSON.parse(matchGlobal[1]) - - return { - gundamId: globalData.gdId, - appJs: matchAppJs[0] - } - } catch (e) { - throw new Error(`活动配置数据获取失败: ${e}`) - } -} - -async function grabCoupon(cookie) { - const tmplData = await getTemplateData(cookie) - const payload = await getPayload(tmplData.gundamId, tmplData.appJs) - const res = await fetch.post(`${HOST}/gundam/gundamGrabV4`, payload, { - cookie, - headers: { - Origin: actUrl.origin, - Referer: actUrl.origin + '/' - } - }) - - if (res.code == 0) return res.data - - if (res.code == 3) { - throw { code: ECODE.AUTH, api: 'gundamGrabV4', msg: res.msg || res.message } - } - - throw { code: ECODE.API, api: 'gundamGrabV4', msg: res.msg || res.message } -} - -export default { - grabCoupon: grabCoupon, - getActUrl: () => actUrl -} diff --git a/lib/fetch.js b/lib/fetch.js index 9928bfca..d893f134 100644 --- a/lib/fetch.js +++ b/lib/fetch.js @@ -12,6 +12,7 @@ const ECODE = { } async function fetch(url, opts = {}) { + const urlObj = new URL(url) const cookieJar = opts.cookie const existCookie = cookieJar?.getCookieStringSync(url) const optCookie = opts.headers?.cookie || '' @@ -36,11 +37,19 @@ async function fetch(url, opts = {}) { } opts.headers = Object.assign({}, defHeader, opts.headers) + + if (opts.params) { + Object.keys(opts.params).forEach((key) => { + urlObj.searchParams.append(key, opts.params?.[key]) + }) + } + delete opts.cookie delete opts.timeout + delete opts.params try { - res = await nodeFetch(url, opts) + res = await nodeFetch(urlObj, opts) } catch (e) { if (opts.signal?.aborted) { throw { code: ECODE.TIMEOUT, url: url, msg: e } @@ -61,16 +70,17 @@ async function fetch(url, opts = {}) { } async function doGet(url, opts = {}) { - const params = new URLSearchParams(opts.params) - - const res = await fetch(`${url}?${params.toString()}`, { + const res = await fetch(url, { headers: opts.headers, cookie: opts.cookie, + params: opts.params, timeout: opts.timeout ?? 10000 }) if (res.ok) return res.json() + console.log('res', res) + throw { code: ECODE.FETCH, url: url, msg: res.statusText } } @@ -79,10 +89,10 @@ async function doPost(url, data, opts = {}) { let cType, body if (payloadType == 'form') { - const params = new URLSearchParams(data) + const formData = new URLSearchParams(data) cType = 'application/x-www-form-urlencoded' - body = params.toString() + body = formData.toString() } else { cType = 'application/json' body = data ? JSON.stringify(data) : '' @@ -92,6 +102,7 @@ async function doPost(url, data, opts = {}) { method: 'POST', body: body, cookie: opts.cookie, + params: opts.params, headers: Object.assign({}, opts.headers, { 'Content-Type': cType }), diff --git a/lib/payload.js b/lib/payload.js index fb9cd42e..9c387b05 100644 --- a/lib/payload.js +++ b/lib/payload.js @@ -1,4 +1,5 @@ import fetch from './fetch.js' +import { getRenderList } from './template.js' function parseInstanceID(moduleId) { const regFloat = /(\d+\.\d+)/ @@ -6,55 +7,44 @@ function parseInstanceID(moduleId) { return moduleId.match(regFloat)?.[1] ?? '' } -function resolveMetadata(renderInfo, jsText) { - try { - for (const id of renderInfo) { - const regRedMod = new RegExp(`red-envelope-${id}.+?(?=isOtherToAPP)`) - const res = jsText.match(regRedMod) +function matchMoudleData(id, jsText) { + const endField = 'isStopTJCoupon' + const reg = new RegExp( + `gdc-fx-new-netunion-red-envelope-${id}.+?(?=${endField})` + ) - if (res) { - const data = eval(`({moduleId:"${res[0]}})`) + const res = jsText.match(reg) - data.instanceID = data.instanceID ?? parseInstanceID(data.moduleId) + if (!res) return null - return data - } - } - } catch (e) { - return null - } + const data = eval(`({moduleId:"${res[0]}})`) - return null + return data } -async function getRenderInfo(gundamId) { - let data - +function resolveMetadata(renderList, jsText) { try { - const res = await fetch.get( - 'https://market.waimai.meituan.com/gd/zc/renderinfo', - { - params: { - el_biz: 'waimai', - el_page: 'gundam.loader', - gdId: gundamId, - tenant: 'gundam' - } - } - ) - - data = res.data + for (const instanceId of renderList) { + const data = matchMoudleData(instanceId, jsText) + + if (!data) continue + + data.instanceID = instanceId + data.isStopTJCoupon = true + + return data + } } catch (e) { - throw new Error('renderinfo 接口调用失败:' + e.message) + return null } - return Object.keys(data).filter((k) => data[k].render) + return null } -async function getPayload(gundamId, appJs) { - const renderInfo = await getRenderInfo(gundamId) +async function getGundamPayload(gundamId, appJs) { + const renderList = await getRenderList(gundamId) const jsText = await fetch(appJs).then((res) => res.text()) - const data = resolveMetadata(renderInfo, jsText) + const data = resolveMetadata(renderList, jsText) if (!data) { throw new Error('Payload 获取失败') @@ -75,4 +65,4 @@ async function getPayload(gundamId, appJs) { } } -export default getPayload +export default getGundamPayload diff --git a/lib/template.js b/lib/template.js new file mode 100644 index 00000000..5f86a5ea --- /dev/null +++ b/lib/template.js @@ -0,0 +1,50 @@ +import fetch from './fetch.js' + +async function getTemplateData(cookie, gundamId) { + const text = await fetch( + `https://market.waimai.meituan.com/api/template/get?env=current&el_biz=waimai&el_page=gundam.loader&gundam_id=${gundamId}`, + { cookie } + ).then((rep) => rep.text()) + const matchGlobal = text.match(/globalData: ({.+})/) + const matchAppJs = text.match(/https:\/\/[./_-\w]+app.*\.js(?=")/g) + + try { + const globalData = JSON.parse(matchGlobal[1]) + + return { + gdId: globalData.gdId, + actName: globalData.pageInfo.title, + appJs: matchAppJs[0], + pageId: globalData.pageId + } + } catch (e) { + throw new Error(`活动配置数据获取失败: ${e}`) + } +} + +// 通过接口获取真实的渲染列表 +async function getRenderList(gdId) { + let data + + try { + const res = await fetch.get( + 'https://market.waimai.meituan.com/gd/zc/renderinfo', + { + params: { + el_biz: 'waimai', + el_page: 'gundam.loader', + gdId: gdId, + tenant: 'gundam' + } + } + ) + + data = res.data + } catch (e) { + throw new Error('renderinfo 接口调用失败:' + e.message) + } + + return Object.keys(data).filter((k) => data[k].render) +} + +export { getTemplateData, getRenderList }