Spawner / LNMTL - Improved translation using Sogou

// ==UserScript==
// @name        LNMTL - Improved translation using Sogou
// @namespace   sglnmt
// @match       https://lnmtl.com/chapter/*
// @grant       GM_xmlhttpRequest
// @grant       GM_addStyle
// @grant       GM_setValue
// @grant       GM_getValue
// @require     https://userscripts-mirror.org/scripts/source/107941.user.js#sha384=Q8t880BurrlGKTdpvYv2+da12PYnvljdiU8aJvakk1uE3QMbzb190ueXNpAUY98p
// @version     1.0.7
// @author      Spawner + mmtf
// @description 3/16/2020, 6:10:03 PM
// @license      MIT
// ==/UserScript==

/* 
    CHANGELOG: 
    
        1.0     - Initial Release
        1.0.1   - Small Optimizations
        1.0.2   - Added word color highlighter and changed dialog style
        1.0.3   - Plugin Settings (Credits: mmtf)
        1.0.4   - Code fix + Added Button for Translation
        1.0.5   - Switch to GM_xmlhttpRequest to fix CORS error + Show/Hide translation
        1.0.6   - Bug fixes
        1.0.7   - Removed the private API since it's no longer working + using a different approach.
    
    NOTE: 
    
        - Don't worry about the API (key & id), Sogou just needs a valid/existing hash.
        - I also want to thank mmtf because I have no experience in JS+CSS or web dev in general, so I shamelessly copied parts of his script.

*/

/* CSS STYLES */

GM_addStyle("hgltr { text-shadow: 0 0 10px #e74c3c, 0 0 10px #e74c3c }");
GM_addStyle(".sgt  { color:white; font-size: 2.2rem; margin-bottom:42px; font-family: Roboto }");

/* VARIABLES */

var uniqueWords = new Map();

var autoSogouOn = GM_SuperValue.get('autoSogouOn' , false);
var autoShowOn  = GM_SuperValue.get('autoShowOn'  , false);
var autoGlowOn  = GM_SuperValue.get('autoGlowOn'  , true);

var chunk             = [];
var translatedChunks  = [];
var translatePromises = [];

var isTranslated = false;

window.onload = function ()
{
    replaceRaws();

    createBtnUI();
    createSettings();

    $( '.original' ).css( 'fontSize', '1.9rem' );
    $( '.original' ).css( 'color', 'rgb(128, 142, 155)' );
  
    $( '.translated t' ).each( function ( index )
    {
      var value = $( this )
                    .attr( 'data-title', $( this ).text() )
                    .text()
                    .trimLeft();
      
      uniqueWords.set( value, index );
    } );

    chunks = seperateIntoChunks( getRawParagraphs() );
    chunks[ 0 ]   = chunks[ 0 ].trimLeft();
    
    console.log(chunks.length);
  
    if ( autoSogouOn )
        doWork();   
  
    if( autoShowOn )
        $( '.btn.btn-primary.js-toggle-translated' )[ 0 ].click();
}

async function doWork()
{
    for ( var id = 0; id < chunks.length; id++ )
    {
        // We must set the cookies on each send
        // if not, it will trigger their bot system guard.
        await sogouSetCookies();
        translatedChunks[ id ] = await sogouTranslator( chunks[ id ] );
    }
    
    isTranslated = true;
  
    var finalResult = seperateChunksIntoPars( translatedChunks );
    createSentence( finalResult );
}

function sogouTranslator( text ) 
{   
    const userId = () => {
        return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace( /[xy]/g, function ( name ) {
            var M = 0 | 16 * Math.random();
            var pid = "x" == name ? M : 8 | 3 & M;
            return pid.toString( 16 );
        } );
    };

    return new Promise( ( resolve ) => 
    { 
        var formData = 
        {
            'from'        : 'zh-CHS',
            'to'          : 'en',
            'text'        : text,
            'client'      : 'pc',
            'fr'          : 'browser_pc',
            'pid'         : 'sogou-dict-vr',
            'dict'        : 'true',
            'word_group'  : 'true',
            'second_query': 'true',
            'uuid'        : userId,
            'needQc'      : '1',
            's'           : md5( 'zh-CHS' + 'en' + text + '8511813095152' )
        };
        
        GM_xmlhttpRequest(
            {
                method : "POST",
                url    : "https://fanyi.sogou.com/reventondc/translateV2",
                data   : $.param( formData ),
                headers: 
                {
                    "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
                    "Accept"      : "application/json",
                    "Referer"     : "https://fanyi.sogou.com/"
                }
                ,
                onload: function ( result )
                {
                    var jsonObj = JSON.parse( result.response );
                    resolve( jsonObj.data.translate.dit );
                }
            } );
     });
}

function sogouSetCookies()
{
    var dateExpires = new Date;
    var match       = "sogou.com";
    var id          = "1"; // Must be randomized in the next version
    
    return new Promise( ( resolve ) =>
    {
      GM_xmlhttpRequest(
      {
          method  : "GET",
          url     : "https://fanyi.sogou.com/",
          headers: 
          {
                "Cookie"      : setCookie("SNUID", id, dateExpires.toGMTString(), match, "/")
          }          
          ,
          onload: function ( result ) 
          {
              resolve( result ) 
          }
      });
    });
}

function setCookie(a, val, url, c, name) 
{
    return a = [a, "=", val], url && a.push(";expires=", url), c && a.push(";domain=", c), name && a.push(";path=", name), document.cookie = a.join(""), a.join("");
}


function createSentence( gtpars )
{
    gtpars.forEach( ( sentence, index ) =>
    {
      if ( autoGlowOn )
      {
        for ( const [ key, value ] of uniqueWords.entries() )
            sentence = sentence.replace( new RegExp( '(\\b)(' + key + ')(\\b)', 'gi' ), '<hgltr style="color:orange">' + key + '</hgltr>' );
      }
      
      var quotesStyle = 
          ( sentence.startsWith( '"' ) && sentence.endsWith( '"' ) ) ? 'style="color:rgb(22, 160, 133);font-style:italic;font-weight:bold"': "";
          
      $( '.translated' ).eq( index ).after( "<div class='sgt' " + quotesStyle + " tab-index=0><sentence data-index=" + index + ">" + sentence + "</sentence></div>" );
    });
}

function createBtnUI () 
{
    $( '.js-toggle-original' ).after( '<button class="btn btn-enabled js-toggle-sgt">SGT</button>' );

    $( '.btn.btn-enabled.js-toggle-sgt' )[0].style.borderColor = "white";
    $( '.btn.btn-enabled.js-toggle-sgt' )[0].style.boxShadow   = "0px 0px 20px #ecf0f1";
    $( '.btn.btn-enabled.js-toggle-sgt' )[0].style.color       = "orange";

    $( '.js-toggle-sgt' ).click( function () 
    {
        if ( isTranslated )
            $( ".sgt" ).fadeToggle( 'fast', 'linear' );
      
        if ( !autoSogouOn && !isTranslated ) 
            doWork();
    } )
      .text( 'SGT' ).addClass( 'btn-default' );
}

function createSettings()
{
    let title            = $( '<h3> Improved Translation Settings </h3>' );
    let checked          = autoSogouOn ? 'checked' : '';
    let optionAutoswitch = $( '<sub><input id="autoSogouOn" type="checkbox" ' + checked + '></sub> <label for="autoSogouOn">Automatically display Sogou translation after loading the page.</label>' )
      .on( 'change', function ()
      {
        autoSogouOn = $( '#autoSogouOn' )[ 0 ].checked;
        GM_SuperValue.set( 'autoSogouOn', autoSogouOn );
      });

    let checked2   = autoGlowOn ? 'checked' : '';
    let optionGlow = $( '<sub><input id="autoGlowOn" type="checkbox" ' + checked2 + '></sub> <label for="autoGlowOn">Apply the Glow word highlighter.</label>' )
      .on( 'change', function ()
      {
        autoGlowOn = $( '#autoGlowOn' )[ 0 ].checked;
        GM_SuperValue.set( 'autoGlowOn', autoGlowOn );
      });

    let checked3   = autoShowOn ? 'checked' : '';
    let optionShow = $( '<sub><input id="autoShowOn" type="checkbox" ' + checked3 + '></sub> <label for="autoShowOn"><span style="color:rgb(26, 188, 156)">Show only Translation.</span></label>' )
      .on( 'change', function ()
      {
        autoShowOn = $( '#autoShowOn' )[ 0 ].checked;
        GM_SuperValue.set( 'autoShowOn', autoShowOn );
      });

    let row = $( '<div class="row"/>' );

    $( "#chapter-display-options-modal .modal-body" )
      .append( title )
      .append( optionAutoswitch ).append( '<br>' )
      .append( optionGlow ).append( '<br>' )
      .append( optionShow ).append( '<br>' )
      .append( '<span style="color:yellow">Changes apply after the page is reloaded.</span>' )
      .append( row );
}

function replaceRaws()
{
    $( '.original t' ).each( function ()
    {
      var mytext = $( this ).text();
      $( this ).text( $( this ).attr( 'data-title' ).replace( /\(\w+\)/g, '' ) );
      $( this ).attr( 'data-title', mytext );
    });
    // add whitespace between two english words
    $( '.original' ).find( 't' ).filter( function ( index )
    {
      var prev = $( this ).get( 0 ).previousSibling;
      return prev ? $( this ).get( 0 ).previousSibling.nodeName == 'T': false;
    }).each( function ()
    {
      $( this ).text( ' ' + $( this ).text() );
    });
}

function seperateIntoChunks( paragraphs )
{
    let chunks       = [];
    let currentchunk = "";

    for ( let i = 0; i < paragraphs.length; i++ )
    {
      if ( ( currentchunk + paragraphs[ i ] ).length >= 4000 )
      {
        chunks.push( currentchunk );
        currentchunk = paragraphs[ i ];
      }
      else
        currentchunk = currentchunk + "\n\n" + paragraphs[ i ];
    }

    if ( paragraphs.length != 0 )
      chunks.push( currentchunk );

    return chunks;
}

function seperateChunksIntoPars( chunks )
{
    let pars = [];
    chunks.forEach( ( chunk ) => chunk.split( '\n\n' ).forEach( ( par ) => pars.push( par ) ) );
    return pars;
}

function getRawParagraphs()
{
    return $( '.original' ).text().trim().split( '\n' )
}

function md5( str )
{
    var k = [],
        i = 0;

    for ( i = 0; i < 64; )
      k[ i ] = 0 | ( Math.abs( Math.sin( ++i ) ) * 4294967296 );

    var b, c, d, j,
      x    = [],
      str2 = unescape( encodeURI( str ) ),
      a    = str2.length,
      h    = [ ( b = 1732584193 ), ( c = -271733879 ), ~b, ~c ];

    for ( i = 0; i <= a; )
      x[ i >> 2 ] |= ( str2.charCodeAt( i ) || 128 ) << ( 8 * ( i++ % 4 ) );

    x[ ( str = ( ( a + 8 ) >> 6 ) * 16 + 14 ) ] = a * 8;
      i = 0;

    for ( ; i < str; i += 16 )
    {
      a = h;
      j = 0;

      for ( ; j < 64; )
      {
        a = [
          ( d = a[ 3 ] ),
          ( b = a[ 1 ] | 0 ) +
          ( ( ( d =
                a[ 0 ] + [
                  ( b & ( c = a[ 2 ] ) ) | ( ~b & d ),
                  ( d & b ) | ( ~d & c ),
                  b ^ c ^ d,
                  c ^ ( b | ~d )
                ][ ( a = j >> 4 ) ] +
                ( k[ j ] + ( x[ ( [ j, 5 * j + 1, 3 * j + 5, 7 * j ][ a ] % 16 ) + i ] | 0 ) ) ) <<
              ( a = [ 7, 12, 17, 22, 5, 9, 14, 20, 4, 11, 16, 23, 6, 10, 15, 21 ][
                4 * a + ( j++ % 4 )
              ] ) ) |
            ( d >>> ( 32 - a ) ) ),
          b,
          c
        ];
      }

      for ( j = 4; j; )
      {
        h[ --j ] = h[ j ] + a[ j ];
      }
    }

    str = "";

    for ( ; j < 32; )
    {
      str += ( ( h[ j >> 3 ] >> ( ( 1 ^ ( j++ & 7 ) ) * 4 ) ) & 15 ).toString( 16 );
    }

    return str;
}