CSSBuy Suite

Automatically upload QC images from CSSBuy to Imgur

As of 2020-06-05. See the latest version.

// ==UserScript==
// @name         CSSBuy Suite
// @namespace    https://www.reddit.com/user/DeliciousLysergic/
// @version      0.5
// @description  Automatically upload QC images from CSSBuy to Imgur
// @author       https://www.reddit.com/user/DeliciousLysergic/
// @match        https://www.cssbuy.com/?go=m&name=orderlist*
// @match        https://www.cssbuy.com/m.php?name=orderlist
// @connect      self
// @connect      imgur.com
// @connect      fashionreps.tools
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/build/alertify.min.js
// @require      https://greasyfork.org/scripts/401399-gm-xhr/code/GM%20XHR.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/spark-md5/3.0.0/spark-md5.min.js
// @grant        GM_xmlhttpRequest
// @run-at       document-end
// ==/UserScript==
'use strict';
/* globals $:false, SparkMD5:false, GM_XHR: false */

let CSSBuySuite = {}

// Stores each order's information
let orderInfo = {}

// Used to store each item's data
var items = {};

// Select random application ID for imgur
const clientIds = ["1ffdbfe5699ecec", "447a420679198a0", "956dd191d8320c9", "efb0b998924ef3c", "6f8eb75873d5f89", "9af4f2c84a12716"];
const clientId = clientIds[Math.floor(Math.random()*clientIds.length)]
// Time it takes for the image URLs to load in the iFrame in milliseconds
const imageLoadingTime = 5000; 

// Stores the MD5 of the user's username (used for fashiontools)
let usernameMD5 = "";

// Setup GM_XHR
$.ajaxSetup({ xhr: function() {return new GM_XHR; } });

// Configure alertify
alertify.set('notifier', 'position', 'bottom-left');
// Add styling for alertify
$("head").append(`
<!-- CSS -->
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/[email protected]/build/css/alertify.min.css"/>
<!-- Bootstrap theme -->
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/[email protected]/build/css/themes/bootstrap.min.css"/>
`);

const redditLink = "TODO";

// Used for setting buttons as disabled
CSSBuySuite.setDisabledButton = function(button, disabled) {
    if (disabled) 
    {
        button.attr("disabled", "disabled");
        button.text("Uploading...");
    }
    else
    {
        button.attr("disabled", null);
        button.text("Upload to Imgur");
    }
}

CSSBuySuite.createAlbumForOrder = function(orderId)
{
    let albumHash = "";
    let albumId = "";

    orderInfo[orderId].infoLabel.text("Creating Imgur album...");

    $.ajax({
        url: "https://api.imgur.com/3/album",
        method: "POST",
        headers: {"Authorization": "Client-ID " + clientId},
        data: {
            description: "Auto uploaded using CSSBuySuite. More info here: " + redditLink,
            privacy: "hidden"
        },
        success: function(response) {
            // Retrieve the delete hash and the ID of the album from the response
            albumHash = response.data.deletehash;
            albumId = response.data.id;
            orderInfo[orderId].albumHash = albumHash;
            orderInfo[orderId].albumId = albumId;
            
            // An album has been created for this, now we can upload the images
            console.log("Album has been created for " + orderId + ". Now uploading images...");
            CSSBuySuite.uploadImagesToAlbum(orderId);
        },
        error: function(response) {
            result = $.parseJSON(result.responseText)
            // Update text
            orderInfo[orderId].infoLabel.text("Failed to create album! Imgur Error: " + result.data.error.message);
            orderInfo[orderId].uploading = false;
            // Display alert
            alertify.error("Failed to create an album for order " + orderId + ". Error: " + result.data.error.message);
            // Re-enable the upload button
            setDisabled(orderInfo[orderId].uploadButton, false)
            console.log("Failed to create album:");
            console.log(result.data.error)
        }
    });
}

CSSBuySuite.uploadImagesToAlbum = function(orderId)
{
    // Stores how many images have been uploaded
    let imageCount = 0;
    // Stores total amount of images we have to upload
    let totalImages = orderInfo[orderId].qcImages.length;
    // Stores the delete hashes of the images we've uploaded
    let hashes = [];
    
    // Update label
    orderInfo[orderId].infoLabel.text("Uploading images to Imgur... (0/" + totalImages + ")");

    // Create the description for the item (inserted into each image)
    let description = "";
    description = description + (orderInfo[orderId].itemUrl != "" ? "W2C: " + orderInfo[orderId].url + "\n": "");
    description = description + (orderInfo[orderId].weight != "" ? "Weight: " + orderInfo[orderId].weight + " grams\n": "");
    description = description + (orderInfo[orderId].price != "" ? "Price: ¥" + toFixed(orderInfo[orderId].price, 2) + "\n": "");
    description = description + (orderInfo[orderId].sizing != "" ? "Item Info: " + orderInfo[orderId].sizing + "\n": "");

    // Form data for the uploading of each image
    imageFormData = {
        album: orderInfo[orderId].albumHash,
        description: description
    }

    // For each image ID
    orderInfo[orderId].qcImages.forEach(function(image) {
        // Update the form data with this image's URL
        // While we can't access another user's CSSBuy QC page, the images themselves have no access control
        // That's why we can use the image URLs
        imageFormData.image = image;
        
        // Now we can upload the image
        $.ajax({
            url: "https://api.imgur.com/3/image",
            method: "POST",
            headers: {"Authorization": "Client-ID " + clientId},
            data: imageFormData,
            success: function(result) {
                // Add the image's delete hash to hashes
                hashes.push(result.data.deletehash);
                imageCount += 1;

                // Update the label
                orderInfo[orderId].infoLabel.text("Uploading images to Imgur... (" + imageCount + "/" + totalImages + ")");
                
                // Now check whether we've uploaded everything
                if(imageCount == totalImages)
                {
                    // We have, so call uploadFinished
                    CSSBuySuite.uploadFinished(orderId);
                }
            },
            error: function(result) {
                result = $.parseJSON(result.responseText)
                // Update text
                orderInfo[orderId].infoLabel.text("Failed to create album! Imgur Error: " + result.data.error.message);
                orderInfo[orderId].uploading = false;
                // Display alert
                alertify.error("Failed to create an album for order " + orderId + ". Error: " + result.data.error.message);
                // Re-enable the upload button
                setDisabled(orderInfo[orderId].uploadButton, false);
            }
        });
    });
}

CSSBuySuite.uploadFinished = function(orderId)
{
    const albumUrl = "https://imgur.com/a/" + orderInfo[orderId].albumId;

    console.log("All images uploaded for " + orderId + ": " + albumUrl);

    // Save the URL
    try
    {
        window.localStorage.setItem("orderalbum_" + orderId, orderInfo[orderId].albumId);
    }
    catch(error)
    {
        console.log("Failed to save order album into local storage.");
        console.log(error);
    }
    
    orderInfo[orderId].uploading = false;
    CSSBuySuite.setDisabledButton(orderInfo[orderId].uploadButton, false);

    // Show the new album ID
    CSSBuySuite.updateOrderAlbumDisplay(orderId);

    // Now attempt to upload to Fashiontools
    CSSBuySuite.checkForFashiontoolsUpload(orderId);
}

// This updates a order with the new link for their album
CSSBuySuite.updateOrderAlbumDisplay = function(orderId)
{
    const albumUrl = "https://imgur.com/a/" + orderInfo[orderId].albumId;
    orderInfo[orderId].infoLabel.text("Uploaded to ");
    orderInfo[orderId].infoLink.text(albumUrl);
    orderInfo[orderId].infoLink.attr("href", albumUrl);
    orderInfo[orderId].copyButton.show();
    orderInfo[orderId].copyButton.attr("onclick", "copyToClipboard('" + albumUrl + "')");
}

CSSBuySuite.clearLink = function(orderId)
{
    orderInfo[orderId].copyButton.hide()
    orderInfo[orderId].infoLink.text("")
}

// This checks whether the URL + sizing is valid and that we have a username to upload with
CSSBuySuite.checkForFashiontoolsUpload = function(orderId)
{
    // Firstly check whether we have a sizing parameter for this order
    if(orderInfo[orderId].sizing == "")
        return;
    
    // Check whether our url is from taobao
    if(orderInfo[orderId].url.indexOf('item.taobao.com') == -1)
        return;
    
    // Check whether we have a username stored, if not, retrieve it then upload
    if(usernameMD5 == "")
    {
        // Go to the cssbuy homepage and get the username
        usernameiFrame = $("<iframe>", {
            style: "display: none;",
            src: "https://www.cssbuy.com/?go=m&"
        });
        usernameiFrame.on("load", () => CSSBuySuite.collectUsername());
        usernameiFrame.insertAfter($("form[name='search-form']"));
        setTimeout(function() {
            CSSBuySuite.uploadToFashiontools(orderId);
        }, 3500);
    }
    else
    {
        // We already have a username, let's upload!
        CSSBuySuite.uploadToFashiontools(orderId);
    }
}

CSSBuySuite.uploadToFashiontools = function(orderId)
{
    console.log("Attemping to upload to fashiontools.");
    // Get the search query parameters
    const urlParams = new URLSearchParams(orderInfo[orderId].url.split("?").pop());
    const itemID = urlParams.get('id');

    $.post('https://fashionreps.tools/qcdb/qcdb.php', {
            'userhash': usernameMD5,
            'imgur': orderInfo[orderId].albumId,
            'w2c': 'https://item.taobao.com/item.htm?id=' + itemID,
            'sizing': orderInfo[orderId].sizing,
            'source': "cssbuyUploader"
    });
}

// iFrame helpers
CSSBuySuite.collectUsername = function()
{
    // Get the username from the homepage
    const username = $("a[href='https://www.cssbuy.com/?go=m&name=edituserinfo']", usernameiFrame.contents()).parent().find("span").text()
    console.log("Found username: " + username);
    usernameMD5 = SparkMD5.hash(username);
    console.log("Username MD5: " + usernameMD5);
}

CSSBuySuite.collectPictures = function(orderId) {
    // Check that we are uploading this order atm
    // onload is called when iFrame is initialised
    if(!(orderId in orderInfo) || !orderInfo[orderId].uploading)
        return;

    // Wait a few seconds to ensure all the images have loaded...
    setTimeout(function() {
        // Check iFrame for all images
        // Add images to a list in form [url, base64]
        let urls = [];
        
        // Retrieve all the images
        orderInfo[orderId].iFrame.contents().find("#photolist").find("img").each(function() {
            let url = $(this).attr("src");

            urls.push(url);
        });

        orderInfo[orderId].qcImages = urls

        if(!urls.length)
        {
            orderInfo[orderId].infoLabel.text("Found no QC images!")
        }
        else
        {
            orderInfo[orderId].infoLabel.text("Found " + urls.length + " QC pictures...")

            CSSBuySuite.createAlbumForOrder(orderId);
        }
    }, imageLoadingTime);
};

CSSBuySuite.uploadOrder = function(orderId) {
    // If we are already uploading this order, return
    if(orderInfo[orderId].uploading)
        return;

    // Update label
    CSSBuySuite.clearLink(orderId);
    orderInfo[orderId].infoLabel.text("Getting QC pictures...");
    orderInfo[orderId].uploading = true;

    // Disable button
    orderInfo[orderId].uploading = true;
    CSSBuySuite.setDisabledButton(orderInfo[orderId].uploadButton, true)

    // Update iFrame to get pictures
    orderInfo[orderId].iFrame.attr("src", orderInfo[orderId].qcUrl);
}

// Add buttons for each of the orders
var buttonSelector = ".oss-photo-view-button > a:contains('QC PIC')";

$(buttonSelector).each(function() {
    const orderId = $(this).parent().attr("data-id");

    // Create the "Upload to Imgur" button
    const qcUrl = $(this).attr("href");
    const uploadButton = $("<a>", {
        class: "btn btn-sm btn-default margin-bottom-5", 
        text: "Upload to Imgur",
        orderId: orderId, 
        style: "background: #1bb76e; border: 1px solid #1bb76e; color: white;"
    });
    uploadButton.appendTo($(this).parent());
    // Add click handler
    uploadButton.on("click", () => CSSBuySuite.uploadOrder(orderId));

    // Get parent TR element to retrieve item name & URL & create progress label
    const parentTableEntry = $(this).parentsUntil("tbody");
    const header = $("td div div:nth-child(2)", parentTableEntry.prev())

    // Create progress label
    const label = $("<span>", {
        text: "", 
        style: "font-weight: bold; margin-left: 40px;"
    });
    label.appendTo(header);

    // Create album link
    const link = $("<a>", {style:"margin-right: 10px;"});
    link.appendTo(header);

    // Create copy butotn
    const copyButton = $("<a>", {
        text: "Copy to Clipboard", 
        style: "padding: 3px; font-size: 1em; cursor: pointer;",
    })
    copyButton.hide()
    copyButton.appendTo(header)

    // Get item name/URL
    const itemLink = parentTableEntry.find("td:nth-child(2) a");
    const itemURL = itemLink.attr("href");
    const price = parentTableEntry.find("td:nth-child(3) span");
    const weight = parentTableEntry.find("td:nth-child(6) span");
    let sizing = "";

    const innerText = parentTableEntry.find("td:nth-child(2)").find("span:eq(1)").html();
    const splitText = innerText.toString().split("<br>");
    if (splitText.length == 1)
    {
        const colour = splitText[0].split(" : ")[1];
        sizing = "Color Classification:"+colour;
    }
    else if(splitText.length == 2)
    {
        const size = splitText[0].split(" : ")[1];
        const colour = splitText[1].split(" : ")[1];
        sizing = "size:" + size + " Colour:"+colour;
    }
    else
    {
        sizing = "";
    }
        
    // Create iframe
    const iFrame = $("<iframe>", {
        style: "display: none;"
    });
    // Add onload listener
    iFrame.on("load", () => CSSBuySuite.collectPictures(orderId));
    iFrame.insertAfter($("form[name='search-form']"));
    
    // Create new item entry
    orderInfo[orderId] = {
        orderId: orderId,
        name: itemLink.text(),
        url: itemURL,
        price: toFixed(price.text(), 2),
        weight: weight.text(),
        sizing: sizing,
        qcUrl: qcUrl,
        qcImages: [],
        copyButton: copyButton,
        infoLink: link,
        infoLabel: label,
        iFrame: iFrame,
        uploading: false,
        uploadButton: uploadButton,
        albumHash: "",
        albumId: ""
    }

    // Check whether they already have a album saved for this id
    let storedAlbum = window.localStorage.getItem("orderalbum_" + orderId);
    if(storedAlbum !== null)
    {
        orderInfo[orderId].albumId = storedAlbum;
        CSSBuySuite.updateOrderAlbumDisplay(orderId);
    }

    // Create Compare QC button
    // URL is in form: https://fashionreps.tools/qcdb/qcview.php?id=616820737680
    if(itemURL.indexOf('item.taobao.com') !== -1)
    {
        // Get the search query parameters
        const urlParams = new URLSearchParams(itemURL.split("?").pop());
        const itemID = urlParams.get('id');
        if(itemID !== null)
        {
            const compareQC = $("<a>", {
                class: "btn btn-sm btn-default margin-bottom-5", 
                text: "View Other QC",
                style: "background: #1b8bb7; border: 1px solid #1b8bb7; color: white;",
                href: "https://fashionreps.tools/qcdb/qcview.php?id=" + itemID,
                target: "_blank"
            });
            compareQC.appendTo($(this).parent());
        }
    }
    
});

// We only need to display the reddit review button on the confirmed page
if(window.location.href.includes("confirmed"))
{
    const tabContainer = $("#submit_form").parent().parent();
    // Create reddit review button
    const redditReviewButton = $("<button>", {
        id: "createRedditReview",
        type: "button",
        class: "btn-u btn-u-xs",
        style: "background-color: red; margin-left: 4px;",
        text: "Create Reddit Review"
    });
    tabContainer.prepend(redditReviewButton);

    // Now go through all the checkboxes and re-enable them
    $("input.products").each(function() { 
        $(this).removeAttr("disabled");
        $(this).parent().removeClass("state-disabled");
    });
    redditReviewButton.on("click", function() {
        // Get all the ticked orders
        let tickedOrderIDs = [];

        $(".products:checked").each(function() {
            // "value" contains the order ID
            tickedOrderIDs.push(parseInt($(this).attr("value")));
        });

        if(!tickedOrderIDs.length)
        {
            alertify.error("Please select some items!");
            return;
        }
        
        let redditReview = "";

        tickedOrderIDs.forEach(orderId => {
            redditReview += `
    **${orderInfo[orderId].name}**

    * ${orderInfo[orderId].price} Yuan - ${orderInfo[orderId].weight}g
    * W2C: ${orderInfo[orderId].url}
    * 10/10: Insert a comment about this item here!
    ${orderInfo[orderId].albumId == "" ? "" : "* QC: https://imgur.com/a/" + orderInfo[orderId].albumId}
    `;
        });

        redditReview += "\n\nReview format made by CSSBuySuite - read more here: " + redditLink;
        
        // Copy to clipboard
        copyToClipboard(redditReview);
        alertify.success("Copied the review to your clipboard!");
    });
}

// Helper functions
// https://stackoverflow.com/a/11818658
function toFixed(num, fixed) {
    var re = new RegExp('^-?\\d+(?:\.\\d{0,' + (fixed || -1) + '})?');
    return num.toString().match(re)[0];
}

window.copyToClipboard = function(text) {
    var $temp = $("<textarea>");
    $("body").append($temp);
    $temp.val(text).select();
    document.execCommand("copy");
    $temp.remove();
}