NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name GGn Dwarf Drop Notifier
// @namespace https://openuserjs.org/users/SB100
// @description Get a notification if a dwarf drops an item. Needs an API key with 'User' permissions that you can generate here: https://gazellegames.net/user.php?action=edit#save
// @updateURL https://openuserjs.org/meta/SB100/GGn_Dwarf_Drop_Notifier.meta.js
// @version 1.3.4
// @author SB100
// @copyright 2022, SB100 (https://openuserjs.org/users/SB100)
// @license MIT
// @match https://gazellegames.net/*
// @connect gazellegames.net
// @grant GM.xmlHttpRequest
// @grant GM.getValue
// @grant GM.setValue
// ==/UserScript==
// ==OpenUserJS==
// @author SB100
// ==/OpenUserJS==
/**
* =============================
* ADVANCED OPTIONS
* =============================
*/
// check every 5 mins max
const SETTING_THROTTLE = 1000 * 60 * 5;
// true = never auto hide the popup (click will remove it)
// false = popup self disappears after 5 seconds
const SETTING_PERMANENT_POPUP = true;
// whether to show console output
const DEBUG_ENABLED = false;
/**
* =============================
* END ADVANCED OPTIONS
* DO NOT MODIFY BELOW THIS LINE
* =============================
*/
/**
* Output a console.log line if debug is enabled
*/
function debug(...str) {
if (!DEBUG_ENABLED) {
return;
}
// eslint-disable-next-line no-console
console.log('[GGn Dwarf Drop Notifier]', ...str);
}
/**
* Try parsing a string into JSON, otherwise fallback
*/
function JsonParseWithDefault(s, fallback = null) {
try {
return JSON.parse(s);
}
catch (e) {
return fallback;
}
}
/**
* Gets your GGn API key. Asks the user for one if it hasn't been set yet
*/
async function getGgnApiKey() {
const key = await GM.getValue('ggn_key', '');
if (!key) {
// eslint-disable-next-line no-alert
const input = window.prompt(`Please input your GGn API key.
If you don't have one, please generate one from your Edit Profile page: https://gazellegames.net/user.php?action=edit.
Please disable this userscript until you have one as this prompt will continue to show until you enter one in.`);
const trimmed = input.trim();
if (/[a-f0-9]{64}/.test(trimmed)) {
await GM.setValue('ggn_key', trimmed);
return trimmed;
}
}
return key;
}
/**
* Creates a UTC date object from a date string
* d should be in the form yyyy-mm-dd hh:ii:ss, and is assumed a UTC date
*/
function createUTCDate(d) {
const parts = d.split(/[-\s:]/);
parts[1] = (parseInt(parts[1], 10) - 1).toString();
return new Date(Date.UTC(...parts));
}
/**
* Creates a UTC date string from a date object
* Returns the format yyyy-mm-dd hh:ii:ss
*/
function createUTCString(d = Date.now()) {
return new Date(d).toISOString().replace(/T/, ' ').replace(/\..+/, '');
}
/**
* Return the last drop info from localStorage, as date objects
*/
function getLastDropInfo() {
// default date is 1 day ago. So when you install the script for the first time, it sets up checks properly.
const utcString = createUTCString(new Date(Date.now() - 24 * 3600 * 1000));
const defaultDropInfo = {
check: utcString,
drop: utcString,
};
const lastDropInfoAsStrings = JsonParseWithDefault(
window.localStorage.getItem('lastDropInfo') || defaultDropInfo,
defaultDropInfo
);
return {
check: createUTCDate(lastDropInfoAsStrings.check),
drop: createUTCDate(lastDropInfoAsStrings.drop),
};
}
/**
* Sets the lastDropInfo value in localStorage
*/
function setLastDropInfo(check, drop, lastDropInfo) {
window.localStorage.setItem(
'lastDropInfo',
JSON.stringify({
check: check !== null ? check : createUTCString(lastDropInfo.check),
drop: drop !== null ? drop : createUTCString(lastDropInfo.drop),
})
);
}
/**
* Returns a boolean by comparing the lastDropInfo.check value to the current time + SETTING_THROTTLE
*/
function isTimeToCheck(lastDropInfo) {
const currentTime = createUTCDate(createUTCString());
if (currentTime < new Date(lastDropInfo.check.getTime() + SETTING_THROTTLE)) {
debug('[GGn Dwarf Drop Notifier] Not checking for companion drop');
return false;
}
debug('Checking for companion drop');
return true;
}
/**
* If we have notifications to show, create a popup, and remove it after 5s
*/
function createNotification(drops) {
if (drops.length === 0) {
debug('No new drops found');
return;
}
const innerHTML = `<a id="ggn-dwarf-drop-notif" href='#close' style='display: block; border-radius: 5px; border: 1px solid rgb(80, 194, 78); box-shadow: rgba(0, 0, 0, 0.1) 0px 2px 4px; color: darkgreen; width: 290px; cursor: pointer; font-size: 13px; line-height: 16px; text-align: left; padding: 8px 10px 9px; overflow: hidden; background-image: linear-gradient(135deg, #8ae68a 25%, #8fee8f 25%, #8fee8f 50%, #8ae68a 50%, #8ae68a 75%, #8fee8f 75%, #8fee8f 100%); background-size: 28.28px 28.28px;'>
<ul style='list-style-type: none; margin: 0; padding: 0;'>
${drops.map((drop) => `<li>${drop.message}</li>`).join('')}
</ul>
</li>`;
const notif = document.createElement('div');
notif.classList.add('i-am-new');
notif.innerHTML = innerHTML;
document.body.appendChild(notif);
if (SETTING_PERMANENT_POPUP) {
notif.onclick = () => {
notif.remove();
};
}
else {
setTimeout(() => {
notif.remove();
}, 5000);
}
}
/**
* Parses the API response, and checks if a new drop has happened
*/
function xmlOnLoad(response, lastDropInfo) {
// something wrong with the response
if (response.status !== 'success') {
debug('API returned unsuccessful response');
return [];
}
// no results found
if (!response.response || response.response.length === 0) {
debug('No drops found in response');
return [];
}
const results = [];
for (let i = 0, len = response.response.length; i < len; i += 1) {
const item = response.response[i];
if (createUTCDate(item.time) > lastDropInfo.check) {
debug('Pushed', createUTCDate(item.time));
results.push(item);
}
}
setLastDropInfo(
createUTCString(),
results.length > 0 ? results[0].time : createUTCString(lastDropInfo.drop),
lastDropInfo
);
return results;
}
/**
* Sends a request to the API, checking for companion drops
*/
function sendApiRequest(key, action, params, lastDropInfo) {
const paramStr = new URLSearchParams(params).toString();
let resolver;
let rejecter;
const p = new Promise((resolveFn, rejectFn) => {
resolver = resolveFn;
rejecter = rejectFn;
});
const url = `/api.php?request=${action}${
paramStr.length > 0 ? `&${paramStr}` : ''
}`;
GM.xmlHttpRequest({
method: 'get',
url,
timeout: 30000,
headers: {
'X-API-Key': key,
},
onload(done) {
if (done.status !== 200) {
setLastDropInfo(createUTCString(), null, lastDropInfo);
rejecter(new Error('Not OK'));
return;
}
resolver(JsonParseWithDefault(done.response));
},
onerror() {
rejecter(new Error('Error'));
},
ontimeout() {
rejecter(new Error('Timeout'));
},
});
return p;
}
/**
* Creates the style tag to show a popup if needed
*/
function createStyleTag() {
const css = `.i-am-new {
bottom: 20px;
right: 20px;
position: fixed;
width: 310px;
margin: 0px;
padding: 0px;
list-style-type: none;
z-index: 10000000;
}
.i-am-new + .i-am-new {
bottom: 70px!important;
}`;
const style = document.createElement('style');
style.appendChild(document.createTextNode(css));
document.head.appendChild(style);
}
(async function main() {
const key = await getGgnApiKey();
if (!key) {
debug(
'[GGn Dwarf Drop Notifier] No valid API key found, exiting userscript'
);
return;
}
const lastDropInfo = getLastDropInfo();
debug('Last Drop Info', lastDropInfo);
if (!isTimeToCheck(lastDropInfo)) {
return;
}
createStyleTag();
sendApiRequest(key, 'userlog', {
search: 'dropped',
limit: 25
}, lastDropInfo)
.then((response) => xmlOnLoad(response, lastDropInfo))
.then((drops) => createNotification(drops))
// eslint-disable-next-line no-console
.catch((e) => console.error(`[GGn Dwarf Drop Notifier] ${e.message}`));
})();