FP Tools

Различные полезности для FunPay: копирование лотов, замена пустого чата на активные лоты, логирование сообщений в Discord

  1. // ==UserScript==
  2. // @name FP Tools
  3. // @namespace https://funpay.com/
  4. // @version 1.6
  5. // @description Различные полезности для FunPay: копирование лотов, замена пустого чата на активные лоты, логирование сообщений в Discord
  6. // @author Your Name
  7. // @match https://funpay.com/*
  8. // @grant GM_xmlhttpRequest
  9. // @grant GM_addStyle
  10. // @run-at document-end
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. function createElement(tag, attributes = {}, styles = {}, innerHTML = '') {
  17. const element = document.createElement(tag);
  18. for (const [key, value] of Object.entries(attributes)) {
  19. element.setAttribute(key, value);
  20. }
  21. for (const [key, value] of Object.entries(styles)) {
  22. element.style[key] = value;
  23. }
  24. element.innerHTML = innerHTML;
  25. return element;
  26. }
  27.  
  28. function sendToDiscordWebhook(node) {
  29. const userName = node.querySelector('.media-user-name').textContent.trim();
  30. const messageText = node.querySelector('.contact-item-message').textContent.trim();
  31. const avatarUrl = node.querySelector('.avatar-photo').style.backgroundImage.slice(5, -2);
  32. const webhookUrl = localStorage.getItem('discordWebhookUrl');
  33.  
  34. if (!webhookUrl) {
  35. console.error('uRL not set');
  36. return;
  37. }
  38.  
  39. const payload = {
  40. username: userName,
  41. avatar_url: avatarUrl,
  42. embeds: [{
  43. description: messageText,
  44. color: 0x00FF00
  45. }]
  46. };
  47.  
  48. fetch(webhookUrl, {
  49. method: 'POST',
  50. headers: {
  51. 'Content-Type': 'application/json'
  52. },
  53. body: JSON.stringify(payload)
  54. })
  55. .then(response => {
  56. if (!response.ok) {
  57. console.error('failed to send message to discord');
  58. }
  59. })
  60. .catch(error => {
  61. console.error('error sending message to ds:', error);
  62. });
  63. }
  64.  
  65. const cloneButton = createElement('button', { class: 'btn btn-default' }, { marginLeft: '10px' }, 'Копировать');
  66.  
  67. const header = Array.from(document.querySelectorAll('h1.page-header.page-header-no-hr'))
  68. .find(h1 => h1.textContent.includes('Редактирование предложения'));
  69.  
  70. if (header) {
  71. header.parentNode.insertBefore(cloneButton, header.nextSibling);
  72. }
  73.  
  74. const popupMenu = createElement('div', {}, {
  75. display: 'none',
  76. position: 'fixed',
  77. top: '50%',
  78. left: '50%',
  79. transform: 'translate(-50%, -50%)',
  80. backgroundColor: 'gray',
  81. border: '1px solid black',
  82. padding: '20px',
  83. zIndex: '10000'
  84. }, `
  85. <button id="fullClone" class="btn btn-primary">Скопировать полностью</button>
  86. <button id="changeCategoryClone" class="btn btn-primary">Поменять категорию и скопировать</button>
  87. <button id="closePopup" class="btn btn-default">Закрыть</button>
  88. `);
  89. document.body.appendChild(popupMenu);
  90.  
  91. const navBar = document.querySelector('.nav.navbar-nav.navbar-right.logged');
  92. const toolsMenu = createElement('li', {}, {}, `
  93. <a style="font-weight: bold; cursor: pointer; user-select: none;" id="fpToolsButton">FP Tools</a>
  94. `);
  95. navBar.appendChild(toolsMenu);
  96.  
  97. const styles = `
  98. .fp-tools-popup {
  99. display: none;
  100. position: fixed;
  101. top: 50%;
  102. left: 50%;
  103. transform: translate(-50%, -50%);
  104. background: rgba(20, 20, 20, 0.8);
  105. backdrop-filter: blur(20px);
  106. border-radius: 30px;
  107. box-shadow: 0 0 100px rgba(149, 0, 255, 0.3), 0 0 30px rgba(0, 247, 255, 0.5);
  108. padding: 40px;
  109. z-index: 10000;
  110. font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  111. max-width: 500px;
  112. width: 100%;
  113. color: #fff;
  114. transition: all 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55);
  115. }
  116. .fp-tools-popup.active {
  117. display: block;
  118. animation: popIn 0.7s cubic-bezier(0.26, 0.53, 0.74, 1.48);
  119. }
  120. @keyframes popIn {
  121. 0% { opacity: 0; transform: translate(-50%, -60%) scale(0.5); }
  122. 100% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  123. }
  124. .fp-tools-popup h2 {
  125. margin: 0 0 30px;
  126. font-size: 32px;
  127. font-weight: 700;
  128. color: #fff;
  129. text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
  130. text-align: center;
  131. letter-spacing: 2px;
  132. }
  133. .fp-tools-popup .close-btn {
  134. position: absolute;
  135. top: 20px;
  136. right: 20px;
  137. background: rgba(255, 255, 255, 0.1);
  138. border: none;
  139. color: #fff;
  140. font-size: 24px;
  141. width: 40px;
  142. height: 40px;
  143. border-radius: 50%;
  144. cursor: pointer;
  145. transition: all 0.3s ease;
  146. display: flex;
  147. align-items: center;
  148. justify-content: center;
  149. }
  150. .fp-tools-popup .close-btn:hover {
  151. background: rgba(255, 255, 255, 0.2);
  152. transform: scale(1.1);
  153. }
  154. .fp-tools-popup .close-btn::after {
  155. content: '×';
  156. display: block;
  157. transform: translateY(-1px);
  158. }
  159. .fp-tools-popup label {
  160. display: flex;
  161. align-items: center;
  162. margin-bottom: 20px;
  163. font-size: 18px;
  164. cursor: pointer;
  165. }
  166. .fp-tools-popup input[type="checkbox"] {
  167. appearance: none;
  168. -webkit-appearance: none;
  169. width: 24px;
  170. height: 24px;
  171. border-radius: 5px;
  172. margin-right: 15px;
  173. background: rgba(255, 255, 255, 0.1);
  174. position: relative;
  175. cursor: pointer;
  176. transition: all 0.3s ease;
  177. }
  178. .fp-tools-popup input[type="checkbox"]:checked {
  179. background: linear-gradient(45deg, #00C9FF, #92FE9D);
  180. }
  181. .fp-tools-popup input[type="checkbox"]::after {
  182. content: '✓';
  183. position: absolute;
  184. top: 50%;
  185. left: 50%;
  186. transform: translate(-50%, -50%);
  187. font-size: 16px;
  188. color: #fff;
  189. opacity: 0;
  190. transition: opacity 0.2s ease;
  191. }
  192. .fp-tools-popup input[type="checkbox"]:checked::after {
  193. opacity: 1;
  194. }
  195. .fp-tools-popup input[type="text"] {
  196. width: 100%;
  197. padding: 15px;
  198. margin-bottom: 25px;
  199. border: none;
  200. border-radius: 15px;
  201. background: rgba(255, 255, 255, 0.1);
  202. color: #fff;
  203. font-size: 16px;
  204. transition: all 0.3s ease;
  205. }
  206. .fp-tools-popup input[type="text"]:focus {
  207. outline: none;
  208. box-shadow: 0 0 0 3px rgba(0, 247, 255, 0.5);
  209. }
  210. .fp-tools-popup input[type="text"]:disabled {
  211. opacity: 0.5;
  212. cursor: not-allowed;
  213. }
  214. .fp-tools-popup input[type="text"]:disabled::before {
  215. content: '';
  216. position: absolute;
  217. top: 0;
  218. left: 0;
  219. right: 0;
  220. bottom: 0;
  221. background: rgba(0, 0, 0, 0.5);
  222. border-radius: 15px;
  223. z-index: 1;
  224. }
  225. .fp-tools-popup button {
  226. background: linear-gradient(45deg, #FF6B6B, #6B66FF);
  227. color: white;
  228. border: none;
  229. padding: 15px 30px;
  230. font-size: 18px;
  231. font-weight: bold;
  232. cursor: pointer;
  233. border-radius: 50px;
  234. transition: all 0.3s ease;
  235. text-transform: uppercase;
  236. letter-spacing: 2px;
  237. box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
  238. position: relative;
  239. overflow: hidden;
  240. }
  241. .fp-tools-popup button::before {
  242. content: '';
  243. position: absolute;
  244. top: -50%;
  245. left: -50%;
  246. width: 200%;
  247. height: 200%;
  248. background: radial-gradient(circle, rgba(255,255,255,0.3) 0%, rgba(255,255,255,0) 80%);
  249. transform: scale(0);
  250. transition: transform 0.6s ease-out;
  251. }
  252. .fp-tools-popup button:hover::before {
  253. transform: scale(1);
  254. }
  255. .fp-tools-popup button:hover {
  256. transform: translateY(-5px);
  257. box-shadow: 0 15px 30px rgba(0, 0, 0, 0.3);
  258. }
  259. .fp-tools-popup button:active {
  260. transform: translateY(2px);
  261. }
  262. `;
  263.  
  264. const styleElement = document.createElement('style');
  265. styleElement.textContent = styles;
  266. document.head.appendChild(styleElement);
  267.  
  268. const toolsPopup = document.createElement('div');
  269. toolsPopup.className = 'fp-tools-popup';
  270. toolsPopup.innerHTML = `
  271. <h2>FP Tools</h2>
  272. <button class="close-btn" aria-label="Закрыть"></button>
  273. <div>
  274. <label>
  275. <input type="checkbox" id="logToDiscordCheckbox">
  276. Логирование сообщений в Discord
  277. </label>
  278. </div>
  279. <input type="text" id="discordWebhookUrl" placeholder="Вставьте ссылку на вебхук" disabled>
  280. <button id="saveSettings">Сохранить</button>
  281. `;
  282.  
  283. document.body.appendChild(toolsPopup);
  284.  
  285. document.getElementById('fpToolsButton').addEventListener('click', () => {
  286. toolsPopup.classList.add('active');
  287. });
  288.  
  289. document.querySelector('.fp-tools-popup .close-btn').addEventListener('click', () => {
  290. toolsPopup.classList.remove('active');
  291. });
  292.  
  293. document.getElementById('logToDiscordCheckbox').addEventListener('change', (event) => {
  294. const webhookInput = document.getElementById('discordWebhookUrl');
  295. webhookInput.disabled = !event.target.checked;
  296. if (event.target.checked) {
  297. webhookInput.focus();
  298. }
  299. });
  300.  
  301. document.getElementById('saveSettings').addEventListener('click', () => {
  302. const webhookUrl = document.getElementById('discordWebhookUrl').value;
  303. const logToDiscord = document.getElementById('logToDiscordCheckbox').checked;
  304. localStorage.setItem('discordWebhookUrl', webhookUrl);
  305. localStorage.setItem('logToDiscord', logToDiscord);
  306. toolsPopup.classList.remove('active');
  307. showNotification('Настройки сохранены!');
  308. });
  309.  
  310. function showNotification(message) {
  311. const notification = document.createElement('div');
  312. notification.textContent = message;
  313. notification.style.cssText = `
  314. position: fixed;
  315. bottom: 30px;
  316. right: 30px;
  317. background: linear-gradient(45deg, #00C9FF, #92FE9D);
  318. color: white;
  319. padding: 20px 30px;
  320. border-radius: 50px;
  321. font-size: 18px;
  322. font-weight: bold;
  323. box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
  324. animation: slideIn 0.5s forwards, fadeOut 0.5s 2.5s forwards;
  325. `;
  326. document.body.appendChild(notification);
  327.  
  328. setTimeout(() => {
  329. document.body.removeChild(notification);
  330. }, 3000);
  331. }
  332.  
  333. const keyframes = `
  334. @keyframes slideIn {
  335. from { transform: translateX(100%); opacity: 0; }
  336. to { transform: translateX(0); opacity: 1; }
  337. }
  338. @keyframes fadeOut {
  339. from { opacity: 1; }
  340. to { opacity: 0; }
  341. }
  342. `;
  343. const styleSheet = document.createElement("style");
  344. styleSheet.type = "text/css";
  345. styleSheet.innerText = keyframes;
  346. document.head.appendChild(styleSheet);
  347.  
  348. const savedWebhookUrl = localStorage.getItem('discordWebhookUrl');
  349. const savedLogToDiscord = localStorage.getItem('logToDiscord') === 'true';
  350.  
  351. if (savedWebhookUrl) {
  352. document.getElementById('discordWebhookUrl').value = savedWebhookUrl;
  353. }
  354. document.getElementById('logToDiscordCheckbox').checked = savedLogToDiscord;
  355. document.getElementById('discordWebhookUrl').disabled = !savedLogToDiscord;
  356.  
  357. function submitForm(formData) {
  358. return new Promise((resolve, reject) => {
  359. const nodeId = new URLSearchParams(window.location.search).get('node');
  360. formData.set('node_id', nodeId);
  361. formData.set('offer_id', '0');
  362.  
  363. const data = {};
  364. formData.forEach((value, key) => {
  365. data[key] = value;
  366. });
  367.  
  368. GM_xmlhttpRequest({
  369. method: 'POST',
  370. url: 'https://funpay.com/lots/offerSave',
  371. headers: {
  372. 'Content-Type': 'application/x-www-form-urlencoded'
  373. },
  374. data: new URLSearchParams(data).toString(),
  375. onload: (response) => {
  376. if (response.status === 200) {
  377. showNotification('Лот успешно продублирован!');
  378. resolve();
  379. } else {
  380. console.error('Ошибка при копировании лота', response);
  381. reject('Ошибка при копировании лота');
  382. }
  383. },
  384. onerror: (error) => {
  385. console.error('Ошибка при выполнении запроса', error);
  386. reject('Ошибка при выполнении запроса');
  387. }
  388. });
  389. });
  390. }
  391.  
  392. cloneButton.addEventListener('click', () => {
  393. popupMenu.style.display = 'block';
  394. });
  395.  
  396. document.getElementById('fullClone').addEventListener('click', () => {
  397. popupMenu.style.display = 'none';
  398. const form = document.querySelector('form.form-offer-editor');
  399. if (!form) {
  400. console.error('Форма не найдена');
  401. return;
  402. }
  403. const formData = new FormData(form);
  404. submitForm(formData);
  405. });
  406.  
  407. document.getElementById('changeCategoryClone').addEventListener('click', () => {
  408. popupMenu.style.display = 'none';
  409. const selects = document.querySelectorAll('select.form-control.lot-field-input, select.form-control[name="server_id"]');
  410. const categoryData = {};
  411. selects.forEach(select => {
  412. const label = select.previousElementSibling ? select.previousElementSibling.textContent.trim() : 'Категория';
  413. if (!categoryData[label]) {
  414. categoryData[label] = [];
  415. }
  416. select.querySelectorAll('option').forEach(option => {
  417. categoryData[label].push({
  418. value: option.value,
  419. text: option.textContent.trim()
  420. });
  421. });
  422. });
  423. const categoryMenu = createElement('div', {}, {
  424. display: 'none',
  425. position: 'fixed',
  426. top: '50%',
  427. left: '50%',
  428. transform: 'translate(-50%, -50%)',
  429. backgroundColor: 'gray',
  430. border: '1px solid black',
  431. padding: '20px',
  432. zIndex: '10000'
  433. });
  434. let htmlContent = '';
  435. for (const label in categoryData) {
  436. htmlContent += `<div>`;
  437. htmlContent += `<label><input type="checkbox" id="${label}SelectAll"> Все</label>`;
  438. htmlContent += `<label for="${label}Select">${label}:</label>`;
  439. htmlContent += `<select id="${label}Select" class="form-control" multiple>`;
  440. categoryData[label].forEach(option => {
  441. htmlContent += `<option value="${option.value}">${option.text}</option>`;
  442. });
  443. htmlContent += `</select>`;
  444. htmlContent += `</div>`;
  445. }
  446. htmlContent += `<button id="copyWithCategory" class="btn btn-primary">Копировать</button>`;
  447. htmlContent += `<button id="closeCategoryMenu" class="btn btn-default">Закрыть</button>`;
  448. categoryMenu.innerHTML = htmlContent;
  449. document.body.appendChild(categoryMenu);
  450. categoryMenu.style.display = 'block';
  451.  
  452. // Добавляем обработчики для чекбоксов "Все"
  453. for (const label in categoryData) {
  454. document.getElementById(`${label}SelectAll`).addEventListener('change', (event) => {
  455. const select = document.getElementById(`${label}Select`);
  456. const options = select.options;
  457. for (let i = 0; i < options.length; i++) {
  458. options[i].selected = event.target.checked;
  459. }
  460. });
  461. }
  462.  
  463. document.getElementById('copyWithCategory').addEventListener('click', async () => {
  464. categoryMenu.style.display = 'none';
  465. const form = document.querySelector('form.form-offer-editor');
  466. if (!form) {
  467. console.error('Форма не найдена');
  468. return;
  469. }
  470. const selectedCategories = [];
  471. for (const label in categoryData) {
  472. const selectedOptions = Array.from(document.getElementById(`${label}Select`).selectedOptions)
  473. .map(option => option.value);
  474. if (selectedOptions.length > 0) {
  475. selectedCategories.push({
  476. label: label,
  477. selectedOptions: selectedOptions
  478. });
  479. }
  480. }
  481. for (const category of selectedCategories) {
  482. for (const option of category.selectedOptions) {
  483. const clonedFormData = new FormData(form);
  484. if (category.label === 'Категория') {
  485. clonedFormData.set('lot_category', option);
  486. } else {
  487. clonedFormData.set('server_id', option);
  488. }
  489. await submitForm(clonedFormData);
  490. await new Promise(resolve => setTimeout(resolve, 1000));
  491. }
  492. }
  493. document.body.removeChild(categoryMenu);
  494. });
  495.  
  496. document.getElementById('closeCategoryMenu').addEventListener('click', () => {
  497. document.body.removeChild(categoryMenu);
  498. });
  499. });
  500.  
  501. document.getElementById('closePopup').addEventListener('click', () => {
  502. popupMenu.style.display = 'none';
  503. });
  504.  
  505. function replaceEmptyChatWithActiveOrders() {
  506. const emptyChat = document.querySelector('.chat-empty');
  507. if (emptyChat) {
  508. GM_xmlhttpRequest({
  509. method: 'GET',
  510. url: 'https://funpay.com/orders/trade',
  511. onload: (response) => {
  512. if (response.status === 200) {
  513. const parser = new DOMParser();
  514. const doc = parser.parseFromString(response.responseText, 'text/html');
  515. const activeOrders = doc.querySelectorAll('.tc-item.info');
  516. const activeOrdersContainer = createElement('div', { class: 'active-orders-container' }, {
  517. width: '100%',
  518. padding: '10px',
  519. boxSizing: 'border-box',
  520. position: 'absolute',
  521. top: '10px',
  522. left: '10px',
  523. fontSize: '0.67em'
  524. });
  525.  
  526. activeOrders.forEach(order => {
  527. const statusElement = order.querySelector('.tc-status');
  528. if (statusElement && statusElement.textContent.trim() === 'Оплачен') {
  529. const orderElement = createElement('a', { href: order.href }, {
  530. display: 'block',
  531. marginBottom: '10px',
  532. padding: '5px',
  533. border: '1px solid #ddd',
  534. borderRadius: '5px',
  535. textDecoration: 'none',
  536. color: 'inherit',
  537. transition: 'all 0.3s ease'
  538. });
  539.  
  540. orderElement.onmouseover = () => {
  541. orderElement.style.backgroundColor = '#f0f0f0';
  542. orderElement.style.transform = 'scale(1.03)';
  543. orderElement.style.boxShadow = '0 2px 5px rgba(0,0,0,0.1)';
  544. };
  545. orderElement.onmouseout = () => {
  546. orderElement.style.backgroundColor = '';
  547. orderElement.style.transform = '';
  548. orderElement.style.boxShadow = '';
  549. };
  550.  
  551. const dateElement = order.querySelector('.tc-date-time');
  552. if (dateElement) {
  553. const fullDate = dateElement.textContent.trim();
  554. const dateParts = fullDate.split(',');
  555. if (dateParts.length > 0) {
  556. const shortDate = dateParts[0].trim();
  557. orderElement.innerHTML += `<div style="font-weight: bold;">${shortDate}</div>`;
  558. }
  559. }
  560.  
  561. const descElement = order.querySelector('.order-desc');
  562. if (descElement) {
  563. const descText = descElement.textContent.trim();
  564. orderElement.innerHTML += `<div style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${descText}</div>`;
  565. }
  566.  
  567. const priceElement = order.querySelector('.tc-price');
  568. if (priceElement) {
  569. orderElement.innerHTML += `<div style="color: green;">${priceElement.textContent.trim()}</div>`;
  570. }
  571.  
  572. activeOrdersContainer.appendChild(orderElement);
  573. }
  574. });
  575.  
  576. if (activeOrdersContainer.children.length > 0) {
  577. emptyChat.innerHTML = '';
  578. emptyChat.appendChild(activeOrdersContainer);
  579. emptyChat.style.padding = '0';
  580. }
  581. } else {
  582. console.error('ошибка при загрузке активных заказов', response);
  583. }
  584. },
  585. onerror: (error) => {
  586. console.error('ошибка при выполнении запроса активных заказов', error);
  587. }
  588. });
  589. }
  590. }
  591.  
  592. replaceEmptyChatWithActiveOrders();
  593.  
  594. function logNewMessagesToDiscord() {
  595. const unreadMessages = document.querySelectorAll('.contact-item.unread');
  596.  
  597. unreadMessages.forEach(message => {
  598. const messageId = message.getAttribute('data-id');
  599. const isAlreadySent = localStorage.getItem(`discordSent_${messageId}`);
  600.  
  601. if (!isAlreadySent) {
  602. sendToDiscordWebhook(message);
  603. localStorage.setItem(`discordSent_${messageId}`, true);
  604. }
  605. });
  606. }
  607.  
  608. logNewMessagesToDiscord();
  609.  
  610. })();