// ==UserScript==
// @name FP Tools
// @namespace https://funpay.com/
// @version 1.6
// @description Различные полезности для FunPay: копирование лотов, замена пустого чата на активные лоты, логирование сообщений в Discord
// @author Your Name
// @match https://funpay.com/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
function createElement(tag, attributes = {}, styles = {}, innerHTML = '') {
const element = document.createElement(tag);
for (const [key, value] of Object.entries(attributes)) {
element.setAttribute(key, value);
}
for (const [key, value] of Object.entries(styles)) {
element.style[key] = value;
}
element.innerHTML = innerHTML;
return element;
}
function sendToDiscordWebhook(node) {
const userName = node.querySelector('.media-user-name').textContent.trim();
const messageText = node.querySelector('.contact-item-message').textContent.trim();
const avatarUrl = node.querySelector('.avatar-photo').style.backgroundImage.slice(5, -2);
const webhookUrl = localStorage.getItem('discordWebhookUrl');
if (!webhookUrl) {
console.error('uRL not set');
return;
}
const payload = {
username: userName,
avatar_url: avatarUrl,
embeds: [{
description: messageText,
color: 0x00FF00
}]
};
fetch(webhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})
.then(response => {
if (!response.ok) {
console.error('failed to send message to discord');
}
})
.catch(error => {
console.error('error sending message to ds:', error);
});
}
const cloneButton = createElement('button', { class: 'btn btn-default' }, { marginLeft: '10px' }, 'Копировать');
const header = Array.from(document.querySelectorAll('h1.page-header.page-header-no-hr'))
.find(h1 => h1.textContent.includes('Редактирование предложения'));
if (header) {
header.parentNode.insertBefore(cloneButton, header.nextSibling);
}
const popupMenu = createElement('div', {}, {
display: 'none',
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: 'gray',
border: '1px solid black',
padding: '20px',
zIndex: '10000'
}, `
<button id="fullClone" class="btn btn-primary">Скопировать полностью</button>
<button id="changeCategoryClone" class="btn btn-primary">Поменять категорию и скопировать</button>
<button id="closePopup" class="btn btn-default">Закрыть</button>
`);
document.body.appendChild(popupMenu);
const navBar = document.querySelector('.nav.navbar-nav.navbar-right.logged');
const toolsMenu = createElement('li', {}, {}, `
<a style="font-weight: bold; cursor: pointer; user-select: none;" id="fpToolsButton">FP Tools</a>
`);
navBar.appendChild(toolsMenu);
const styles = `
.fp-tools-popup {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(20, 20, 20, 0.8);
backdrop-filter: blur(20px);
border-radius: 30px;
box-shadow: 0 0 100px rgba(149, 0, 255, 0.3), 0 0 30px rgba(0, 247, 255, 0.5);
padding: 40px;
z-index: 10000;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 500px;
width: 100%;
color: #fff;
transition: all 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55);
}
.fp-tools-popup.active {
display: block;
animation: popIn 0.7s cubic-bezier(0.26, 0.53, 0.74, 1.48);
}
@keyframes popIn {
0% { opacity: 0; transform: translate(-50%, -60%) scale(0.5); }
100% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}
.fp-tools-popup h2 {
margin: 0 0 30px;
font-size: 32px;
font-weight: 700;
color: #fff;
text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
text-align: center;
letter-spacing: 2px;
}
.fp-tools-popup .close-btn {
position: absolute;
top: 20px;
right: 20px;
background: rgba(255, 255, 255, 0.1);
border: none;
color: #fff;
font-size: 24px;
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.fp-tools-popup .close-btn:hover {
background: rgba(255, 255, 255, 0.2);
transform: scale(1.1);
}
.fp-tools-popup .close-btn::after {
content: '×';
display: block;
transform: translateY(-1px);
}
.fp-tools-popup label {
display: flex;
align-items: center;
margin-bottom: 20px;
font-size: 18px;
cursor: pointer;
}
.fp-tools-popup input[type="checkbox"] {
appearance: none;
-webkit-appearance: none;
width: 24px;
height: 24px;
border-radius: 5px;
margin-right: 15px;
background: rgba(255, 255, 255, 0.1);
position: relative;
cursor: pointer;
transition: all 0.3s ease;
}
.fp-tools-popup input[type="checkbox"]:checked {
background: linear-gradient(45deg, #00C9FF, #92FE9D);
}
.fp-tools-popup input[type="checkbox"]::after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 16px;
color: #fff;
opacity: 0;
transition: opacity 0.2s ease;
}
.fp-tools-popup input[type="checkbox"]:checked::after {
opacity: 1;
}
.fp-tools-popup input[type="text"] {
width: 100%;
padding: 15px;
margin-bottom: 25px;
border: none;
border-radius: 15px;
background: rgba(255, 255, 255, 0.1);
color: #fff;
font-size: 16px;
transition: all 0.3s ease;
}
.fp-tools-popup input[type="text"]:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(0, 247, 255, 0.5);
}
.fp-tools-popup input[type="text"]:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.fp-tools-popup input[type="text"]:disabled::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
border-radius: 15px;
z-index: 1;
}
.fp-tools-popup button {
background: linear-gradient(45deg, #FF6B6B, #6B66FF);
color: white;
border: none;
padding: 15px 30px;
font-size: 18px;
font-weight: bold;
cursor: pointer;
border-radius: 50px;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 2px;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
position: relative;
overflow: hidden;
}
.fp-tools-popup button::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.3) 0%, rgba(255,255,255,0) 80%);
transform: scale(0);
transition: transform 0.6s ease-out;
}
.fp-tools-popup button:hover::before {
transform: scale(1);
}
.fp-tools-popup button:hover {
transform: translateY(-5px);
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.3);
}
.fp-tools-popup button:active {
transform: translateY(2px);
}
`;
const styleElement = document.createElement('style');
styleElement.textContent = styles;
document.head.appendChild(styleElement);
const toolsPopup = document.createElement('div');
toolsPopup.className = 'fp-tools-popup';
toolsPopup.innerHTML = `
<h2>FP Tools</h2>
<button class="close-btn" aria-label="Закрыть"></button>
<div>
<label>
<input type="checkbox" id="logToDiscordCheckbox">
Логирование сообщений в Discord
</label>
</div>
<input type="text" id="discordWebhookUrl" placeholder="Вставьте ссылку на вебхук" disabled>
<button id="saveSettings">Сохранить</button>
`;
document.body.appendChild(toolsPopup);
document.getElementById('fpToolsButton').addEventListener('click', () => {
toolsPopup.classList.add('active');
});
document.querySelector('.fp-tools-popup .close-btn').addEventListener('click', () => {
toolsPopup.classList.remove('active');
});
document.getElementById('logToDiscordCheckbox').addEventListener('change', (event) => {
const webhookInput = document.getElementById('discordWebhookUrl');
webhookInput.disabled = !event.target.checked;
if (event.target.checked) {
webhookInput.focus();
}
});
document.getElementById('saveSettings').addEventListener('click', () => {
const webhookUrl = document.getElementById('discordWebhookUrl').value;
const logToDiscord = document.getElementById('logToDiscordCheckbox').checked;
localStorage.setItem('discordWebhookUrl', webhookUrl);
localStorage.setItem('logToDiscord', logToDiscord);
toolsPopup.classList.remove('active');
showNotification('Настройки сохранены!');
});
function showNotification(message) {
const notification = document.createElement('div');
notification.textContent = message;
notification.style.cssText = `
position: fixed;
bottom: 30px;
right: 30px;
background: linear-gradient(45deg, #00C9FF, #92FE9D);
color: white;
padding: 20px 30px;
border-radius: 50px;
font-size: 18px;
font-weight: bold;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
animation: slideIn 0.5s forwards, fadeOut 0.5s 2.5s forwards;
`;
document.body.appendChild(notification);
setTimeout(() => {
document.body.removeChild(notification);
}, 3000);
}
const keyframes = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
`;
const styleSheet = document.createElement("style");
styleSheet.type = "text/css";
styleSheet.innerText = keyframes;
document.head.appendChild(styleSheet);
const savedWebhookUrl = localStorage.getItem('discordWebhookUrl');
const savedLogToDiscord = localStorage.getItem('logToDiscord') === 'true';
if (savedWebhookUrl) {
document.getElementById('discordWebhookUrl').value = savedWebhookUrl;
}
document.getElementById('logToDiscordCheckbox').checked = savedLogToDiscord;
document.getElementById('discordWebhookUrl').disabled = !savedLogToDiscord;
function submitForm(formData) {
return new Promise((resolve, reject) => {
const nodeId = new URLSearchParams(window.location.search).get('node');
formData.set('node_id', nodeId);
formData.set('offer_id', '0');
const data = {};
formData.forEach((value, key) => {
data[key] = value;
});
GM_xmlhttpRequest({
method: 'POST',
url: 'https://funpay.com/lots/offerSave',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: new URLSearchParams(data).toString(),
onload: (response) => {
if (response.status === 200) {
showNotification('Лот успешно продублирован!');
resolve();
} else {
console.error('Ошибка при копировании лота', response);
reject('Ошибка при копировании лота');
}
},
onerror: (error) => {
console.error('Ошибка при выполнении запроса', error);
reject('Ошибка при выполнении запроса');
}
});
});
}
cloneButton.addEventListener('click', () => {
popupMenu.style.display = 'block';
});
document.getElementById('fullClone').addEventListener('click', () => {
popupMenu.style.display = 'none';
const form = document.querySelector('form.form-offer-editor');
if (!form) {
console.error('Форма не найдена');
return;
}
const formData = new FormData(form);
submitForm(formData);
});
document.getElementById('changeCategoryClone').addEventListener('click', () => {
popupMenu.style.display = 'none';
const selects = document.querySelectorAll('select.form-control.lot-field-input, select.form-control[name="server_id"]');
const categoryData = {};
selects.forEach(select => {
const label = select.previousElementSibling ? select.previousElementSibling.textContent.trim() : 'Категория';
if (!categoryData[label]) {
categoryData[label] = [];
}
select.querySelectorAll('option').forEach(option => {
categoryData[label].push({
value: option.value,
text: option.textContent.trim()
});
});
});
const categoryMenu = createElement('div', {}, {
display: 'none',
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: 'gray',
border: '1px solid black',
padding: '20px',
zIndex: '10000'
});
let htmlContent = '';
for (const label in categoryData) {
htmlContent += `<div>`;
htmlContent += `<label><input type="checkbox" id="${label}SelectAll"> Все</label>`;
htmlContent += `<label for="${label}Select">${label}:</label>`;
htmlContent += `<select id="${label}Select" class="form-control" multiple>`;
categoryData[label].forEach(option => {
htmlContent += `<option value="${option.value}">${option.text}</option>`;
});
htmlContent += `</select>`;
htmlContent += `</div>`;
}
htmlContent += `<button id="copyWithCategory" class="btn btn-primary">Копировать</button>`;
htmlContent += `<button id="closeCategoryMenu" class="btn btn-default">Закрыть</button>`;
categoryMenu.innerHTML = htmlContent;
document.body.appendChild(categoryMenu);
categoryMenu.style.display = 'block';
// Добавляем обработчики для чекбоксов "Все"
for (const label in categoryData) {
document.getElementById(`${label}SelectAll`).addEventListener('change', (event) => {
const select = document.getElementById(`${label}Select`);
const options = select.options;
for (let i = 0; i < options.length; i++) {
options[i].selected = event.target.checked;
}
});
}
document.getElementById('copyWithCategory').addEventListener('click', async () => {
categoryMenu.style.display = 'none';
const form = document.querySelector('form.form-offer-editor');
if (!form) {
console.error('Форма не найдена');
return;
}
const selectedCategories = [];
for (const label in categoryData) {
const selectedOptions = Array.from(document.getElementById(`${label}Select`).selectedOptions)
.map(option => option.value);
if (selectedOptions.length > 0) {
selectedCategories.push({
label: label,
selectedOptions: selectedOptions
});
}
}
for (const category of selectedCategories) {
for (const option of category.selectedOptions) {
const clonedFormData = new FormData(form);
if (category.label === 'Категория') {
clonedFormData.set('lot_category', option);
} else {
clonedFormData.set('server_id', option);
}
await submitForm(clonedFormData);
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
document.body.removeChild(categoryMenu);
});
document.getElementById('closeCategoryMenu').addEventListener('click', () => {
document.body.removeChild(categoryMenu);
});
});
document.getElementById('closePopup').addEventListener('click', () => {
popupMenu.style.display = 'none';
});
function replaceEmptyChatWithActiveOrders() {
const emptyChat = document.querySelector('.chat-empty');
if (emptyChat) {
GM_xmlhttpRequest({
method: 'GET',
url: 'https://funpay.com/orders/trade',
onload: (response) => {
if (response.status === 200) {
const parser = new DOMParser();
const doc = parser.parseFromString(response.responseText, 'text/html');
const activeOrders = doc.querySelectorAll('.tc-item.info');
const activeOrdersContainer = createElement('div', { class: 'active-orders-container' }, {
width: '100%',
padding: '10px',
boxSizing: 'border-box',
position: 'absolute',
top: '10px',
left: '10px',
fontSize: '0.67em'
});
activeOrders.forEach(order => {
const statusElement = order.querySelector('.tc-status');
if (statusElement && statusElement.textContent.trim() === 'Оплачен') {
const orderElement = createElement('a', { href: order.href }, {
display: 'block',
marginBottom: '10px',
padding: '5px',
border: '1px solid #ddd',
borderRadius: '5px',
textDecoration: 'none',
color: 'inherit',
transition: 'all 0.3s ease'
});
orderElement.onmouseover = () => {
orderElement.style.backgroundColor = '#f0f0f0';
orderElement.style.transform = 'scale(1.03)';
orderElement.style.boxShadow = '0 2px 5px rgba(0,0,0,0.1)';
};
orderElement.onmouseout = () => {
orderElement.style.backgroundColor = '';
orderElement.style.transform = '';
orderElement.style.boxShadow = '';
};
const dateElement = order.querySelector('.tc-date-time');
if (dateElement) {
const fullDate = dateElement.textContent.trim();
const dateParts = fullDate.split(',');
if (dateParts.length > 0) {
const shortDate = dateParts[0].trim();
orderElement.innerHTML += `<div style="font-weight: bold;">${shortDate}</div>`;
}
}
const descElement = order.querySelector('.order-desc');
if (descElement) {
const descText = descElement.textContent.trim();
orderElement.innerHTML += `<div style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${descText}</div>`;
}
const priceElement = order.querySelector('.tc-price');
if (priceElement) {
orderElement.innerHTML += `<div style="color: green;">${priceElement.textContent.trim()}</div>`;
}
activeOrdersContainer.appendChild(orderElement);
}
});
if (activeOrdersContainer.children.length > 0) {
emptyChat.innerHTML = '';
emptyChat.appendChild(activeOrdersContainer);
emptyChat.style.padding = '0';
}
} else {
console.error('ошибка при загрузке активных заказов', response);
}
},
onerror: (error) => {
console.error('ошибка при выполнении запроса активных заказов', error);
}
});
}
}
replaceEmptyChatWithActiveOrders();
function logNewMessagesToDiscord() {
const unreadMessages = document.querySelectorAll('.contact-item.unread');
unreadMessages.forEach(message => {
const messageId = message.getAttribute('data-id');
const isAlreadySent = localStorage.getItem(`discordSent_${messageId}`);
if (!isAlreadySent) {
sendToDiscordWebhook(message);
localStorage.setItem(`discordSent_${messageId}`, true);
}
});
}
logNewMessagesToDiscord();
})();