pinnacleClient.js 8.7 KB

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