Skip to content

Instantly share code, notes, and snippets.

@Karmalakas
Last active June 23, 2024 18:52
Show Gist options
  • Save Karmalakas/25910e649f2feb26e4e8298af938a09e to your computer and use it in GitHub Desktop.
Save Karmalakas/25910e649f2feb26e4e8298af938a09e to your computer and use it in GitHub Desktop.
GuruShots end time
// ==UserScript==
// @name GuruShots end time
// @description Show ending time in GuruShots next to countdown timer
// @namespace http://karmalakas.lt/
// @version 1.14.0
// @author Karmalakas
// @updateURL https://gist.github.com/Karmalakas/25910e649f2feb26e4e8298af938a09e/raw/GS_End_Time.user.js
// @downloadURL https://gist.github.com/Karmalakas/25910e649f2feb26e4e8298af938a09e/raw/GS_End_Time.user.js
// @supportURL https://gist.github.com/Karmalakas/25910e649f2feb26e4e8298af938a09e
// @match https://gurushots.com/*
// @require http://code.jquery.com/jquery-3.5.1.slim.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.27.0/moment.min.js
// @run-at document-idle
// @grant GM_addStyle
// ==/UserScript==
(function($) {
'use strict';
GM_addStyle('' +
'.gs-challenge__countdown {padding-bottom: 1px;}' +
'.c-challenges-item__title .TM-timer-end-date {' +
' bottom: 22px;' +
' font-size: .95em;' +
'}' +
'.challenges-exhibition-banner__title .TM-timer-end-date {' +
' bottom: 24px;' +
' position: absolute;' +
' text-align: center;' +
' width: 100%;' +
' color: #FFFFFF;' +
'}' +
'.c-challenges-speed-item__countdown .TM-timer-end-date,' +
'.challengesItemSuggested__timer__wrap .TM-timer-end-date {' +
' top:33%;' +
' color:#b4ada4;' +
'}' +
'.match-active__top__info__c-timer .TM-timer-end-date {' +
' text-align: right;' +
' color: white;' +
' font-weight: bold;' +
' font-size: 14px;' +
'}' +
'.gs-challenge__data .gs-challenge__match-timer,' +
'.gs-challenge__data .gs-challenge__match-timer .gs-challenge__countdown,' +
'#page leaderboard-page leaderboard-header league-timer {' +
' width: auto;' +
' padding: 0 5px;' +
'}' +
'.gs-challenge__data .gs-challenge__match-timer .gs-challenge__countdown.TM-timer-team {' +
' border-right: 1px solid white;' +
' color: white;' +
'}' +
'.TM-timer-list,' +
'.TM-timer-team {' +
' font-size: 12px;' +
' font-weight: normal;' +
'}' +
'p > .TM-timer-team {' +
' font-weight: bold;' +
'}' +
'.leader-board__modal__timer > .TM-timer-team {' +
' font-weight: bold;' +
' font-size: 12px;' +
' margin-bottom: 0;' +
'}'+
'.team-leaderboard__main-header__content-timer div.TM-timer-league,' +
'#page leaderboard-page leaderboard-header league-timer div.TM-timer-league {' +
' border-right: 1px solid #3397d2;' +
' padding-right: 4px;' +
' margin-right: 3px;' +
'}' +
'.team-leaderboard__main-header__content-timer div.TM-timer-league {' +
' font-size: 12px;' +
' color: #3397d2;' +
' margin-bottom: inherit;' +
'}' +
'#page leaderboard-page leaderboard-header league-timer div.TM-timer-league {' +
' font-weight: 400;' +
'}' +
'challenges-item div.TM-timer-boost-holder {' +
' height: auto;' +
' width: auto;' +
' left: -17px;' +
' top: -5px;' +
' right: auto !important;' +
' padding: 2px !important;' +
' padding-left: 9px !important;' +
' border-top-right-radius: 10px;' +
' border-bottom-right-radius: 10px;' +
'}' +
'challenges-item div.TM-timer-boost {' +
' font-size: .9em;' +
'}' +
'challenges-upcoming gs-challenge footer .soon .starts,' +
'challenge-details .c-tab-view__stat__item--soon p > div {' +
' display: grid;' +
' row-gap: 5px;' +
' column-gap: 5px;' +
' grid-template-rows: auto;' +
' grid-template-columns: auto auto;' +
' justify-content: space-between;' +
' padding: 0 10px;' +
'}' +
'challenges-upcoming gs-challenge footer .soon .starts div:nth-child(odd),' +
'challenge-details .c-tab-view__stat__item--soon p > div div:nth-child(odd) {' +
' text-align: left;' +
'}' +
'challenges-upcoming gs-challenge footer .soon .starts div:nth-child(even),' +
'challenge-details .c-tab-view__stat__item--soon p > div div:nth-child(even) {' +
' text-align: right;' +
'}' +
'challenge-details .c-tab-view__stat__item--soon p > div {' +
' font-size: .75em;' +
'}'
);
var mutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
var timerSelectors = ['gs-timer'];
var timeoutWarnings = {};
function process() {
var timers = document.querySelectorAll('gs-timer');
if (!timers.length) {
return;
}
for (var i = 0; i < timers.length; i++) {
processTimer(
timers[i],
getTimerType(timers[i])
);
}
}
function getTimerType(timer) {
var timerParent = $(timer).parent();
if (timerParent.hasClass('c-challenges-item__title')) {
return 'daysLeft';
} else if (timerParent.hasClass('c-challenges-speed-item__countdown') || timerParent.hasClass('challengesItemSuggested__timer__wrap')) {
return 'circleBar';
} else if (timerParent.hasClass('gs-challenge')) {
return 'list';
} else if (timerParent.hasClass('match-active__top__info__c-timer')) {
return 'match';
} else if (timerParent.hasClass('team-leaderboard__main-header__content-timer') || timerParent.hasClass('league-timer')) {
return 'league';
} else if (timerParent.hasClass('gs-challenge__match-timer')) {
return 'teamMatch';
} else if (timerParent.hasClass('challenges-exhibition-banner__title')) {
return 'banner';
} else if (timerParent.hasClass('') === true) {
return 'other';
}
}
function processTimer(timer, type) {
if ($(timer).prev().hasClass('TM-timer-end-date')) {
return;
}
switch(type) {
case 'daysLeft':
break;
case 'circleBar':
processTimerCircleBar(timer);
break;
case 'list':
processTimerList(timer);
break;
case 'match':
processTimerMatch(timer);
break;
case 'league':
processTimerLeague(timer);
break;
case 'teamMatch':
processTimerTeamMatch(timer);
break;
default:
processTimerOther(timer);
break;
}
}
function processTimerCircleBar(timer) {
timer = $(timer);
var duration = moment.duration(
timer.find('.timer__digit, .timer__delimiter').text()
);
addDate(timer, duration, 'c-challenges-speed-item__countdown__timer');
}
function processTimerList(timer) {
timer = $(timer);
addDate(timer, getBasicDuration(timer), 'TM-timer-list');
}
function processTimerMatch(timer) {
timer = $(timer);
addDate(timer, getBasicDuration(timer), 'TM-timer-match');
}
function processTimerLeague(timer) {
addDate($(timer), getStringDuration(timer), 'TM-timer-league');
}
function processTimerOther(timer) {
timer = $(timer);
addDate(timer, getBasicDuration(timer), 'TM-timer-match');
}
function processTimerTeamMatch(timer) {
var duration = getStringDuration(timer);
timer = $(timer);
addDate(timer, duration, timer.attr('class') + ' TM-timer-team');
}
function getBasicDuration(timer) {
var text = timer.text().replace(/(h|m|s)/g, '');
text = text.trim().replace(/\s+/g, ':');
return moment.duration(text);
}
function getStringDuration(timer) {
var text = timer.textContent || timer.innerText || '',
timeMatches = [...text.trim().matchAll(/(?:(\d+)d\s?)?(?:(\d+)h\s?)?(?:(\d+)m\s?)?(?:(\d+)s\s?)?/gi)];
return moment.duration({
seconds: parseInt(30),
minutes: parseInt(timeMatches[0][3] || 0, 10),
hours: parseInt(timeMatches[0][2] || 0, 10),
days: parseInt(timeMatches[0][1] || 0, 10)
});
}
function addDate(timer, duration, cssClass, format) {
format = format || 'ddd HH:mm';
var date = moment().add(duration);
// Lets round to 1 minute (there are challenges ending like xx:14)
var coeff = 60 * 1;
date = moment.unix(Math.round(date.unix() / coeff) * coeff)
timer.before(
$('<div class="' + (cssClass || '') + ' TM-timer-end-date">' + date.format(format) + '</div>')
);
}
function processMemberChallenges(response, retry_count)
{
if (response.success !== true || !Array.isArray(response.items)) {
return;
}
var domChallenges = awaitDomToLoad('challenges-upcoming gs-challenge');
if (domChallenges === false) {
return;
}
var i, challenge, challengeData, startsHolder;
for (i = 0; i < domChallenges.length; i++) {
challenge = domChallenges[i];
challengeData = response.items.find(o => `"${o.title}"` === challenge.querySelector('a').textContent);
challenge.querySelector('footer .soon .starts').innerHTML = '<div>Start:</div><div>' + moment.unix(challengeData.start_time).format('Do MMM, ddd HH:mm') + '</div><div>End:</div><div>' + moment.unix(challengeData.close_time).format('Do MMM, ddd HH:mm') + '</div>';
}
}
function processChallenge(response)
{
if (response.success !== true || typeof(response.challenge) !== 'object') {
return;
}
var upcomingSingleChallengeSoon = awaitDomToLoad('challenge-details .c-tab-view__stat__item--soon', true);
if (upcomingSingleChallengeSoon === false) {
return;
}
var challengeData = response.challenge;
var closeTime = moment.unix(challengeData.close_time);
upcomingSingleChallengeSoon.querySelector('p').innerHTML = '<div>End:</div><div>' + moment.unix(challengeData.close_time).format('Do MMM, ddd HH:mm') + '</div>';
}
function processActiveChallenges(response)
{
if (response.success !== true || !Array.isArray(response.challenges)) {
return;
}
var domChallenges = awaitDomToLoad('.my-challenges__items .my-challenges__item');
if (domChallenges === false) {
return;
}
for (var challenge of response.challenges) {
const domChallenge = [...domChallenges].filter(element => element.classList.contains(`c-id-${challenge.id}`))[0];
const boostTimeout = challenge.member?.boost?.timeout;
delete challenge.time_left.days;
const duration = moment.duration(challenge.time_left);
const timer = $(domChallenge.querySelector('gs-timer, .c-challenges-item__title__days'));
addDate(timer, duration, 'c-challenges-item__title__days_left');
processTimeoutWarning(timer, duration);
if (boostTimeout > Math.ceil(new Date().getTime() / 1000)) {
domChallenge.querySelector('.action-button__status__icon-message__text').innerHTML = moment.unix(challenge.member.boost.timeout).format('HH:mm')
}
}
}
function processTimeoutWarning(timer, duration) {
var colorBorder = function(id) {
var el = $('.c-id-' + id + ' > challenges-item');
el.css('box-shadow', '0 0 0 7px rgba(225,151,151,0.75)');
};
var challengeHolder = timer.closest('.my-challenges__item');
if (!challengeHolder.length) {
return;
}
var challengeId = getChallengeIdFromClass(challengeHolder);
if (typeof challengeId === 'undefined' || typeof timeoutWarnings[challengeId] !== 'undefined') {
return;
}
var minutes = 10;
if (moment.duration(duration).asMinutes() <= minutes) {
colorBorder(challengeId);
} else {
timeoutWarnings[challengeId] = setTimeout(function() {
colorBorder(challengeId);
}, duration - (minutes * 60 * 1000));
}
}
function awaitDomToLoad(selector, single, retry_count)
{
single = single || false;
retry_count = retry_count || 0;
var domResult = single ? document.querySelector(selector) : document.querySelectorAll(selector);
if (
(!single && !domResult.length)
|| (single && typeof(domResult) !== 'object')
) {
if (retry_count < 5) {
setTimeout(awaitDomToLoad(selector, single, ++retry_count), 1500);
}
return false;
}
return domResult;
}
function getChallengeIdFromClass(challengeHolder) {
var challengeId;
for (var index = 0; index < challengeHolder.get(0).classList.length; ++index) {
var value = challengeHolder.get(0).classList[index];
if (value.indexOf('c-id-') === 0) {
challengeId = value.match(/\d+$/g)[0];
break;
}
}
return challengeId;
}
function processAJAXPointer(pointer)
{
switch (pointer.responseURL) {
case 'https://api.gurushots.com/rest/get_member_challenges':
processMemberChallenges(JSON.parse(pointer.responseText));
break;
case 'https://api.gurushots.com/rest/get_challenge':
processChallenge(JSON.parse(pointer.responseText));
break;
case 'https://api.gurushots.com/rest/get_my_active_challenges':
processActiveChallenges(JSON.parse(pointer.responseText));
break;
}
}
var proxiedAJAXSend = window.XMLHttpRequest.prototype.send;
window.XMLHttpRequest.prototype.send = function() {
//Here is where you can add any code to process the request.
//If you want to pass the Ajax request object, pass the 'pointer' below
var pointer = this
var intervalId = window.setInterval(function(){
if(pointer.readyState != 4){
return;
}
processAJAXPointer(pointer);
//Here is where you can add any code to process the response.
//If you want to pass the Ajax request object, pass the 'pointer' below
clearInterval(intervalId);
}, 0);//I found a delay of 1 to be sufficient, modify it as you need.
return proxiedAJAXSend.apply(this, [].slice.call(arguments));
};
if (mutationObserver) {
var body = document.querySelector('body');
if (!body) {
return;
}
(new mutationObserver(process)).observe(body, {
'childList': true,
'subtree': true
});
}
})(jQuery);
@Karmalakas
Copy link
Author

Karmalakas commented Aug 27, 2020

Despite lots of issues GS had recently (and still has some), most of the time I enjoy participating in challenges, but one thing really frustrates me - calculating exact time when a challenge is going to end. Yes, I'm that lazy, so I wrote a script for myself. Some of our team members also use it for a while now, and they seem to be happy with it.

I checked GS rules and I don't see any violation if I give back to this community by sharing it.

This will definitely help plan your challenges much easier.
Maybe GS will add this feature some day natively both on web and app :)

Basically what this script does, is just find all timers on a page, calculate end time for each timer based on your operating system time and add an element with that end time.


Update v1.12.0
Added GS league timer (see screenshot below)


Update v1.11.1
Fixed styles on team challenges page


Update v1.11
Added separate logic for free boost timer and updated styles (see screenshot below)
NB: As GS provides only hours left, can't really know exact time, so it's only an approximate time. If/When I figure out how to get more exact time, will do my best to update the script


Update v1.10
Added ending time in a single upcoming challenge page (see screenshot below)
NB: this is only visible if you navigate normally from upcoming challenges list or similar. Didn't find yet an easy way to show it when going directly to the page (eg. entering URL in browser's address bar).


Update v1.9
Adds start and end times in the Upcoming challenges page (see screenshot in latest comment)


Update v1.8
Adds a red border around the challenge in main page if less than 10 minutes are left till the end of the challenge (see screenshot in latest comment)


This only works with TamperMonkey (or similar) extension. Supported browsers are Chrome, Firefox, Edge (maybe others too).

See images bellow.

TamperMonkey for:

Once you have TamperMonkey installed, just go to the @downloadURL you see in a script.
If later TM fails to update script, just go to @updateURL you see in a script. This should trigger the update in a new window with TM open.

Any questions or suggestions are welcome.

NB: There was one case reported, when AdBlocker on Opera browser prevented script from displaying time in Upcoming challenges page


If you like this script, please consider keeping me scripting Keep me scripting 😄

@Karmalakas
Copy link
Author

Main current challenges view:
001

Open challenges view:
002

Challenge details view:
003

Team chat view:
004

Team match view:
005

Team match selection:
006

League view (season switch time):
007

Banner:
008

Suggestions:
009

@Karmalakas
Copy link
Author

Karmalakas commented Dec 27, 2020

Red border when less than 10 minutes left
Screenshot 2020-12-27 212125

Start and End times in Upcoming challenges page
Screenshot 2021-01-15 192452

End time in a single upcoming challenge page
image

Free boost timer
image

If you wonder what's this green border, you should check out this script too (full description in the comment).

@Karmalakas
Copy link
Author

Karmalakas commented Feb 8, 2021

GS League timer
image

@ghareeb-falazi
Copy link

Are you parsing the page's HTML or intercepting HTTP responses to get the data?

@Karmalakas
Copy link
Author

Are you parsing the page's HTML or intercepting HTTP responses to get the data?

CSS selectors for HTML objects.
Only upcoming challenges timers are watching for AJAX call and parsing JSON response, but it's not working exactly as I'd like it to

@ghareeb-falazi
Copy link

Are you parsing the page's HTML or intercepting HTTP responses to get the data?

CSS selectors for HTML objects.
Only upcoming challenges timers are watching for AJAX call and parsing JSON response, but it's not working exactly as I'd like it to

For this boost stuff, the response from the get_member_joined_active_challenges rest call seems to contain the exact time you were seeking.
Anyway, the Requestly add on works very well for intercepting HTTP traffic

@ghareeb-falazi
Copy link

Anyway I suggest removing these messages when you read them... :D

@Karmalakas
Copy link
Author

I'm thinking about developing a proper Chrome extension, but I have my hands full so it's for future :)
And messages are fine :) I never delete even criticism or any negative messages for historical reasons :D

@ghareeb-falazi
Copy link

I'm thinking about developing a proper Chrome extension, but I have my hands full so it's for future :)
And messages are fine :) I never delete even criticism or any negative messages for historical reasons :D

You mean an extension specifically for GS?

@Karmalakas
Copy link
Author

Yes :) Was thinking about Chrome extension for GS, but never done any extensions so need to do some research also to figure out if it's worth it at all

@Samoiedo77
Copy link

Hi, after GS renewed his graphics layout, the page "upcoming" isn't working regularly...
The start-end date details are not displayed correctly in each challenge. Do you think it can be fixed?
Thanks for the script is really super useful!

@Karmalakas
Copy link
Author

Should work now with v1.13.0 😉

@Samoiedo77
Copy link

Time on free boost is still not showing, but all the rest is working perfectly! Thanks, amazing job!
image

@Karmalakas
Copy link
Author

Oh, I don't get these often 😅 Will check when I notice

@Karmalakas
Copy link
Author

@Samoiedo77, changed logic a bit. Now with v1.14.0 it shows when the boost is ending exactly. Also now it shows end time for the active challenges that have only days displayed

@Samoiedo77
Copy link

Wow that's amazing! Very much useful! Great upgrade! Thanks a lot for your time! And good luck with GS ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment