|
@@ -5,6 +5,8 @@
|
|
|
*/
|
|
|
|
|
|
import clippy from "clippyjs";
|
|
|
+import "./static/clippy_assets/agents/Clippy/agent.js";
|
|
|
+import clippyMap from "./static/clippy_assets/agents/Clippy/map.png";
|
|
|
|
|
|
/**
|
|
|
* Waiter to handle seasonal events and easter eggs.
|
|
@@ -35,8 +37,9 @@ class SeasonalWaiter {
|
|
|
|
|
|
// Clippy
|
|
|
const now = new Date();
|
|
|
- //if (now.getMonth() === 3 && now.getDate() === 1) {
|
|
|
- if (now.getMonth() === 2 && now.getDate() === 22) {
|
|
|
+ if (now.getMonth() === 3 && now.getDate() === 1) {
|
|
|
+ this.addClippyOption();
|
|
|
+ this.manager.addDynamicListener(".option-item #clippy", "change", this.setupClippy, this);
|
|
|
this.setupClippy();
|
|
|
}
|
|
|
}
|
|
@@ -63,26 +66,56 @@ class SeasonalWaiter {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Sets up Clippy on April Fools Day
|
|
|
+ * Creates an option in the Options menu for turning Clippy on or off
|
|
|
+ */
|
|
|
+ addClippyOption() {
|
|
|
+ const optionsBody = document.getElementById("options-body"),
|
|
|
+ optionItem = document.createElement("span");
|
|
|
+
|
|
|
+ optionItem.className = "bmd-form-group is-filled";
|
|
|
+ optionItem.innerHTML = `<div class="checkbox option-item">
|
|
|
+ <label for="clippy">
|
|
|
+ <input type="checkbox" option="clippy" id="clippy" checked="">
|
|
|
+ Use the Clippy helper
|
|
|
+ </label>
|
|
|
+ </div>`;
|
|
|
+ optionsBody.appendChild(optionItem);
|
|
|
+
|
|
|
+ this.manager.options.load();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Sets up Clippy for April Fools Day
|
|
|
*/
|
|
|
setupClippy() {
|
|
|
- //const clippyAssets = "./agents/";
|
|
|
- const clippyAssets = undefined;
|
|
|
+ // Destroy any previous agents
|
|
|
+ if (this.clippyAgent) {
|
|
|
+ this.clippyAgent.closeBalloonImmediately();
|
|
|
+ this.clippyAgent.hide();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!this.app.options.clippy) {
|
|
|
+ this.clippyTimeouts.forEach(t => clearTimeout(t));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Set base path to # to prevent external network requests
|
|
|
+ const clippyAssets = "#";
|
|
|
+ // Shim the library to prevent external network requests
|
|
|
+ shimClippy(clippy);
|
|
|
|
|
|
const self = this;
|
|
|
clippy.load("Clippy", (agent) => {
|
|
|
- window.agent = agent;
|
|
|
- shimClippy(agent);
|
|
|
+ shimClippyAgent(agent);
|
|
|
self.clippyAgent = agent;
|
|
|
-
|
|
|
agent.show();
|
|
|
- //agent.animate();
|
|
|
- agent.speak("Hello, I'm Clippy, your personal cyber assistant!", true);
|
|
|
+ agent.speak("Hello, I'm Clippy, your personal cyber assistant!");
|
|
|
}, undefined, clippyAssets);
|
|
|
|
|
|
// Watch for the Auto Magic button appearing
|
|
|
const magic = document.getElementById("magic");
|
|
|
const observer = new MutationObserver((mutationsList, observer) => {
|
|
|
+ // Read in message and recipe
|
|
|
let msg, recipe;
|
|
|
for (const mutation of mutationsList) {
|
|
|
if (mutation.attributeName === "data-original-title") {
|
|
@@ -93,42 +126,145 @@ class SeasonalWaiter {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // Cancel current animation and hide balloon (after it has finished)
|
|
|
- self.clippyAgent._queue.clear();
|
|
|
- self.clippyAgent._queue.next();
|
|
|
- self.clippyAgent.stopCurrent();
|
|
|
- self.clippyAgent._balloon._hold = false;
|
|
|
- self.clippyAgent._balloon.hide();
|
|
|
+ // Close balloon if it is currently showing a magic hint
|
|
|
+ const balloon = self.clippyAgent._balloon._balloon;
|
|
|
+ if (balloon.is(":visible") && balloon.text().indexOf("That looks like encoded data") >= 0) {
|
|
|
+ self.clippyAgent._balloon.hide(true);
|
|
|
+ this.clippyAgent._balloon._hidden = true;
|
|
|
+ }
|
|
|
|
|
|
+ // If a recipe was found, get Clippy to tell the user
|
|
|
if (recipe) {
|
|
|
recipe = this.manager.controls.generateStateUrl(true, true, JSON.parse(recipe));
|
|
|
- msg = `That looks like encoded data!<br><br>${msg}<br><br>Click <a href="${recipe}">here</a> to load this recipe.`;
|
|
|
+ msg = `That looks like encoded data!<br><br>${msg}<br><br>Click <a class="clippyMagicRecipe" href="${recipe}">here</a> to load this recipe.`;
|
|
|
|
|
|
- // Stop balloon activity immediately and trigger speak again
|
|
|
- self.clippyAgent._balloon.pause();
|
|
|
- delete self.clippyAgent._balloon._addWord;
|
|
|
- self.clippyAgent._balloon._active = false;
|
|
|
- self.clippyAgent._balloon.hide(true);
|
|
|
+ // Stop current balloon activity immediately and trigger speak again
|
|
|
+ this.clippyAgent.closeBalloonImmediately();
|
|
|
self.clippyAgent.speak(msg, true);
|
|
|
+ // self.clippyAgent._queue.next();
|
|
|
}
|
|
|
});
|
|
|
- observer.observe(document.getElementById("magic"), {
|
|
|
- attributes: true
|
|
|
- });
|
|
|
+ observer.observe(document.getElementById("magic"), {attributes: true});
|
|
|
+
|
|
|
+ // Play animations for various things
|
|
|
+ this.manager.addListeners("#search", "click", () => {
|
|
|
+ this.clippyAgent.play("Searching");
|
|
|
+ }, this);
|
|
|
+ this.manager.addListeners("#save,#save-to-file", "click", () => {
|
|
|
+ this.clippyAgent.play("Save");
|
|
|
+ }, this);
|
|
|
+ this.manager.addListeners("#clr-recipe,#clr-io", "click", () => {
|
|
|
+ this.clippyAgent.play("EmptyTrash");
|
|
|
+ }, this);
|
|
|
+ this.manager.addListeners("#bake", "click", e => {
|
|
|
+ if (e.target.closest("button").textContent.toLowerCase().indexOf("bake") >= 0) {
|
|
|
+ this.clippyAgent.play("Thinking");
|
|
|
+ } else {
|
|
|
+ this.clippyAgent.play("EmptyTrash");
|
|
|
+ }
|
|
|
+ this.clippyAgent._queue.clear();
|
|
|
+ }, this);
|
|
|
+ this.manager.addListeners("#input-text", "keydown", () => {
|
|
|
+ this.clippyAgent.play("Writing");
|
|
|
+ this.clippyAgent._queue.clear();
|
|
|
+ }, this);
|
|
|
+ this.manager.addDynamicListener("a.clippyMagicRecipe", "click", (e) => {
|
|
|
+ this.clippyAgent.play("Congratulate");
|
|
|
+ }, this);
|
|
|
+
|
|
|
+ this.clippyTimeouts = [];
|
|
|
+ // Show challenge after timeout
|
|
|
+ this.clippyTimeouts.push(setTimeout(() => {
|
|
|
+ const hex = "1f 8b 08 00 ae a1 9b 5c 00 ff 05 40 a1 12 00 10 0c fd 26 61 5b 76 aa 9d 26 a8 02 02 37 84 f7 fb bb c5 a4 5f 22 c6 09 e5 6e c5 4c 2d 3f e9 30 a6 ea 41 a2 f2 ac 1c 00 00 00";
|
|
|
+ self.clippyAgent.speak(`How about a fun challenge?<br><br>Try decoding this (click to load):<br><a href="#recipe=[]&input=${encodeURIComponent(btoa(hex))}">${hex}</a>`, true);
|
|
|
+ self.clippyAgent.play("GetAttention");
|
|
|
+ }, 1 * 60 * 1000));
|
|
|
+
|
|
|
+ this.clippyTimeouts.push(setTimeout(() => {
|
|
|
+ self.clippyAgent.speak("<i>Did you know?</i><br><br>You can load files into CyberChef up to around 500MB using drag and drop or the load file button.", 15000);
|
|
|
+ self.clippyAgent.play("Wave");
|
|
|
+ }, 2 * 60 * 1000));
|
|
|
+
|
|
|
+ this.clippyTimeouts.push(setTimeout(() => {
|
|
|
+ self.clippyAgent.speak("<i>Did you know?</i><br><br>You can use the 'Fork' operation to split up your input and run the recipe over each branch separately.<br><br><a class='clippyMagicRecipe' href=\"#recipe=Fork('%5C%5Cn','%5C%5Cn',false)From_UNIX_Timestamp('Seconds%20(s)')&input=OTc4MzQ2ODAwCjEwMTI2NTEyMDAKMTA0NjY5NjQwMAoxMDgxMDg3MjAwCjExMTUzMDUyMDAKMTE0OTYwOTYwMA\">Here's an example</a>.", 15000);
|
|
|
+ self.clippyAgent.play("Print");
|
|
|
+ }, 3 * 60 * 1000));
|
|
|
+
|
|
|
+ this.clippyTimeouts.push(setTimeout(() => {
|
|
|
+ self.clippyAgent.speak("<i>Did you know?</i><br><br>The 'Magic' operation uses a number of methods to detect encoded data and the operations which can be used to make sense of it. A technical description of these methods can be found <a href=\"https://github.com/gchq/CyberChef/wiki/Automatic-detection-of-encoded-data-using-CyberChef-Magic\">here</a>.", 15000);
|
|
|
+ self.clippyAgent.play("Alert");
|
|
|
+ }, 4 * 60 * 1000));
|
|
|
+
|
|
|
+ this.clippyTimeouts.push(setTimeout(() => {
|
|
|
+ self.clippyAgent.speak("<i>Did you know?</i><br><br>You can use parts of the input as arguments to operations.<br><br><a class='clippyMagicRecipe' href=\"#recipe=Register('key%3D(%5B%5C%5Cda-f%5D*)',true,false)Find_/_Replace(%7B'option':'Regex','string':'.*data%3D(.*)'%7D,'$1',true,false,true)RC4(%7B'option':'Hex','string':'$R0'%7D,'Hex','Latin1')&input=aHR0cDovL21hbHdhcmV6LmJpei9iZWFjb24ucGhwP2tleT0wZTkzMmE1YyZkYXRhPThkYjdkNWViZTM4NjYzYTU0ZWNiYjMzNGUzZGIxMQ\">Click here for an example</a>.", 15000);
|
|
|
+ self.clippyAgent.play("CheckingSomething");
|
|
|
+ }, 5 * 60 * 1000));
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
+
|
|
|
/**
|
|
|
* Shims various ClippyJS functions to modify behaviour.
|
|
|
*
|
|
|
+ * @param {Clippy} clippy - The Clippy library
|
|
|
+ */
|
|
|
+function shimClippy(clippy) {
|
|
|
+ // Shim _loadSounds so that it doesn't actually try to load any sounds
|
|
|
+ clippy.load._loadSounds = function _loadSounds (name, path) {
|
|
|
+ let dfd = clippy.load._sounds[name];
|
|
|
+ if (dfd) return dfd;
|
|
|
+
|
|
|
+ // set dfd if not defined
|
|
|
+ dfd = clippy.load._sounds[name] = $.Deferred();
|
|
|
+
|
|
|
+ // Resolve immediately without loading
|
|
|
+ dfd.resolve({});
|
|
|
+
|
|
|
+ return dfd.promise();
|
|
|
+ };
|
|
|
+
|
|
|
+ // Shim _loadMap so that it uses the local copy
|
|
|
+ clippy.load._loadMap = function _loadMap (path) {
|
|
|
+ let dfd = clippy.load._maps[path];
|
|
|
+ if (dfd) return dfd;
|
|
|
+
|
|
|
+ // set dfd if not defined
|
|
|
+ dfd = clippy.load._maps[path] = $.Deferred();
|
|
|
+
|
|
|
+ const src = clippyMap;
|
|
|
+ const img = new Image();
|
|
|
+
|
|
|
+ img.onload = dfd.resolve;
|
|
|
+ img.onerror = dfd.reject;
|
|
|
+
|
|
|
+ // start loading the map;
|
|
|
+ img.setAttribute("src", src);
|
|
|
+
|
|
|
+ return dfd.promise();
|
|
|
+ };
|
|
|
+
|
|
|
+ // Make sure we don't request the remote map
|
|
|
+ clippy.Animator.prototype._setupElement = function _setupElement (el) {
|
|
|
+ const frameSize = this._data.framesize;
|
|
|
+ el.css("display", "none");
|
|
|
+ el.css({ width: frameSize[0], height: frameSize[1] });
|
|
|
+ el.css("background", "url('" + clippyMap + "') no-repeat");
|
|
|
+
|
|
|
+ return el;
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Shims various ClippyJS Agent functions to modify behaviour.
|
|
|
+ *
|
|
|
* @param {Agent} agent - The Clippy Agent
|
|
|
*/
|
|
|
-function shimClippy(agent) {
|
|
|
+function shimClippyAgent(agent) {
|
|
|
// Turn off all sounds
|
|
|
agent._animator._playSound = () => {};
|
|
|
|
|
|
- // Improve speak function
|
|
|
+ // Improve speak function to support HTML markup
|
|
|
const self = agent._balloon;
|
|
|
agent._balloon.speak = (complete, text, hold) => {
|
|
|
self._hidden = false;
|
|
@@ -147,10 +283,11 @@ function shimClippy(agent) {
|
|
|
|
|
|
self._complete = complete;
|
|
|
self._sayWords(text, hold, complete);
|
|
|
+ if (hold) agent._queue.next();
|
|
|
};
|
|
|
|
|
|
- // Improve the _sayWords function to allow HTML
|
|
|
- agent._balloon.WORD_SPEAK_TIME = 100;
|
|
|
+ // Improve the _sayWords function to allow HTML and support timeouts
|
|
|
+ agent._balloon.WORD_SPEAK_TIME = 60;
|
|
|
agent._balloon._sayWords = (text, hold, complete) => {
|
|
|
self._active = true;
|
|
|
self._hold = hold;
|
|
@@ -158,6 +295,7 @@ function shimClippy(agent) {
|
|
|
const time = self.WORD_SPEAK_TIME;
|
|
|
const el = self._content;
|
|
|
let idx = 1;
|
|
|
+ clearTimeout(self.holdTimeout);
|
|
|
|
|
|
self._addWord = $.proxy(function () {
|
|
|
if (!self._active) return;
|
|
@@ -167,6 +305,12 @@ function shimClippy(agent) {
|
|
|
if (!self._hold) {
|
|
|
complete();
|
|
|
self.hide();
|
|
|
+ } else if (typeof hold === "number") {
|
|
|
+ self.holdTimeout = setTimeout(() => {
|
|
|
+ self._hold = false;
|
|
|
+ complete();
|
|
|
+ self.hide();
|
|
|
+ }, hold);
|
|
|
}
|
|
|
} else {
|
|
|
el.html(words.slice(0, idx).join(" "));
|
|
@@ -178,10 +322,24 @@ function shimClippy(agent) {
|
|
|
self._addWord();
|
|
|
};
|
|
|
|
|
|
- // Close the balloon on click
|
|
|
+ // Add break-word to balloon CSS
|
|
|
+ agent._balloon._balloon.css("word-break", "break-word");
|
|
|
+
|
|
|
+ // Close the balloon on click (unless it was a link)
|
|
|
agent._balloon._balloon.click(e => {
|
|
|
- agent._balloon._finishHideBalloon();
|
|
|
+ if (e.target.nodeName !== "A") {
|
|
|
+ agent._balloon.hide(true);
|
|
|
+ agent._balloon._hidden = true;
|
|
|
+ }
|
|
|
});
|
|
|
+
|
|
|
+ // Add function to immediately close the balloon even if it is currently doing something
|
|
|
+ agent.closeBalloonImmediately = () => {
|
|
|
+ agent._queue.clear();
|
|
|
+ agent._balloon.hide(true);
|
|
|
+ agent._balloon._hidden = true;
|
|
|
+ agent._queue.next();
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
export default SeasonalWaiter;
|