pinnacleClient.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. import dotenv from 'dotenv';
  2. import axios from "axios";
  3. import { HttpsProxyAgent } from "https-proxy-agent";
  4. import { randomUUID } from 'crypto';
  5. import Logs from "./logs.js";
  6. import getDateInTimezone from "./getDateInTimezone.js";
  7. dotenv.config();
  8. const axiosDefaultOptions = {
  9. baseURL: "",
  10. url: "",
  11. method: "GET",
  12. headers: {},
  13. params: {},
  14. data: {},
  15. timeout: 10000,
  16. };
  17. const pinnacleWebOptions = {
  18. ...axiosDefaultOptions,
  19. baseURL: "https://www.part987.com",
  20. headers: {
  21. "accept": "application/json, text/plain, */*",
  22. "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
  23. "x-app-data": "directusToken=TwEdnphtyxsfMpXoJkCkWaPsL2KJJ3lo;lang=zh_CN;dpVXz=ZDfaFZUP9"
  24. },
  25. }
  26. export const pinnacleRequest = async (options) => {
  27. const { url, username, password, ...optionsRest } = options;
  28. if (!url || !username || !password) {
  29. throw new Error("url、username、password is required");
  30. }
  31. const authHeader = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
  32. const axiosConfig = { ...axiosDefaultOptions, ...optionsRest, url, baseURL: "https://api.pinnacle888.com" };
  33. Object.assign(axiosConfig.headers, {
  34. "Authorization": authHeader,
  35. "Accept": "application/json",
  36. });
  37. const proxy = process.env.NODE_HTTP_PROXY;
  38. if (proxy) {
  39. axiosConfig.proxy = false;
  40. axiosConfig.httpsAgent = new HttpsProxyAgent(proxy);
  41. }
  42. return axios(axiosConfig).then(res => {
  43. // Logs.out('pinnacle request', url, axiosConfig, res.data);
  44. return res.data;
  45. });
  46. }
  47. /**
  48. * Pinnacle API Get请求
  49. * @param {*} url
  50. * @param {*} params
  51. * @returns
  52. */
  53. export const pinnacleGet = async (url, params) => {
  54. return pinnacleRequest({
  55. url,
  56. params,
  57. username: process.env.PINNACLE_USERNAME,
  58. password: process.env.PINNACLE_PASSWORD,
  59. })
  60. .catch(err => {
  61. const source = { url, params };
  62. if (err?.response?.data) {
  63. const data = err.response.data;
  64. Object.assign(source, { data });
  65. }
  66. err.source = source;
  67. return Promise.reject(err);
  68. });
  69. }
  70. /**
  71. * Pinnacle API Post请求
  72. * @param {*} url
  73. * @param {*} data
  74. * @returns
  75. */
  76. export const pinnaclePost = async (url, data) => {
  77. return pinnacleRequest({
  78. url,
  79. method: 'POST',
  80. headers: {
  81. 'Content-Type': 'application/json',
  82. },
  83. data,
  84. username: process.env.PINNACLE_USERNAME,
  85. password: process.env.PINNACLE_PASSWORD,
  86. });
  87. }
  88. /**
  89. * 清理对象中的undefined值
  90. * @param {*} obj
  91. * @returns {Object}
  92. */
  93. const cleanUndefined = (obj) => {
  94. return Object.fromEntries(
  95. Object.entries(obj).filter(([, v]) => v !== undefined)
  96. );
  97. }
  98. /**
  99. * 获取直盘线
  100. */
  101. export const getLineInfo = async (info = {}) => {
  102. const {
  103. leagueId, eventId, betType, handicap, team, side,
  104. specialId, contestantId,
  105. periodNumber=0, oddsFormat='Decimal', sportId=29
  106. } = info;
  107. let url = '/v2/line/';
  108. let data = { sportId, leagueId, eventId, betType, handicap, periodNumber, team, side, oddsFormat };
  109. if (specialId) {
  110. url = '/v2/line/special';
  111. data = { specialId, contestantId, oddsFormat };
  112. }
  113. data = cleanUndefined(data);
  114. return pinnacleGet(url, data)
  115. .then(ret => ({ info: ret, line: data }))
  116. .catch(err => {
  117. Logs.errDev('get line info error', err);
  118. return Promise.reject(err);
  119. });
  120. }
  121. /**
  122. * 获取账户余额
  123. */
  124. export const getAccountBalance = async () => {
  125. return pinnacleGet('/v1/client/balance');
  126. }
  127. /**
  128. * 下注
  129. */
  130. export const placeOrder = async ({ info, line, stakeSize }) => {
  131. // return Promise.resolve({info, line, stakeSize});
  132. const uuid = randomUUID()
  133. if (line.specialId) {
  134. const data = cleanUndefined({
  135. oddsFormat: line.oddsFormat,
  136. uniqueRequestId: uuid,
  137. acceptBetterLine: true,
  138. stake: stakeSize,
  139. winRiskStake: 'RISK',
  140. lineId: info.lineId,
  141. specialId: info.specialId,
  142. contestantId: info.contestantId,
  143. });
  144. Logs.outDev('pinnacle place order data', data);
  145. return pinnaclePost('/v4/bets/special', { bets: [data] })
  146. .then(ret => ret.bets?.[0] ?? ret)
  147. .then(ret => {
  148. Logs.outDev('pinnacle place order', ret, uuid);
  149. return ret;
  150. })
  151. .catch(err => {
  152. Logs.outDev('pinnacle place order error', err.response.data, uuid);
  153. Logs.errDev(err);
  154. return Promise.reject(err);
  155. });
  156. }
  157. else {
  158. const data = cleanUndefined({
  159. oddsFormat: line.oddsFormat,
  160. uniqueRequestId: uuid,
  161. acceptBetterLine: true,
  162. stake: stakeSize,
  163. winRiskStake: 'RISK',
  164. lineId: info.lineId,
  165. altLineId: info.altLineId,
  166. fillType: 'NORMAL',
  167. sportId: line.sportId,
  168. eventId: line.eventId,
  169. periodNumber: line.periodNumber,
  170. betType: line.betType,
  171. team: line.team,
  172. side: line.side,
  173. handicap: line.handicap,
  174. });
  175. Logs.outDev('pinnacle place order data', data);
  176. return pinnaclePost('/v4/bets/place', data)
  177. .then(ret => {
  178. Logs.outDev('pinnacle place order', ret, uuid);
  179. return ret;
  180. })
  181. .catch(err => {
  182. Logs.outDev('pinnacle place order error', err.response.data, uuid);
  183. Logs.errDev(err);
  184. return Promise.reject(err);
  185. });
  186. }
  187. }
  188. /**
  189. * 获取Web端联赛数据
  190. * @param {*} marketType 0: 早盘赛事, 1: 今日赛事
  191. * @param {*} locale en_US or zh_CN
  192. * @returns
  193. */
  194. export const pinnacleWebLeagues = async (marketType = 1, locale = "zh_CN") => {
  195. const dateString = marketType == 1 ? getDateInTimezone(-4) : getDateInTimezone(-4, Date.now()+24*60*60*1000);
  196. const nowTime = Date.now();
  197. const axiosConfig = {
  198. ...pinnacleWebOptions,
  199. url: "/sports-service/sv/compact/leagues",
  200. params: {
  201. btg: 1, c: "", d: dateString,
  202. l: true, mk: marketType,
  203. pa: 0, pn: -1, sp: 29, tm: 0,
  204. locale, _: nowTime, withCredentials: true
  205. },
  206. };
  207. const proxy = process.env.NODE_HTTP_PROXY;
  208. if (proxy) {
  209. axiosConfig.proxy = false;
  210. axiosConfig.httpsAgent = new HttpsProxyAgent(proxy);
  211. }
  212. return axios(axiosConfig).then(res => res.data?.[0]?.[2]?.map(item => {
  213. const [ id, , name ] = item;
  214. return [id, { id, name }];
  215. }) ?? []);
  216. }
  217. /**
  218. * 获取Web比赛列表
  219. */
  220. export const pinnacleWebGames = async (leagues=[], marketType=1, locale="zh_CN") => {
  221. const dateString = marketType == 1 ? getDateInTimezone(-4) : getDateInTimezone(-4, Date.now()+24*60*60*1000);
  222. const nowTime = Date.now();
  223. const axiosConfig = {
  224. ...pinnacleWebOptions,
  225. url: "/sports-service/sv/odds/events",
  226. params: {
  227. sp: 29, lg: leagues.join(','), ev: "",
  228. mk: marketType, btg: 1, ot: 1,
  229. d: dateString, o: 0, l: 100, v: 0,
  230. me: 0, more: false, tm: 0, pa: 0, c: "",
  231. g: "QQ==", cl: 100, pimo: "0,1,8,39,2,3,6,7,4,5",
  232. inl: false, _: nowTime, locale
  233. },
  234. };
  235. const proxy = process.env.NODE_HTTP_PROXY;
  236. if (proxy) {
  237. axiosConfig.proxy = false;
  238. axiosConfig.httpsAgent = new HttpsProxyAgent(proxy);
  239. }
  240. return axios(axiosConfig).then(res => res.data.n?.[0]?.[2]?.map(league => {
  241. const [leagueId, leagueName, games] = league;
  242. return games.map(game => {
  243. const [id, teamHomeName, teamAwayName, , timestamp] = game;
  244. const startTime = getDateInTimezone('+8', timestamp, true);
  245. return { id, leagueId, leagueName, teamHomeName, teamAwayName, timestamp, startTime }
  246. })
  247. }).flat().filter(game => {
  248. const { teamHomeName, teamAwayName } = game;
  249. if (teamHomeName.startsWith('主队')) {
  250. return false;
  251. }
  252. else if (teamAwayName.startsWith('客队')) {
  253. return false;
  254. }
  255. else if (teamHomeName.startsWith('Home Teams')) {
  256. return false;
  257. }
  258. else if (teamAwayName.startsWith('Away Teams')) {
  259. return false;
  260. }
  261. return true;
  262. }) ?? []);
  263. }
  264. export const platformRequest = async (options) => {
  265. const { url } = options;
  266. if (!url) {
  267. throw new Error("url is required");
  268. }
  269. const mergedOptions = {
  270. ...axiosDefaultOptions,
  271. ...options,
  272. baseURL: "http://127.0.0.1:9020",
  273. };
  274. return axios(mergedOptions).then(res => res.data);
  275. }
  276. export const platformPost = async (url, data) => {
  277. return platformRequest({
  278. url,
  279. method: 'POST',
  280. headers: {
  281. 'Content-Type': 'application/json',
  282. },
  283. data,
  284. });
  285. }
  286. export const platformGet = async (url, params) => {
  287. return platformRequest({ url, method: 'GET', params });
  288. }
  289. /**
  290. * 通知异常
  291. * @param {*} message
  292. */
  293. export const notifyException = async (message) => {
  294. Logs.out('notify exception', message);
  295. }