Skip to content

Instantly share code, notes, and snippets.

@cjmaxik
Last active November 7, 2024 07:41
Show Gist options
  • Save cjmaxik/630b1e0d2c0fb6ca1b3ed6034446e605 to your computer and use it in GitHub Desktop.
Save cjmaxik/630b1e0d2c0fb6ca1b3ed6034446e605 to your computer and use it in GitHub Desktop.
Skeb Helper
// ==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