Last active
November 7, 2024 07:41
-
-
Save cjmaxik/630b1e0d2c0fb6ca1b3ed6034446e605 to your computer and use it in GitHub Desktop.
Skeb Helper
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Skeb Helper | |
// @namespace http://skeb.jp/ | |
// @version 2024-11-07 | |
// @description Helpful tools for Skeb | |
// @author CJMAXiK | |
// @homepage https://gist.github.com/cjmaxik/630b1e0d2c0fb6ca1b3ed6034446e605 | |
// @match https://skeb.jp/* | |
// @icon https://www.google.com/s2/favicons?sz=64&domain=skeb.jp | |
// @downloadURL https://gist.github.com/cjmaxik/630b1e0d2c0fb6ca1b3ed6034446e605/raw/script.user.js | |
// @updateURL https://gist.github.com/cjmaxik/630b1e0d2c0fb6ca1b3ed6034446e605/raw/script.user.js | |
// @grant GM_xmlhttpRequest | |
// @grant GM_getValue | |
// @grant GM_setValue | |
// @grant GM_deleteValue | |
// @connect cdn.jsdelivr.net | |
// ==/UserScript== | |
let currency | |
let rates | |
let currentPageUrl | |
let lastCreator | |
const try_fee = 1.05 | |
const rub_fee = 1.15 | |
const DEBUG = false | |
const print_debug = (text) => { | |
if (!DEBUG) return | |
console.debug(text) | |
} | |
const makeRequest = (url, additional_headers) => { | |
return new Promise((resolve, reject) => { | |
GM_xmlhttpRequest({ | |
method: 'GET', | |
url, | |
headers: { | |
'Content-Type': 'application/json', | |
...additional_headers | |
}, | |
onload: function (response) { | |
resolve(response.responseText) | |
}, | |
onerror: function (error) { | |
reject(error) | |
}, | |
}) | |
}) | |
} | |
const updateRates = async () => { | |
const url = `https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/jpy.min.json?${Math.random()}` | |
const data = await makeRequest(url) | |
const rates = JSON.parse(data) | |
// 12-hour timeout for the value | |
GM_setValue('timeout', Date.now() + 12 * 3600 * 1000) | |
GM_setValue('rates', rates) | |
print_debug('updateCurrency', rates) | |
return rates | |
} | |
const getRates = async () => { | |
if (currency) return | |
const timeout = GM_getValue('timeout', null) | |
const cachedRates = GM_getValue('rates', null) | |
const rateDate = cachedRates ? Date.parse(cachedRates.date) : Date.now() | |
print_debug('getRates CACHE', timeout, cachedRates) | |
// No cache OR no timeout OR timeout is after the current date OR rate internal date is after 2 days from now (failsafe) | |
if ( | |
!cachedRates || | |
!timeout || | |
timeout <= Date.now() || | |
rateDate + 48 * 3600 * 1000 <= Date.now() | |
) { | |
currency = await updateRates() | |
print_debug('getRates NEW', currency) | |
} else { | |
currency = cachedRates | |
print_debug('getRates CACHED', currency) | |
} | |
} | |
const findElements = async (node) => { | |
currentPageUrl = window.location.href | |
if (!node) { | |
print_debug('-- Node is empty') | |
node = document.querySelector('section.section') | |
} | |
if (node === null) { | |
print_debug('-- No nodes') | |
return | |
} | |
let pricesToUpdate = [] | |
let timestampsToUpdate = [] | |
// 1. Profile page, left column | |
if (currentPageUrl.includes("skeb.jp/@")) { | |
print_debug("-- Profile page, left column") | |
// Recommended amount | |
const elements = node.querySelectorAll("small:not(.done)") | |
// Cannot use `forEach` because we need i+1 element | |
for (let i = 0; i < elements.length; i++) { | |
if (elements[i].innerText === 'Recommended amount') pricesToUpdate.push([elements[i + 1], true]) | |
} | |
} | |
// 2. Requests | |
if (currentPageUrl.includes('skeb.jp/requests') || currentPageUrl.includes('skeb.jp/appeals/')) { | |
print_debug("-- Requests page") | |
// Price, timestamps | |
node.querySelectorAll('span.tag:not(.done)').forEach(element => { | |
var text = element.innerText | |
// Price | |
if (text.includes('JPY')) pricesToUpdate.push([element, false]) | |
// Timestamps | |
if (text.includes(' AM') || text.includes(' PM')) timestampsToUpdate.push([element]) | |
}) | |
} | |
// 3. Charges | |
if (currentPageUrl.includes('skeb.jp/charges')) { | |
print_debug("-- Charges page") | |
// Timestamps | |
node.querySelectorAll('tbody > tr td:nth-child(3):not(.done)').forEach(element => { | |
if (element.innerText.includes('/')) timestampsToUpdate.push([element, false]) | |
}) | |
// Price | |
node.querySelectorAll('tbody > tr td:nth-child(6):not(.done)').forEach(element => { | |
if (element.innerText.includes('JPY')) pricesToUpdate.push([element, true]) | |
}) | |
} | |
// 4. Work page | |
if (currentPageUrl.includes('/works/')) { | |
print_debug("-- Works page") | |
if (document.querySelector('table#miniProfile')) return | |
const creatorName = currentPageUrl.split('/')[3].replace('@', '') | |
const response = await makeRequest(`https://skeb.jp/api/users/${creatorName}`, getBearerToken()) | |
lastCreator = JSON.parse(response) | |
putCreatorData() | |
} | |
if (pricesToUpdate.length || timestampsToUpdate.length) print_debug('Elements to update:', pricesToUpdate, timestampsToUpdate) | |
pricesToUpdate.forEach(x => injectPrice(...x)) | |
timestampsToUpdate.forEach(x => injectTimestamp(...x)) | |
} | |
const injectPrice = async (element, full = true) => { | |
const jpyPrice = element.innerText | |
.replace(/[^0-9.,-]+/g, '') | |
.replace('.', '') | |
.replace(',', '') | |
const convertedPrice = convertPrice(jpyPrice) | |
if (convertedPrice.rub >= 5000) { | |
element.style.color = '#FF0000' | |
} else if (convertedPrice.rub >= 2500) { | |
element.style.color = '#FFFF00' | |
} | |
element.title = element.innerText | |
element.innerHTML = full ? `≈${convertedPrice.rub} ₽<br/>≈${convertedPrice.try} TRY` : `≈${convertedPrice.rub} ₽, ≈${convertedPrice.try} TRY` | |
element.classList.add('done') | |
} | |
const convertPrice = (price) => { | |
return { | |
try: Math.floor((price * rates.try) * try_fee), | |
rub: Math.floor((price * rates.rub) * rub_fee) | |
} | |
} | |
const formatDays = (time) => { | |
let text = "Unknown" | |
if (time) { | |
const days = Math.floor(time / 60 / 60 / 24) | |
if (days) { | |
text = `${days} days` | |
} else { | |
text = "< 1 day" | |
} | |
} | |
return text | |
} | |
const today = new Date() | |
const injectTimestamp = (element, withTime = true) => { | |
const date = new Date(element.innerText) | |
element.title = element.innerText | |
if (withTime) { | |
element.innerText = date.toLocaleString() | |
} else { | |
const diff = formatDays((today - date) / 1000) | |
element.innerText = `${date.toLocaleDateString()} (${diff})` | |
if (diff >= 150) element.style.color = "#FF0000" | |
} | |
element.classList.add('done') | |
} | |
const putCreatorData = () => { | |
if (!lastCreator) { | |
print_debug("No lastCreator") | |
return | |
} | |
if (document.querySelector('table#miniProfile')) return | |
const element = document.querySelector('section.section div.is-divider') | |
const table = ` | |
<table class="table is-fullwidth is-narrow" id="miniProfile"> | |
<tbody> | |
${miniProfile()} | |
</tbody> | |
</table> | |
<div data-v-63c9f59d="" class="is-divider"></div> | |
` | |
element.insertAdjacentHTML('afterEnd', table) | |
} | |
const skillGenre = { | |
art: 'Artwork', | |
comic: 'Comic', | |
voice: 'Voice', | |
novel: 'Text', | |
video: 'Video', | |
music: 'Music', | |
correction: 'Advice' | |
} | |
const miniProfile = () => { | |
let template = "" | |
if (!lastCreator.acceptable) { | |
template += ` | |
<tr> | |
<td><small style="color: #FF0000;">Not seeking</small></td> | |
<td></td> | |
</tr> | |
` | |
} | |
lastCreator.skills.forEach((skill) => { | |
let type = skillGenre[skill.genre] ?? 'Unknown' | |
var convertedPrice = convertPrice(skill.default_amount) | |
let style | |
if (convertedPrice.rub >= 5000) { | |
style = 'color: #FF0000;' | |
} else if (convertedPrice.rub >= 2500) { | |
style = 'color: #FFFF00;' | |
} | |
template += ` | |
<tr> | |
<td><small>${type}</small></td> | |
<td><small title="JPY ${skill.default_amount}" style="${style}">≈${convertedPrice.rub} ₽<br/>≈${convertedPrice.try} TRY</small></td> | |
</tr> | |
` | |
}) | |
template += daysTemplate(lastCreator.received_requests_average_response_time, "Response average") | |
template += daysTemplate(lastCreator.completing_average_time, "Complete average") | |
template += ` | |
<tr> | |
<td><small>Complete rate</small></td> | |
<td><small style="${lastCreator.complete_rate < 0.95 ? 'color: #ff0000;' : ''}"> | |
${lastCreator.complete_rate * 100}% | |
</small></td> | |
</tr> | |
` | |
return template | |
} | |
const daysTemplate = (time, text) => { | |
const days = formatDays(time) | |
const daysNumber = Number(days.replace("< ", "").replace(" day", "").replace("s", "")) | |
let style = "" | |
if (time === undefined || daysNumber === NaN || daysNumber > 60) { | |
style = "color: #FF0000;" | |
} | |
return ` | |
<tr> | |
<td><small>${text}</small></td> | |
<td><small style="${style}">${days}</small></td> | |
</tr> | |
` | |
} | |
const getBearerToken = () => { | |
return { | |
'Authorization': `Bearer ${window.localStorage.token}` | |
} | |
} | |
const main = async () => { | |
'use strict' | |
// Updating currency | |
await getRates() | |
print_debug('Current rates', currency) | |
// Grabbing the rate | |
rates = { | |
try: Math.round(currency.jpy.try * 100) / 100, | |
rub: Math.round(currency.jpy.rub * 100) / 100 | |
} | |
print_debug('Effective rate', rates) | |
// Injecting prices for the first time | |
await findElements() | |
// Dynamically inject prices | |
const observer = new MutationObserver(async (mutations, _observer) => { | |
for (const mutation of mutations.filter(x => x.addedNodes.length > 0)) { | |
const node = mutation.addedNodes[0] | |
if (["LINK", "STYLE", "SCRIPT", "NOSCRIPT"].includes(node.tagName)) continue | |
if (node.tagName === undefined) continue | |
await findElements(node) | |
} | |
}) | |
observer.observe(document.body, { | |
childList: true, | |
subtree: true, | |
attributes: true, | |
}) | |
} | |
window.onload = main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment