diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index b7e0fb86..00000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - // See https://go.microsoft.com/fwlink/?LinkId=733558 - // for the documentation about the tasks.json format - "version": "2.0.0", - "tasks": [ - { - "label": "Build Raw", - "type": "shell", - "command": "python ./build.py --nominify --copytracker", - "group": { - "kind": "build", - "isDefault": true - }, - "presentation": { - "reveal": "always", - "panel": "shared", - "echo": true, - "focus": false, - "showReuseMessage": false - }, - "problemMatcher": [] - }, - { - "label": "Build Minified", - "type": "shell", - "command": "python ./build.py --copytracker", - "group": "build", - "presentation": { - "reveal": "always", - "panel": "shared", - "echo": true, - "focus": false, - "showReuseMessage": false - }, - "problemMatcher": [] - }, - { - "label": "Build Website", - "type": "shell", - "command": "python ./build.py --website", - "group": "build", - "presentation": { - "reveal": "always", - "panel": "shared", - "echo": true, - "focus": false, - "showReuseMessage": false - }, - "problemMatcher": [] - } - ] -} diff --git a/README.md b/README.md index e0baf5be..d98cbd33 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,32 @@ # Welcome -All you need to **use Discord History Tracker** is either an up-to-date browser, or the [Discord desktop client](https://discord.com/download). Visit the [official website](https://dht.chylex.com) for instructions. +For instructions on how to **use Discord History Tracker**, visit the [official website](https://dht.chylex.com). To **report an issue or suggestion**, first please see the [issues](https://github.com/chylex/Discord-History-Tracker/issues) page and make sure someone else hasn't already created a similar issue report. If you do find an existing issue, comment on it or add a reaction. Otherwise, either click [New Issue](https://github.com/chylex/Discord-History-Tracker/issues/new), or contact me via email [contact@chylex.com](mailto:contact@chylex.com) or Twitter [@chylexmc](https://twitter.com/chylexmc). -If you are interested in **creating your own version** from the source code, continue reading the [build instructions](#Build-Instructions) below. +If you are interested in **building from source code**, continue reading the [build instructions](#Build-Instructions) below. -# Build Instructions +This branch is dedicated to the Discord History Tracker desktop app. If you are looking for the older browser-only version, visit the [master-browser-only](https://github.com/chylex/Discord-History-Tracker/tree/master-browser-only) branch. -Follow the steps below to create your own version of Discord History Tracker. +# Build Instructions ### Setup Fork the repository and clone it to your computer (if you've never used git, you can download the [GitHub Desktop](https://desktop.github.com) client to get started quickly). -Now you can modify the source code: -* `src/tracker/` contains JS files that are automatically combined into the **tracker bookmark/script** -* `src/viewer/` contains HTML, CSS, JS files that are then combined into the **offline viewer page** +Folder organization: +* `app/` contains a Visual Studio solution for the desktop app * `lib/` contains utilities required to build the project * `web/` contains source code of the [official website](https://dht.chylex.com), which can be used as a template when making your own website -### Building +To start editing source code for the desktop app, open `app/DiscordHistoryTracker.sln` in [Visual Studio](https://visualstudio.microsoft.com/downloads/) or [Rider](https://www.jetbrains.com/rider/). -After you've done changes to the source code, you will need to build it. Before that, download and install: -* (**required**) [Python 3](https://www.python.org/downloads) - * Use to run the build script -* (optional) [Node + npm](https://nodejs.org/en) & command line [uglify-js](https://www.npmjs.com/package/uglify-js) - * Not required on Windows - * Only required for optional [JS minification](#Minification) on Linux/Mac - -Now open the folder that contains `build.py` in a command line, and run `python build.py` to create a build with default settings. The following files will be created: -* `bld/track.js` is the raw tracker script that can be pasted into a browser console -* `bld/track.html` is the tracker script but sanitized for inclusion in HTML (see `web/index.php` for examples) -* `bld/viewer.html` is the complete offline viewer +### Building -You can tweak the build process using the following flags: -* `python build.py --nominify` to disable [minification](#Minification) +To build a `Debug` version of the desktop app, there are no additional requirements. -### Minification +To build a `Release` version of the desktop app, you will need [Python 3](https://www.python.org/downloads), which is used by the build process to launch `app/Resources/minify.py` script. -The build process automatically minifies JS using `UglifyJS@3`, and CSS using a custom minifier. +When creating `Release` builds on systems other than 64-bit Windows, you will also need [Node + npm](https://nodejs.org/en), and [uglify-js](https://www.npmjs.com/package/uglify-js) installed globally (`npm install uglify-js -g`). On 64-bit Windows, both Node and uglify-js are already included in the `lib/` folder for convenience. -* If the `--nominify` flag is used, minification will be completely disabled -* If `uglify-js` is not available from the command line, JS minification will be skipped - * When building on Windows 64-bit, the build script will use the included Node runner and packages - * When building on Windows 32-bit, you will need to download [Node 32-bit](https://nodejs.org/en/download) and replace the included one in `lib/` - * When building on Linux/Mac, the build script will attempt to find `uglifyjs` in the command line +To create `Release` builds ready for distribution, run the `app/build.bat` script on Windows, or `app/build.sh` script on other operating systems. This will create self-contained executables for each major operating system, and a portable version that works on all other systems but requires .NET 5 to be installed. All builds are placed in the `app/bin` folder. diff --git a/bld/track.js b/bld/track.js deleted file mode 100644 index 5b31a0f2..00000000 --- a/bld/track.js +++ /dev/null @@ -1 +0,0 @@ -javascript:(function(){var s=function(){function c(){return f.i("messagesWrapper")}function d(){return c().querySelector("[class*='scroller-']")}function i(){return c().querySelectorAll("[class*='message-']")}function e(t){var e=Object.keys(t||{}),n=e.find(t=>t.startsWith("__reactInternalInstance"));return n?t[n].memoizedProps:(n=e.find(t=>t.startsWith("__reactProps$")))?t[n]:null}function o(t){if((t=e(t)).children&&4<=t.children.length){t=t.children[3].props;if("message"in t&&"channel"in t)return t}return null}function u(){return null===document.querySelector("#messagesNavigationDescription + [class^=container]")}function h(){try{const e=[];for(const n of i()){var t=o(n);null!=t&&e.push(t.message)}return e}catch(t){return console.error(t),[]}}return{o:function(s){let a=0,i=!1,o=!1;const l=new Set,t=window.setInterval(()=>{if(0{const t=d();t.scrollTop=t.scrollHeight/2},1)}else i=!1;e=h();let t=!1;for(const n of e)if(!l.has(n.id)){t=!0;break}if(t){l.clear();for(const r of e)l.add(r.id);o=!1,s(e)}else o||u()||(o=!0,s(!1))}else a=2},200);window.l.push(()=>window.clearInterval(t))},h:e,g:function(){try{let n;for(const e of i()){var t=o(e);if(null!=t){n=t.channel;break}}if(!n)return null;var r=f.i("privateChannels");if(r){let t;for(const a of r.querySelectorAll("[class*='channel-'] [class*='selected-'] [class^='name-'] *, [class*='channel-'][class*='selected-'] [class^='name-'] *")){var s=Array.prototype.find.call(a.childNodes,t=>t.nodeType===Node.TEXT_NODE);if(s){t=s.nodeValue;break}}if(!t)return null;let e;switch(n.type){case 1:e="DM";break;case 3:e="GROUP";break;default:return null}return{server:t,channel:t,id:n.id,type:e,extra:{}}}return n.guild_id?{server:document.querySelector("nav header > h1").innerText,channel:n.name,id:n.id,type:"SERVER",extra:{position:n.position,topic:n.topic,nsfw:n.nsfw}}:null}catch(t){return console.error(t),null}},p:h,v:()=>!!c(),C:u,k:function(){let t=d();0 nav[class*='container'] > div[class*='scroller']");if(!e)return!1;for(var n=Array.prototype.filter.call(e.querySelectorAll("[class*='containerDefault']"),t=>0t.includes("wrapper-")&&!t.includes("clickable-"))(t.children[0].className)&&(t=>!!t.querySelector('path[d="M5.88657 21C5.57547 21 5.3399 20.7189 5.39427 20.4126L6.00001 17H2.59511C2.28449 17 2.04905 16.7198 2.10259 16.4138L2.27759 15.4138C2.31946 15.1746 2.52722 15 2.77011 15H6.35001L7.41001 9H4.00511C3.69449 9 3.45905 8.71977 3.51259 8.41381L3.68759 7.41381C3.72946 7.17456 3.93722 7 4.18011 7H7.76001L8.39677 3.41262C8.43914 3.17391 8.64664 3 8.88907 3H9.87344C10.1845 3 10.4201 3.28107 10.3657 3.58738L9.76001 7H15.76L16.3968 3.41262C16.4391 3.17391 16.6466 3 16.8891 3H17.8734C18.1845 3 18.4201 3.28107 18.3657 3.58738L17.76 7H21.1649C21.4755 7 21.711 7.28023 21.6574 7.58619L21.4824 8.58619C21.4406 8.82544 21.2328 9 20.9899 9H17.41L16.35 15H19.7549C20.0655 15 20.301 15.2802 20.2474 15.5862L20.0724 16.5862C20.0306 16.8254 19.8228 17 19.5799 17H16L15.3632 20.5874C15.3209 20.8261 15.1134 21 14.8709 21H13.8866C13.5755 21 13.3399 20.7189 13.3943 20.4126L14 17H8.00001L7.36325 20.5874C7.32088 20.8261 7.11337 21 6.87094 21H5.88657ZM9.41045 9L8.35045 15H14.3504L15.4104 9H9.41045Z"]')||!!t.querySelector('path[d="M14 8C14 7.44772 13.5523 7 13 7H9.76001L10.3657 3.58738C10.4201 3.28107 10.1845 3 9.87344 3H8.88907C8.64664 3 8.43914 3.17391 8.39677 3.41262L7.76001 7H4.18011C3.93722 7 3.72946 7.17456 3.68759 7.41381L3.51259 8.41381C3.45905 8.71977 3.69449 9 4.00511 9H7.41001L6.35001 15H2.77011C2.52722 15 2.31946 15.1746 2.27759 15.4138L2.10259 16.4138C2.04905 16.7198 2.28449 17 2.59511 17H6.00001L5.39427 20.4126C5.3399 20.7189 5.57547 21 5.88657 21H6.87094C7.11337 21 7.32088 20.8261 7.36325 20.5874L8.00001 17H14L13.3943 20.4126C13.3399 20.7189 13.5755 21 13.8866 21H14.8709C15.1134 21 15.3209 20.8261 15.3632 20.5874L16 17H19.5799C19.8228 17 20.0306 16.8254 20.0724 16.5862L20.2474 15.5862C20.301 15.2802 20.0655 15 19.7549 15H16.35L16.6758 13.1558C16.7823 12.5529 16.3186 12 15.7063 12C15.2286 12 14.8199 12.3429 14.7368 12.8133L14.3504 15H8.35045L9.41045 9H13C13.5523 9 14 8.55228 14 8Z"]'))(t)),r=null,s=0;s{t=document.createElement(t);return t.id=n||"",t.innerHTML=r||"",e.appendChild(t),t};return{id:(t,e)=>(e||document).getElementById(t),i:(t,e)=>(e||document).querySelector(`[class*="${t}-"]`),createElement:(t,e,n,r)=>s(t,e,n,r),L:t=>t.parentNode.removeChild(t),H:t=>s("style",document.head,"",t),T:(t,e)=>window.setTimeout(t,e),O:(t,e,n)=>t.addEventListener(e,n),M:(t,e,n)=>{n=new Date(Date.now()+1e3*n).toUTCString();document.cookie=t+"="+encodeURIComponent(JSON.stringify(e))+";path=/;expires="+n},A:t=>{t=document.cookie.replace(new RegExp("(?:(?:^|.*;\\s*)"+t+"\\s*\\=\\s*([^;]*).*$)|^.*$"),"$1");return t.length?JSON.parse(decodeURIComponent(t)):null},N:(t,e)=>{var n=new Blob([e],{type:"octet/stream"});if("msSaveBlob"in window.navigator)return window.navigator.msSaveBlob(n,t);e=window.URL.createObjectURL(n),n=s("a",document.body);n.href=e,n.download=t,n.style.display="none",n.click(),document.body.removeChild(n),window.URL.revokeObjectURL(e)}}}(),t=function(){function e(t){r||(h.D(n),u.F(n),r=!0),n("gui",t)}var i,o,l=()=>{h.R()?(i._.$.disabled=!0,i._.U.disabled=!0,i._.I.disabled=!0):(i._.$.disabled=!1,i._.U.disabled=!1,i._.j.disabled=i._.I.disabled=!h.P())},n=(t,e)=>{var n,r,s,a;i&&(s="gui"===t&&"controller"===e,"data"!==t&&!s||l(),"tracking"!==t&&!s||(l(),i._.G.innerHTML=h.R()?"Pause Tracking":"Start Tracking"),"data"!==t&&!s||(r=n=0,h.P()&&(n=h.V().J(),r=h.V().W()),i._.Z.innerHTML=[n," message",1===n?"":"s"," from ",r," channel",1===r?"":"s"].join(""))),o&&((s="gui"===t&&"settings"===e)&&(o._.B.checked=u.autoscroll,o._.q[u.afterFirstMsg].checked=!0,o._.X[u.afterSavedMsg].checked=!0),"setting"!==t&&!s||(a=!u.autoscroll,Object.values(o._.q).forEach(t=>t.disabled=a),Object.values(o._.X).forEach(t=>t.disabled=a)))},r=!1,s={K:function(){(i={}).Y=f.H(` #app-mount > div[class*="app-"] { margin-bottom: 48px !important; } #dht-ctrl { position: absolute; bottom: 0; width: 100%; height: 48px; background-color: #FFF; } #dht-ctrl button { height: 32px; margin: 8px 0 8px 8px; font-size: 16px; padding: 0 12px; background-color: #7289DA; color: #FFF; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.75); } #dht-ctrl button:disabled { background-color: #7A7A7A; cursor: default; } #dht-ctrl-close { margin: 8px 8px 8px 0 !important; float: right; } #dht-ctrl p { display: inline-block; margin: 14px 12px; } #dht-ctrl input { display: none; }`);var t=(t,e)=>"";i.tt=f.createElement("div",document.body,"dht-ctrl",` ${t("upload","Upload & Combine")} ${t("settings","Settings")} ${t("track","")} ${t("download","Download")} ${t("reset","Reset")}

${t("close","X")}`),i._={$:f.id("dht-ctrl-upload"),U:f.id("dht-ctrl-settings"),G:f.id("dht-ctrl-track"),j:f.id("dht-ctrl-download"),I:f.id("dht-ctrl-reset"),et:f.id("dht-ctrl-close"),Z:f.id("dht-ctrl-status"),nt:f.id("dht-ctrl-upload-input")},f.O(i._.$,"click",()=>{i._.nt.click()}),f.O(i._.U,"click",()=>{s.rt()}),f.O(i._.G,"click",()=>{h.st(!h.R())}),f.O(i._.j,"click",()=>{h.at()}),f.O(i._.I,"click",()=>{h.it()}),f.O(i._.et,"click",()=>{s.ot(),window.l.forEach(t=>t()),window.DHT_LOADED=!1}),f.O(i._.nt,"change",()=>{Array.prototype.forEach.call(i._.nt.files,e=>{var n=new FileReader;n.onload=function(){var t={};try{t=JSON.parse(n.result)}catch(t){return alert("Could not parse '"+e.name+"', see console for details."),void console.error(t)}c.lt(t)?h.ct(e.name,new c(t)):alert("File '"+e.name+"' has an invalid format.")},n.readAsText(e,"UTF-8")}),i._.nt.value=null}),e("controller")},ot:function(){i&&(f.L(i.tt),f.L(i.Y),i=null)},rt:function(){(o={}).Y=f.H(` #dht-cfg-overlay { position: absolute; left: 0; top: 0; width: 100%; height: 100%; background-color: #000; opacity: 0.5; display: block; z-index: 1000; } #dht-cfg { position: absolute; left: 50%; top: 50%; width: 800px; height: 262px; margin-left: -400px; margin-top: -131px; padding: 8px; background-color: #fff; z-index: 1001; } #dht-cfg-note { margin-top: 22px; } #dht-cfg sub { color: #666; font-size: 13px; }`),o.dt=f.createElement("div",document.body,"dht-cfg-overlay"),f.O(o.dt,"click",()=>{s.ut()});var t=(t,e,n)=>"
";o.tt=f.createElement("div",document.body,"dht-cfg",`


${t("afm","nothing","Do Nothing")} ${t("afm","pause","Pause Tracking")} ${t("afm","switch","Switch to Next Channel")}

${t("asm","nothing","Do Nothing")} ${t("asm","pause","Pause Tracking")} ${t("asm","switch","Switch to Next Channel")}

It is recommended to disable link and image previews to avoid putting unnecessary strain on your browser.

v.31a, released 12 Feb 2022

`),o._={B:f.id("dht-cfg-autoscroll"),q:{},X:{}},o._.q[d.ht]=f.id("dht-cfg-afm-nothing"),o._.q[d.ft]=f.id("dht-cfg-afm-pause"),o._.q[d.gt]=f.id("dht-cfg-afm-switch"),o._.X[d.ht]=f.id("dht-cfg-asm-nothing"),o._.X[d.ft]=f.id("dht-cfg-asm-pause"),o._.X[d.gt]=f.id("dht-cfg-asm-switch"),o._.B.addEventListener("change",()=>{u.autoscroll=o._.B.checked}),Object.keys(o._.q).forEach(t=>{f.O(o._.q[t],"click",()=>{u.afterFirstMsg=t})}),Object.keys(o._.X).forEach(t=>{f.O(o._.X[t],"click",()=>{u.afterSavedMsg=t})}),e("settings")},ut:function(){o&&(f.L(o.dt),f.L(o.tt),f.L(o.Y),o=null)}};return s}();class c{constructor(t){var e=this;c.lt(t)||(t={meta:{},data:{}}),e.meta=t.meta,e.data=t.data,e.meta.users=e.meta.users||{},e.meta.userindex=e.meta.userindex||[],e.meta.servers=e.meta.servers||[],e.meta.channels=e.meta.channels||{},e.vt={wt:{},bt:new Set,Ct:new Set,kt:new Set}}static lt(t){return t&&"object"==typeof t.meta&&"object"==typeof t.data}St(e,t,n,r){var s=e in this.meta.users,a=s?this.meta.users[e]:{};return a.name=t,n&&(a.tag=n),r&&(a.avatar=r),s?e in this.vt.wt?this.vt.wt[e]:this.vt.wt[e]=this.meta.userindex.findIndex(t=>t==e):(this.meta.users[e]=a,this.meta.userindex.push(e),this.vt.wt[e]=this.meta.userindex.length-1)}yt(e,n){var t=this.meta.servers.findIndex(t=>t.name===e&&t.type===n);return-1===t?(this.meta.servers.push({name:e,type:n}),this.meta.servers.length-1):t}xt(t,e,n,r){if(this.meta.servers[t]){var s=e in this.meta.channels,t=s?this.meta.channels[e]:{server:t};return t.name=n,r.position&&(t.position=r.position),r.topic&&(t.topic=r.topic),r.nsfw&&(t.nsfw=r.nsfw),!s&&(this.meta.channels[e]=t,this.vt.bt.add(e),!0)}}Lt(t,e,n){var r=this.data[t]||(this.data[t]={}),t=e in r;return r[e]=n,this.vt.Ct.add(e),!t}Ht(t){var e=t.author,e={u:this.St(e.id,e.username,e.bot?null:e.discriminator,e.avatar),t:t.timestamp.toDate().getTime()};return 0{let e={url:t.url,type:t.type};return"rich"===t.type&&Array.isArray(t.title)&&1===t.title.length&&"string"==typeof t.title[0]&&(e.t=t.title[0],Array.isArray(t.description)&&1===t.description.length&&"string"==typeof t.description[0]&&(e.d=t.description[0])),e})),0({url:t.url}))),null!==t.messageReference&&(e.r=t.messageReference.message_id),0{let e={c:t.count,n:t.emoji.name};return null!==t.emoji.id&&(e.id=t.emoji.id),t.emoji.animated&&(e.an=!0),e})),e}Tt(t){return this.vt.kt.has(t)}Ot(t,e){var n,r=!1;for(n of e){var s=n.type;0!==s&&19!==s||"SENT"!==n.state||!this.Lt(t,n.id,this.Ht(n))||(this.vt.kt.add(n.id),r=!0)}return r}W(){return this.vt.bt.size}J(){return this.vt.Ct.size}Mt(t){var e,n,r,s={},a=!1;for(e in t.meta.users){var i=t.meta.users[e];s[t.meta.userindex.findIndex(t=>t==e)]=this.St(e,i.name,i.tag,i.avatar)}for(n in t.meta.channels){var o=t.meta.servers[t.meta.channels[n].server],l=t.meta.channels[n];this.xt(this.yt(o.name,o.type),n,l.name,l)}for(n in t.data)for(r in l=t.data[n]){var c=l[r];(i=c.u)in s?(c.u=s[i],this.Lt(n,r,c)):(a||(a=!0,alert("The uploaded archive appears to be corrupted, some messages will be skipped. See console for details."),console.error("User list:",t.meta.users),console.error("User index:",t.meta.userindex),console.error("Generated mapping:",s),console.error("Missing user for the following messages:")),console.error(c))}}At(){return JSON.stringify({meta:this.meta,data:this.data})}}var d={ht:"optNothing",ft:"optPause",gt:"optSwitch"},r=!1,u=function(){function s(){f.M("DHT_SETTINGS",e,15768e4)}function t(e,n,t){var r="_"+n;Object.defineProperty(e,n,{get:()=>e[r],set:t=>{e[r]=t,function(t,e){for(var n of a)n(t,e);s()}("setting",n)}}),e[r]=t}var e={},a=[],n=f.A("DHT_SETTINGS");return n||(n={_autoscroll:!0,_afterFirstMsg:d.ft,_afterSavedMsg:d.ft},r=!0),t(e,"autoscroll",n._autoscroll),t(e,"afterFirstMsg",n._afterFirstMsg),t(e,"afterSavedMsg",n._afterSavedMsg),e.F=function(t){a.push(t)},r&&s(),e}(),h=function(){function a(t,e){for(var n of r)n(t,e)}var r=[];return new class{constructor(){this.it()}it(){this.Nt=null,this.Dt=!1,this.Ft=null,a("data","reset")}V(){return this.Nt||(this.Nt=new c),this.Nt}P(){return null!=this.Nt}R(){return this.Dt}st(t){this.Dt=t,a("tracking",t)}ct(t,e){this.Ft=t,this.V().Mt(e),a("data","upload")}at(){this.P()&&f.N(this.Ft||"dht.txt",this.Nt.At())}Rt(t,e,n,r,s){e=this.V().yt(t,e);!0===this.V().xt(e,n,r,s)&&a("data","channel")}$t(t,e){return!!this.V().Ot(t,e)&&(a("data","messages"),!0)}Tt(t){return this.V().Tt(t)}D(t){r.push(t)}}}();const e=window.location.href;if(!e.includes("discord.com/")&&!e.includes("discordapp.com/")&&!confirm("Could not detect Discord in the URL, do you want to run the script anyway?"))return;if(window.DHT_LOADED)return void alert("Discord History Tracker is already loaded.");window.DHT_LOADED=!0,window.l=[];let a=new Set,i=null,o=function(t){a.add("stopping"),f.T(()=>{h.st(!1),a.delete("stopping"),t&&t()},200)};s.o(t=>{if(h.R()&&0===a.size){let n=s.g();if(n)if(h.Rt(n.server,n.type,n.id,n.channel,n.extra),!1===t||t.length){var r=!1!==t&&h.$t(n.id,t);if(u.autoscroll){let e=null;!1===t?e=u.afterFirstMsg:r||h.Tt(t[0].id)||(e=u.afterSavedMsg),null===e?i=r?(s.k(),window.clearTimeout(i),null):window.setTimeout(s.k,2500):(a.add("stalling"),f.T(()=>{a.delete("stalling");var t=s.g();!t||t.id!==n.id||null!=(t=s.p())&&h.$t(n.id,t),(e!==d.gt||s.S())&&e!==d.ft||h.st(!1)},250))}}else s.k();else o()}}),h.D((t,e)=>{"tracking"===t&&e&&((t=s.g())?null!=(e=s.p())?(h.Rt(t.server,t.type,t.id,t.channel,t.extra),h.$t(t.id,e),u.autoscroll&&s.v()&&(s.C()?s.k():((e=u.afterFirstMsg)!==d.gt||s.S())&&e!==d.ft||o())):o(()=>alert("Cannot see any messages.")):o(()=>alert("The selected channel is not visible in the channel list.")))}),t.K(),r&&t.rt();})() \ No newline at end of file diff --git a/bld/track.user.js b/bld/track.user.js deleted file mode 100644 index bebe3a6a..00000000 --- a/bld/track.user.js +++ /dev/null @@ -1,1398 +0,0 @@ -// ==UserScript== -// @name Discord History Tracker -// @version v.31a -// @license MIT -// @namespace https://chylex.com -// @homepageURL https://dht.chylex.com/ -// @supportURL https://github.com/chylex/Discord-History-Tracker/issues -// @include https://discord.com/* -// @run-at document-idle -// @grant none -// ==/UserScript== - -const start = function(){ - -var DISCORD = (function(){ - var getMessageOuterElement = function(){ - return DOM.queryReactClass("messagesWrapper"); - }; - - var getMessageScrollerElement = function(){ - return getMessageOuterElement().querySelector("[class*='scroller-']"); - }; - - var getMessageElements = function() { - return getMessageOuterElement().querySelectorAll("[class*='message-']"); - }; - - var getReactProps = function(ele) { - var keys = Object.keys(ele || {}); - var key = keys.find(key => key.startsWith("__reactInternalInstance")); - - if (key){ - return ele[key].memoizedProps; - } - - key = keys.find(key => key.startsWith("__reactProps$")); - return key ? ele[key] : null; - }; - - var getMessageElementProps = function(ele) { - const props = getReactProps(ele); - - if (props.children && props.children.length >= 4) { - const childProps = props.children[3].props; - - if ("message" in childProps && "channel" in childProps) { - return childProps; - } - } - - return null; - }; - - var hasMoreMessages = function() { - return document.querySelector("#messagesNavigationDescription + [class^=container]") === null; - }; - - var getMessages = function() { - try { - const messages = []; - - for (const ele of getMessageElements()) { - const props = getMessageElementProps(ele); - - if (props != null) { - messages.push(props.message); - } - } - - return messages; - } catch (e) { - console.error(e); - return []; - } - }; - - return { - /** - * Calls the provided function with a list of messages whenever the currently loaded messages change, - * or with `false` if there are no more messages. - */ - setupMessageCallback: function(callback) { - let skipsLeft = 0; - let waitForCleanup = false; - let hasReachedStart = false; - const previousMessages = new Set(); - - const intervalId = window.setInterval(() => { - if (skipsLeft > 0) { - --skipsLeft; - return; - } - - const view = getMessageOuterElement(); - - if (!view) { - skipsLeft = 2; - return; - } - - const anyMessage = DOM.queryReactClass("message", getMessageOuterElement()); - const messageCount = anyMessage ? anyMessage.parentElement.children.length : 0; - - if (messageCount > 300) { - if (waitForCleanup) { - return; - } - - skipsLeft = 3; - waitForCleanup = true; - - window.setTimeout(() => { - const view = getMessageScrollerElement(); - view.scrollTop = view.scrollHeight / 2; - }, 1); - } - else { - waitForCleanup = false; - } - - const messages = getMessages(); - let hasChanged = false; - - for (const message of messages) { - if (!previousMessages.has(message.id)) { - hasChanged = true; - break; - } - } - - if (!hasChanged) { - if (!hasReachedStart && !hasMoreMessages()) { - hasReachedStart = true; - callback(false); - } - - return; - } - - previousMessages.clear(); - for (const message of messages) { - previousMessages.add(message.id); - } - - hasReachedStart = false; - callback(messages); - }, 200); - - window.DHT_ON_UNLOAD.push(() => window.clearInterval(intervalId)); - }, - - /* - * Returns internal React state object of an element. - */ - getReactProps: function(ele){ - return getReactProps(ele); - }, - - /* - * Returns an object containing the selected server name, selected channel name and ID, and the object type. - * For types DM and GROUP, the server and channel names are identical. - * For SERVER type, the channel has to be in view, otherwise Discord unloads it. - */ - getSelectedChannel: function() { - try { - let obj; - - for (const ele of getMessageElements()) { - const props = getMessageElementProps(ele); - - if (props != null) { - obj = props.channel; - break; - } - } - - if (!obj) { - return null; - } - - var dms = DOM.queryReactClass("privateChannels"); - - if (dms){ - let name; - - for (const ele of dms.querySelectorAll("[class*='channel-'] [class*='selected-'] [class^='name-'] *, [class*='channel-'][class*='selected-'] [class^='name-'] *")) { - const node = Array.prototype.find.call(ele.childNodes, node => node.nodeType === Node.TEXT_NODE); - - if (node) { - name = node.nodeValue; - break; - } - } - - if (!name) { - return null; - } - - let type; - - // https://discord.com/developers/docs/resources/channel#channel-object-channel-types - switch (obj.type) { - case 1: type = "DM"; break; - case 3: type = "GROUP"; break; - default: return null; - } - - return { - "server": name, - "channel": name, - "id": obj.id, - "type": type, - "extra": {} - }; - } - else if (obj.guild_id) { - return { - "server": document.querySelector("nav header > h1").innerText, - "channel": obj.name, - "id": obj.id, - "type": "SERVER", - "extra": { - "position": obj.position, - "topic": obj.topic, - "nsfw": obj.nsfw - } - }; - } - else { - return null; - } - } catch(e) { - console.error(e); - return null; - } - }, - - /* - * Returns an array containing currently loaded messages. - */ - getMessages: function(){ - return getMessages(); - }, - - /* - * Returns true if the message view is visible. - */ - isInMessageView: () => !!getMessageOuterElement(), - - /* - * Returns true if there are more messages available or if they're still loading. - */ - hasMoreMessages: function(){ - return hasMoreMessages(); - }, - - /* - * Forces the message view to load older messages by scrolling all the way up. - */ - loadOlderMessages: function(){ - let view = getMessageScrollerElement(); - - if (view.scrollTop > 0){ - view.scrollTop = 0; - } - }, - - /* - * Selects the next text channel and returns true, otherwise returns false if there are no more channels. - */ - selectNextTextChannel: function(){ - var dms = DOM.queryReactClass("privateChannels"); - - if (dms){ - var currentChannel = DOM.queryReactClass("selected", dms); - var nextChannel = currentChannel && currentChannel.nextElementSibling; - - if (!nextChannel || !nextChannel.getAttribute("class").includes("channel-") || !("href" in nextChannel) || !nextChannel.href.includes("/@me/")){ - return false; - } - else{ - nextChannel.click(); - nextChannel.scrollIntoView(true); - return true; - } - } - else{ - var channelIconNormal = "M5.88657 21C5.57547 21 5.3399 20.7189 5.39427 20.4126L6.00001 17H2.59511C2.28449 17 2.04905 16.7198 2.10259 16.4138L2.27759 15.4138C2.31946 15.1746 2.52722 15 2.77011 15H6.35001L7.41001 9H4.00511C3.69449 9 3.45905 8.71977 3.51259 8.41381L3.68759 7.41381C3.72946 7.17456 3.93722 7 4.18011 7H7.76001L8.39677 3.41262C8.43914 3.17391 8.64664 3 8.88907 3H9.87344C10.1845 3 10.4201 3.28107 10.3657 3.58738L9.76001 7H15.76L16.3968 3.41262C16.4391 3.17391 16.6466 3 16.8891 3H17.8734C18.1845 3 18.4201 3.28107 18.3657 3.58738L17.76 7H21.1649C21.4755 7 21.711 7.28023 21.6574 7.58619L21.4824 8.58619C21.4406 8.82544 21.2328 9 20.9899 9H17.41L16.35 15H19.7549C20.0655 15 20.301 15.2802 20.2474 15.5862L20.0724 16.5862C20.0306 16.8254 19.8228 17 19.5799 17H16L15.3632 20.5874C15.3209 20.8261 15.1134 21 14.8709 21H13.8866C13.5755 21 13.3399 20.7189 13.3943 20.4126L14 17H8.00001L7.36325 20.5874C7.32088 20.8261 7.11337 21 6.87094 21H5.88657ZM9.41045 9L8.35045 15H14.3504L15.4104 9H9.41045Z"; - var channelIconSpecial = "M14 8C14 7.44772 13.5523 7 13 7H9.76001L10.3657 3.58738C10.4201 3.28107 10.1845 3 9.87344 3H8.88907C8.64664 3 8.43914 3.17391 8.39677 3.41262L7.76001 7H4.18011C3.93722 7 3.72946 7.17456 3.68759 7.41381L3.51259 8.41381C3.45905 8.71977 3.69449 9 4.00511 9H7.41001L6.35001 15H2.77011C2.52722 15 2.31946 15.1746 2.27759 15.4138L2.10259 16.4138C2.04905 16.7198 2.28449 17 2.59511 17H6.00001L5.39427 20.4126C5.3399 20.7189 5.57547 21 5.88657 21H6.87094C7.11337 21 7.32088 20.8261 7.36325 20.5874L8.00001 17H14L13.3943 20.4126C13.3399 20.7189 13.5755 21 13.8866 21H14.8709C15.1134 21 15.3209 20.8261 15.3632 20.5874L16 17H19.5799C19.8228 17 20.0306 16.8254 20.0724 16.5862L20.2474 15.5862C20.301 15.2802 20.0655 15 19.7549 15H16.35L16.6758 13.1558C16.7823 12.5529 16.3186 12 15.7063 12C15.2286 12 14.8199 12.3429 14.7368 12.8133L14.3504 15H8.35045L9.41045 9H13C13.5523 9 14 8.55228 14 8Z"; - - var isValidChannelClass = cls => cls.includes("wrapper-") && !cls.includes("clickable-"); - var isValidChannelType = ele => !!ele.querySelector('path[d="' + channelIconNormal + '"]') || !!ele.querySelector('path[d="' + channelIconSpecial + '"]'); - var isValidChannel = ele => ele.childElementCount > 0 && isValidChannelClass(ele.children[0].className) && isValidChannelType(ele); - - var channelListEle = document.querySelector("div[class*='sidebar'] > nav[class*='container'] > div[class*='scroller']"); - - if (!channelListEle){ - return false; - } - - var allChannels = Array.prototype.filter.call(channelListEle.querySelectorAll("[class*='containerDefault']"), isValidChannel); - var nextChannel = null; - - for(var index = 0; index < allChannels.length-1; index++){ - if (allChannels[index].children[0].className.includes("modeSelected")){ - nextChannel = allChannels[index+1]; - break; - } - } - - if (nextChannel === null){ - return false; - } - - const nextChannelLink = nextChannel.querySelector("a[href^='/channels/']"); - if (!nextChannelLink) { - return false; - } - - nextChannelLink.click(); - nextChannel.scrollIntoView(true); - return true; - } - } - }; -})(); - -var DOM = (function(){ - var createElement = (tag, parent, id, html) => { - var ele = document.createElement(tag); - ele.id = id || ""; - ele.innerHTML = html || ""; - parent.appendChild(ele); - return ele; - }; - - return { - /* - * Returns a child element by its ID. Parent defaults to the entire document. - */ - id: (id, parent) => (parent || document).getElementById(id), - - /* - * Returns the first child element containing the specified obfuscated class. Parent defaults to the entire document. - */ - queryReactClass: (cls, parent) => (parent || document).querySelector(`[class*="${cls}-"]`), - - /* - * Creates an element, adds it to the DOM, and returns it. - */ - createElement: (tag, parent, id, html) => createElement(tag, parent, id, html), - - /* - * Removes an element from the DOM. - */ - removeElement: (ele) => ele.parentNode.removeChild(ele), - - /* - * Creates a new style element with the specified CSS and returns it. - */ - createStyle: (styles) => createElement("style", document.head, "", styles), - - /* - * Convenience setTimeout function to save space after minification. - */ - setTimer: (callback, timeout) => window.setTimeout(callback, timeout), - - /* - * Convenience addEventListener function to save space after minification. - */ - listen: (ele, event, callback) => ele.addEventListener(event, callback), - - /* - * Utility function to save an object into a cookie. - */ - saveToCookie: (name, obj, expiresInSeconds) => { - var expires = new Date(Date.now()+1000*expiresInSeconds).toUTCString(); - document.cookie = name+"="+encodeURIComponent(JSON.stringify(obj))+";path=/;expires="+expires; - }, - - /* - * Utility function to load an object from a cookie. - */ - loadFromCookie: (name) => { - var value = document.cookie.replace(new RegExp("(?:(?:^|.*;\\s*)"+name+"\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1"); - return value.length ? JSON.parse(decodeURIComponent(value)) : null; - }, - - /* - * Triggers a UTF-8 text file download. - */ - downloadTextFile: (fileName, fileContents) => { - var blob = new Blob([fileContents], { "type": "octet/stream" }); - - if ("msSaveBlob" in window.navigator){ - return window.navigator.msSaveBlob(blob, fileName); - } - - var url = window.URL.createObjectURL(blob); - - var ele = createElement("a", document.body); - ele.href = url; - ele.download = fileName; - ele.style.display = "none"; - - ele.click(); - - document.body.removeChild(ele); - window.URL.revokeObjectURL(url); - } - }; -})(); - -var GUI = (function(){ - var controller; - var settings; - - var updateButtonState = () => { - if (STATE.isTracking()){ - controller.ui.btnUpload.disabled = true; - controller.ui.btnSettings.disabled = true; - controller.ui.btnReset.disabled = true; - } - else{ - controller.ui.btnUpload.disabled = false; - controller.ui.btnSettings.disabled = false; - controller.ui.btnDownload.disabled = controller.ui.btnReset.disabled = !STATE.hasSavedData(); - } - }; - - var stateChangedEvent = (type, detail) => { - if (controller){ - var force = type === "gui" && detail === "controller"; - - if (type === "data" || force){ - updateButtonState(); - } - - if (type === "tracking" || force){ - updateButtonState(); - controller.ui.btnToggleTracking.innerHTML = STATE.isTracking() ? "Pause Tracking" : "Start Tracking"; - } - - if (type === "data" || force){ - var messageCount = 0; - var channelCount = 0; - - if (STATE.hasSavedData()){ - messageCount = STATE.getSavefile().countMessages(); - channelCount = STATE.getSavefile().countChannels(); - } - - controller.ui.textStatus.innerHTML = [ - messageCount, " message", (messageCount === 1 ? "" : "s"), - " from ", - channelCount, " channel", (channelCount === 1 ? "" : "s") - ].join(""); - } - } - - if (settings){ - var force = type === "gui" && detail === "settings"; - - if (force){ - settings.ui.cbAutoscroll.checked = SETTINGS.autoscroll; - settings.ui.optsAfterFirstMsg[SETTINGS.afterFirstMsg].checked = true; - settings.ui.optsAfterSavedMsg[SETTINGS.afterSavedMsg].checked = true; - } - - if (type === "setting" || force){ - var autoscrollRev = !SETTINGS.autoscroll; - - // discord polyfills Object.values - Object.values(settings.ui.optsAfterFirstMsg).forEach(ele => ele.disabled = autoscrollRev); - Object.values(settings.ui.optsAfterSavedMsg).forEach(ele => ele.disabled = autoscrollRev); - } - } - }; - - var registeredEvent = false; - - var setupStateChanged = function(detail){ - if (!registeredEvent){ - STATE.onStateChanged(stateChangedEvent); - SETTINGS.onSettingsChanged(stateChangedEvent); - registeredEvent = true; - } - - stateChangedEvent("gui", detail); - }; - - var root = { - showController: function(){ - controller = {}; - - // styles - - controller.styles = DOM.createStyle(` -#app-mount > div[class*="app-"] { margin-bottom: 48px !important; } -#dht-ctrl { position: absolute; bottom: 0; width: 100%; height: 48px; background-color: #FFF; } -#dht-ctrl button { height: 32px; margin: 8px 0 8px 8px; font-size: 16px; padding: 0 12px; background-color: #7289DA; color: #FFF; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.75); } -#dht-ctrl button:disabled { background-color: #7A7A7A; cursor: default; } -#dht-ctrl-close { margin: 8px 8px 8px 0 !important; float: right; } -#dht-ctrl p { display: inline-block; margin: 14px 12px; } -#dht-ctrl input { display: none; }`); - - // main - - var btn = (id, title) => ""; - - controller.ele = DOM.createElement("div", document.body, "dht-ctrl", ` -${btn("upload", "Upload & Combine")} -${btn("settings", "Settings")} -${btn("track", "")} -${btn("download", "Download")} -${btn("reset", "Reset")} -

- -${btn("close", "X")}`); - - // elements - - controller.ui = { - btnUpload: DOM.id("dht-ctrl-upload"), - btnSettings: DOM.id("dht-ctrl-settings"), - btnToggleTracking: DOM.id("dht-ctrl-track"), - btnDownload: DOM.id("dht-ctrl-download"), - btnReset: DOM.id("dht-ctrl-reset"), - btnClose: DOM.id("dht-ctrl-close"), - textStatus: DOM.id("dht-ctrl-status"), - inputUpload: DOM.id("dht-ctrl-upload-input") - }; - - // events - - DOM.listen(controller.ui.btnUpload, "click", () => { - controller.ui.inputUpload.click(); - }); - - DOM.listen(controller.ui.btnSettings, "click", () => { - root.showSettings(); - }); - - DOM.listen(controller.ui.btnToggleTracking, "click", () => { - STATE.setIsTracking(!STATE.isTracking()); - }); - - DOM.listen(controller.ui.btnDownload, "click", () => { - STATE.downloadSavefile(); - }); - - DOM.listen(controller.ui.btnReset, "click", () => { - STATE.resetState(); - }); - - DOM.listen(controller.ui.btnClose, "click", () => { - root.hideController(); - window.DHT_ON_UNLOAD.forEach(f => f()); - window.DHT_LOADED = false; - }); - - DOM.listen(controller.ui.inputUpload, "change", () => { - Array.prototype.forEach.call(controller.ui.inputUpload.files, file => { - var reader = new FileReader(); - - reader.onload = function(){ - var obj = {}; - - try{ - obj = JSON.parse(reader.result); - }catch(e){ - alert("Could not parse '"+file.name+"', see console for details."); - console.error(e); - return; - } - - if (SAVEFILE.isValid(obj)){ - STATE.uploadSavefile(file.name, new SAVEFILE(obj)); - } - else{ - alert("File '"+file.name+"' has an invalid format."); - } - }; - - reader.readAsText(file, "UTF-8"); - }); - - controller.ui.inputUpload.value = null; - }); - - setupStateChanged("controller"); - }, - - hideController: function(){ - if (controller){ - DOM.removeElement(controller.ele); - DOM.removeElement(controller.styles); - controller = null; - } - }, - - showSettings: function(){ - settings = {}; - - // styles - - settings.styles = DOM.createStyle(` -#dht-cfg-overlay { position: absolute; left: 0; top: 0; width: 100%; height: 100%; background-color: #000; opacity: 0.5; display: block; z-index: 1000; } -#dht-cfg { position: absolute; left: 50%; top: 50%; width: 800px; height: 262px; margin-left: -400px; margin-top: -131px; padding: 8px; background-color: #fff; z-index: 1001; } -#dht-cfg-note { margin-top: 22px; } -#dht-cfg sub { color: #666; font-size: 13px; }`); - - // overlay - - settings.overlay = DOM.createElement("div", document.body, "dht-cfg-overlay"); - - DOM.listen(settings.overlay, "click", () => { - root.hideSettings(); - }); - - // main - - var radio = (type, id, label) => "
"; - - settings.ele = DOM.createElement("div", document.body, "dht-cfg", ` -
-
-
-${radio("afm", "nothing", "Do Nothing")} -${radio("afm", "pause", "Pause Tracking")} -${radio("afm", "switch", "Switch to Next Channel")} -
-
-${radio("asm", "nothing", "Do Nothing")} -${radio("asm", "pause", "Pause Tracking")} -${radio("asm", "switch", "Switch to Next Channel")} -

-It is recommended to disable link and image previews to avoid putting unnecessary strain on your browser.

-v.31a, released 12 Feb 2022 -

`); - - // elements - - settings.ui = { - cbAutoscroll: DOM.id("dht-cfg-autoscroll"), - optsAfterFirstMsg: {}, - optsAfterSavedMsg: {} - }; - - settings.ui.optsAfterFirstMsg[CONSTANTS.AUTOSCROLL_ACTION_NOTHING] = DOM.id("dht-cfg-afm-nothing"); - settings.ui.optsAfterFirstMsg[CONSTANTS.AUTOSCROLL_ACTION_PAUSE] = DOM.id("dht-cfg-afm-pause"); - settings.ui.optsAfterFirstMsg[CONSTANTS.AUTOSCROLL_ACTION_SWITCH] = DOM.id("dht-cfg-afm-switch"); - - settings.ui.optsAfterSavedMsg[CONSTANTS.AUTOSCROLL_ACTION_NOTHING] = DOM.id("dht-cfg-asm-nothing"); - settings.ui.optsAfterSavedMsg[CONSTANTS.AUTOSCROLL_ACTION_PAUSE] = DOM.id("dht-cfg-asm-pause"); - settings.ui.optsAfterSavedMsg[CONSTANTS.AUTOSCROLL_ACTION_SWITCH] = DOM.id("dht-cfg-asm-switch"); - - // events - - settings.ui.cbAutoscroll.addEventListener("change", () => { - SETTINGS.autoscroll = settings.ui.cbAutoscroll.checked; - }); - - Object.keys(settings.ui.optsAfterFirstMsg).forEach(key => { - DOM.listen(settings.ui.optsAfterFirstMsg[key], "click", () => { - SETTINGS.afterFirstMsg = key; - }); - }); - - Object.keys(settings.ui.optsAfterSavedMsg).forEach(key => { - DOM.listen(settings.ui.optsAfterSavedMsg[key], "click", () => { - SETTINGS.afterSavedMsg = key; - }); - }); - - setupStateChanged("settings"); - }, - - hideSettings: function(){ - if (settings){ - DOM.removeElement(settings.overlay); - DOM.removeElement(settings.ele); - DOM.removeElement(settings.styles); - settings = null; - } - } - }; - - return root; -})(); - -/* - * SAVEFILE STRUCTURE - * ================== - * - * { - * meta: { - * users: { - * : { - * name: , - * avatar: , - * tag: // only present if not a bot - * }, ... - * }, - * - * // the user index is an array of discord user ids, - * // these indexes are used in the message objects to save space - * userindex: [ - * , ... - * ], - * - * servers: [ - * { - * name: , - * type: <"SERVER"|"GROUP"|DM"> - * }, ... - * ], - * - * channels: { - * : { - * server: , - * name: , - * position: , // only present if server type == SERVER - * topic: , // only present if server type == SERVER - * nsfw: // only present if server type == SERVER - * }, ... - * } - * }, - * - * data: { - * : { - * : { - * u: , - * t: , - * m: , // only present if not empty - * f: , // only present if edited in which case it equals 1, deprecated (use 'te' instead) - * te: , // only present if edited - * e: [ // omit for no embeds - * { - * url: , - * type: , - * t: , // only present if type == rich, and if not empty - * d: // only present if type == rich, and if the embed has a simple description text - * }, ... - * ], - * a: [ // omit for no attachments - * { - * url: - * }, ... - * ], - * r: , // only present if referencing another message (reply) - * re: [ // omit for no reactions - * { - * c: - * n: , - * id: , // only present for custom emoji - * an: , // only present for custom animated emoji - * }, ... - * ] - * }, ... - * }, ... - * } - * } - * - * - * TEMPORARY OBJECT STRUCTURE - * ========================== - * - * { - * userlookup: { - * : - * }, - * channelkeys: Set, - * messagekeys: Set, - * freshmsgs: Set // only messages which were newly added to the savefile in the current session - * } - */ - -class SAVEFILE{ - constructor(parsedObj){ - var me = this; - - if (!SAVEFILE.isValid(parsedObj)){ - parsedObj = { - meta: {}, - data: {} - }; - } - - me.meta = parsedObj.meta; - me.data = parsedObj.data; - - me.meta.users = me.meta.users || {}; - me.meta.userindex = me.meta.userindex || []; - me.meta.servers = me.meta.servers || []; - me.meta.channels = me.meta.channels || {}; - - me.tmp = { - userlookup: {}, - channelkeys: new Set(), - messagekeys: new Set(), - freshmsgs: new Set() - }; - } - - static isValid(parsedObj){ - return parsedObj && typeof parsedObj.meta === "object" && typeof parsedObj.data === "object"; - } - - findOrRegisterUser(userId, userName, userDiscriminator, userAvatar){ - var wasPresent = userId in this.meta.users; - var userObj = wasPresent ? this.meta.users[userId] : {}; - - userObj.name = userName; - - if (userDiscriminator){ - userObj.tag = userDiscriminator; - } - - if (userAvatar){ - userObj.avatar = userAvatar; - } - - if (!wasPresent){ - this.meta.users[userId] = userObj; - this.meta.userindex.push(userId); - return this.tmp.userlookup[userId] = this.meta.userindex.length-1; - } - else if (!(userId in this.tmp.userlookup)){ - return this.tmp.userlookup[userId] = this.meta.userindex.findIndex(id => id == userId); - } - else{ - return this.tmp.userlookup[userId]; - } - } - - findOrRegisterServer(serverName, serverType){ - var index = this.meta.servers.findIndex(server => server.name === serverName && server.type === serverType); - - if (index === -1){ - this.meta.servers.push({ - "name": serverName, - "type": serverType - }); - - return this.meta.servers.length-1; - } - else{ - return index; - } - } - - tryRegisterChannel(serverIndex, channelId, channelName, extraInfo){ - if (!this.meta.servers[serverIndex]){ - return undefined; - } - - var wasPresent = channelId in this.meta.channels; - var channelObj = wasPresent ? this.meta.channels[channelId] : { "server": serverIndex }; - - channelObj.name = channelName; - - if (extraInfo.position){ - channelObj.position = extraInfo.position; - } - - if (extraInfo.topic){ - channelObj.topic = extraInfo.topic; - } - - if (extraInfo.nsfw){ - channelObj.nsfw = extraInfo.nsfw; - } - - if (wasPresent){ - return false; - } - else{ - this.meta.channels[channelId] = channelObj; - this.tmp.channelkeys.add(channelId); - return true; - } - } - - addMessage(channelId, messageId, messageObject){ - var container = this.data[channelId] || (this.data[channelId] = {}); - var wasPresent = messageId in container; - - container[messageId] = messageObject; - this.tmp.messagekeys.add(messageId); - return !wasPresent; - } - - convertToMessageObject(discordMessage){ - var author = discordMessage.author; - - var obj = { - u: this.findOrRegisterUser(author.id, author.username, author.bot ? null : author.discriminator, author.avatar), - t: discordMessage.timestamp.toDate().getTime() - }; - - if (discordMessage.content.length > 0){ - obj.m = discordMessage.content; - } - - if (discordMessage.editedTimestamp !== null){ - obj.te = discordMessage.editedTimestamp.toDate().getTime(); - } - - if (discordMessage.embeds.length > 0){ - obj.e = discordMessage.embeds.map(embed => { - let conv = { - url: embed.url, - type: embed.type - }; - - if (embed.type === "rich"){ - if (Array.isArray(embed.title) && embed.title.length === 1 && typeof embed.title[0] === "string"){ - conv.t = embed.title[0]; - - if (Array.isArray(embed.description) && embed.description.length === 1 && typeof embed.description[0] === "string"){ - conv.d = embed.description[0]; - } - } - } - - return conv; - }); - } - - if (discordMessage.attachments.length > 0){ - obj.a = discordMessage.attachments.map(attachment => ({ - url: attachment.url - })); - } - - if (discordMessage.messageReference !== null){ - obj.r = discordMessage.messageReference.message_id; - } - - if (discordMessage.reactions.length > 0) { - obj.re = discordMessage.reactions.map(reaction => { - let conv = { - c: reaction.count, - n: reaction.emoji.name - }; - - if (reaction.emoji.id !== null) { - conv.id = reaction.emoji.id; - } - - if (reaction.emoji.animated) { - conv.an = true; - } - - return conv; - }); - } - - return obj; - } - - isMessageFresh(id){ - return this.tmp.freshmsgs.has(id); - } - - addMessagesFromDiscord(channelId, discordMessageArray){ - var hasNewMessages = false; - - for(var discordMessage of discordMessageArray){ - var type = discordMessage.type; - - // https://discord.com/developers/docs/resources/channel#message-object-message-reference-structure - if ((type === 0 || type === 19) && discordMessage.state === "SENT" && this.addMessage(channelId, discordMessage.id, this.convertToMessageObject(discordMessage))){ - this.tmp.freshmsgs.add(discordMessage.id); - hasNewMessages = true; - } - } - - return hasNewMessages; - } - - countChannels(){ - return this.tmp.channelkeys.size; - } - - countMessages(){ - return this.tmp.messagekeys.size; - } - - combineWith(obj){ - var userMap = {}; - var shownError = false; - - for(var userId in obj.meta.users){ - var oldUser = obj.meta.users[userId]; - userMap[obj.meta.userindex.findIndex(id => id == userId)] = this.findOrRegisterUser(userId, oldUser.name, oldUser.tag, oldUser.avatar); - } - - for(var channelId in obj.meta.channels){ - var oldServer = obj.meta.servers[obj.meta.channels[channelId].server]; - var oldChannel = obj.meta.channels[channelId]; - this.tryRegisterChannel(this.findOrRegisterServer(oldServer.name, oldServer.type), channelId, oldChannel.name, oldChannel /* filtered later */); - } - - for(var channelId in obj.data){ - var oldChannel = obj.data[channelId]; - - for(var messageId in oldChannel){ - var oldMessage = oldChannel[messageId]; - var oldUser = oldMessage.u; - - if (oldUser in userMap){ - oldMessage.u = userMap[oldUser]; - this.addMessage(channelId, messageId, oldMessage); - } - else{ - if (!shownError){ - shownError = true; - alert("The uploaded archive appears to be corrupted, some messages will be skipped. See console for details."); - - console.error("User list:", obj.meta.users); - console.error("User index:", obj.meta.userindex); - console.error("Generated mapping:", userMap); - console.error("Missing user for the following messages:"); - } - - console.error(oldMessage); - } - } - } - } - - toJson(){ - return JSON.stringify({ - "meta": this.meta, - "data": this.data - }); - } -} - -var CONSTANTS = { - AUTOSCROLL_ACTION_NOTHING: "optNothing", - AUTOSCROLL_ACTION_PAUSE: "optPause", - AUTOSCROLL_ACTION_SWITCH: "optSwitch" -}; - -var IS_FIRST_RUN = false; - -var SETTINGS = (function(){ - var root = {}; - var settingsChangedEvents = []; - - var saveSettings = function(){ - DOM.saveToCookie("DHT_SETTINGS", root, 60*60*24*365*5); - }; - - var triggerSettingsChanged = function(changeType, changeDetail){ - for(var callback of settingsChangedEvents){ - callback(changeType, changeDetail); - } - - saveSettings(); - }; - - var defineTriggeringProperty = function(obj, property, value){ - var name = "_"+property; - - Object.defineProperty(obj, property, { - get: (() => obj[name]), - set: (value => { - obj[name] = value; - triggerSettingsChanged("setting", property); - }) - }); - - obj[name] = value; - }; - - var loaded = DOM.loadFromCookie("DHT_SETTINGS"); - - if (!loaded){ - loaded = { - "_autoscroll": true, - "_afterFirstMsg": CONSTANTS.AUTOSCROLL_ACTION_PAUSE, - "_afterSavedMsg": CONSTANTS.AUTOSCROLL_ACTION_PAUSE - }; - - IS_FIRST_RUN = true; - } - - defineTriggeringProperty(root, "autoscroll", loaded._autoscroll); - defineTriggeringProperty(root, "afterFirstMsg", loaded._afterFirstMsg); - defineTriggeringProperty(root, "afterSavedMsg", loaded._afterSavedMsg); - - root.onSettingsChanged = function(callback){ - settingsChangedEvents.push(callback); - }; - - if (IS_FIRST_RUN){ - saveSettings(); - } - - return root; -})(); - -var STATE = (function(){ - var stateChangedEvents = []; - - var triggerStateChanged = function(changeType, changeDetail){ - for(var callback of stateChangedEvents){ - callback(changeType, changeDetail); - } - }; - - /* - * Internal class constructor. - */ - class CLS{ - constructor(){ - this.resetState(); - }; - - /* - * Resets the state to default values. - */ - resetState(){ - this._savefile = null; - this._isTracking = false; - this._lastFileName = null; - triggerStateChanged("data", "reset"); - } - - /* - * Returns the savefile object, creates a new one if needed. - */ - getSavefile(){ - if (!this._savefile){ - this._savefile = new SAVEFILE(); - } - - return this._savefile; - } - - /* - * Returns true if the database file contains any data. - */ - hasSavedData(){ - return this._savefile != null; - } - - /* - * Returns true if currently tracking message. - */ - isTracking(){ - return this._isTracking; - } - - /* - * Sets the tracking state. - */ - setIsTracking(state){ - this._isTracking = state; - triggerStateChanged("tracking", state); - } - - /* - * Combines current savefile with the provided one. - */ - uploadSavefile(fileName, fileObject){ - this._lastFileName = fileName; - this.getSavefile().combineWith(fileObject); - triggerStateChanged("data", "upload"); - } - - /* - * Triggers a savefile download, if available. - */ - downloadSavefile(){ - if (this.hasSavedData()){ - DOM.downloadTextFile(this._lastFileName || "dht.txt", this._savefile.toJson()); - } - } - - /* - * Registers a Discord server and channel. - */ - addDiscordChannel(serverName, serverType, channelId, channelName, extraInfo){ - var serverIndex = this.getSavefile().findOrRegisterServer(serverName, serverType); - - if (this.getSavefile().tryRegisterChannel(serverIndex, channelId, channelName, extraInfo) === true){ - triggerStateChanged("data", "channel"); - } - } - - /* - * Adds all messages from the array to the specified channel. Returns true if the savefile was updated. - */ - addDiscordMessages(channelId, discordMessageArray){ - if (this.getSavefile().addMessagesFromDiscord(channelId, discordMessageArray)){ - triggerStateChanged("data", "messages"); - return true; - } - else{ - return false; - } - } - - /* - * Returns true if the message was added during this session. - */ - isMessageFresh(id){ - return this.getSavefile().isMessageFresh(id); - } - - /* - * Adds a listener that is called whenever the state changes. The callback is a function that takes subject (generic type) and detail (specific type or data). - */ - onStateChanged(callback){ - stateChangedEvents.push(callback); - } - } - - return new CLS(); -})(); - -const url = window.location.href; - -if (!url.includes("discord.com/") && !url.includes("discordapp.com/") && !confirm("Could not detect Discord in the URL, do you want to run the script anyway?")){ - return; -} - -if (window.DHT_LOADED){ - alert("Discord History Tracker is already loaded."); - return; -} - -window.DHT_LOADED = true; -window.DHT_ON_UNLOAD = []; - -// Execution - -let ignoreMessageCallback = new Set(); -let frozenMessageLoadingTimer = null; - -let stopTrackingDelayed = function(callback){ - ignoreMessageCallback.add("stopping"); - - DOM.setTimer(() => { - STATE.setIsTracking(false); - ignoreMessageCallback.delete("stopping"); - - if (callback){ - callback(); - } - }, 200); // give the user visual feedback after clicking the button before switching off -}; - -DISCORD.setupMessageCallback(messages => { - if (STATE.isTracking() && ignoreMessageCallback.size === 0){ - let info = DISCORD.getSelectedChannel(); - - if (!info){ - stopTrackingDelayed(); - return; - } - - STATE.addDiscordChannel(info.server, info.type, info.id, info.channel, info.extra); - - if (messages !== false && !messages.length){ - DISCORD.loadOlderMessages(); - return; - } - - let hasUpdatedFile = messages !== false && STATE.addDiscordMessages(info.id, messages); - - if (SETTINGS.autoscroll){ - let action = null; - - if (messages === false) { - action = SETTINGS.afterFirstMsg; - } - else if (!hasUpdatedFile && !STATE.isMessageFresh(messages[0].id)){ - action = SETTINGS.afterSavedMsg; - } - - if (action === null){ - if (hasUpdatedFile){ - DISCORD.loadOlderMessages(); - window.clearTimeout(frozenMessageLoadingTimer); - frozenMessageLoadingTimer = null; - } - else{ - frozenMessageLoadingTimer = window.setTimeout(DISCORD.loadOlderMessages, 2500); - } - } - else{ - ignoreMessageCallback.add("stalling"); - - DOM.setTimer(() => { - ignoreMessageCallback.delete("stalling"); - - let updatedInfo = DISCORD.getSelectedChannel(); - - if (updatedInfo && updatedInfo.id === info.id){ - let lastMessages = DISCORD.getMessages(); // sometimes needed to catch the last few messages before switching - - if (lastMessages != null){ - STATE.addDiscordMessages(info.id, lastMessages); - } - } - - if ((action === CONSTANTS.AUTOSCROLL_ACTION_SWITCH && !DISCORD.selectNextTextChannel()) || action === CONSTANTS.AUTOSCROLL_ACTION_PAUSE){ - STATE.setIsTracking(false); - } - }, 250); - } - } - } -}); - -STATE.onStateChanged((type, enabled) => { - if (type === "tracking" && enabled){ - let info = DISCORD.getSelectedChannel(); - - if (info){ - let messages = DISCORD.getMessages(); - - if (messages != null){ - STATE.addDiscordChannel(info.server, info.type, info.id, info.channel, info.extra); - STATE.addDiscordMessages(info.id, messages); - } - else{ - stopTrackingDelayed(() => alert("Cannot see any messages.")); - return; - } - } - else{ - stopTrackingDelayed(() => alert("The selected channel is not visible in the channel list.")); - return; - } - - if (SETTINGS.autoscroll && DISCORD.isInMessageView()){ - if (DISCORD.hasMoreMessages()){ - DISCORD.loadOlderMessages(); - } - else{ - let action = SETTINGS.afterFirstMsg; - - if ((action === CONSTANTS.AUTOSCROLL_ACTION_SWITCH && !DISCORD.selectNextTextChannel()) || action === CONSTANTS.AUTOSCROLL_ACTION_PAUSE){ - stopTrackingDelayed(); - } - } - } - } -}); - -GUI.showController(); - -if (IS_FIRST_RUN){ - GUI.showSettings(); -} - - -}; - -const css = document.createElement("style"); - -css.innerText = ` -#dht-userscript-trigger { cursor: pointer; margin-top: 5px } -#dht-userscript-trigger svg { opacity: 0.6 } -#dht-userscript-trigger:hover svg { opacity: 1 } -`; - -document.head.appendChild(css); - -window.setInterval(function(){ - if (document.getElementById("dht-userscript-trigger")){ - return; - } - - const help = document.querySelector("section[class^='title'] a[href*='support.discord.com']"); - - if (help){ - help.insertAdjacentHTML("afterend", ` - - - - - - - - -`); - - document.getElementById("dht-userscript-trigger").addEventListener("click", start); - } -}, 200); diff --git a/bld/viewer.html b/bld/viewer.html deleted file mode 100644 index 9e9a651b..00000000 --- a/bld/viewer.html +++ /dev/null @@ -1,170 +0,0 @@ - - - - - Discord Offline History - - - - - - - -
-
-
-
- - - - diff --git a/build.py b/build.py deleted file mode 100644 index e54066f7..00000000 --- a/build.py +++ /dev/null @@ -1,225 +0,0 @@ -# Python 3 - -import fileinput -import glob -import shutil -import sys -import os -import re -import distutils.dir_util - -VERSION_SHORT = "v.31a" -VERSION_FULL = VERSION_SHORT + ", released 12 Feb 2022" - -EXEC_UGLIFYJS_WIN = "{2}/lib/uglifyjs.cmd --parse bare_returns --compress --mangle toplevel --mangle-props keep_quoted,reserved=[{3}] --output \"{1}\" \"{0}\"" -EXEC_UGLIFYJS_AUTO = "uglifyjs --parse bare_returns --compress --mangle toplevel --mangle-props keep_quoted,reserved=[{3}] --output \"{1}\" \"{0}\"" - -USE_UGLIFYJS = "--nominify" not in sys.argv -USE_MINIFICATION = "--nominify" not in sys.argv -BUILD_WEBSITE = "--website" in sys.argv -CLIPBOARD_TRACKER = "--copytracker" in sys.argv - -WORKING_DIR = os.getcwd() - -# UglifyJS Setup - -if os.name == "nt": - EXEC_UGLIFYJS = EXEC_UGLIFYJS_WIN -else: - EXEC_UGLIFYJS = EXEC_UGLIFYJS_AUTO - - if USE_UGLIFYJS and shutil.which("uglifyjs") is None: - USE_UGLIFYJS = False - print("Could not find 'uglifyjs', JS minification will be disabled") - -if USE_UGLIFYJS: - with open("reserve.txt", "r") as reserved: - RESERVED_PROPS = ",".join(line.strip() for line in reserved.readlines()) - - -# File Utilities - -def combine_files(input_pattern, output_file): - is_first_file = True - - with fileinput.input(sorted(glob.glob(input_pattern))) as stream: - for line in stream: - if stream.isfirstline(): - if is_first_file: - is_first_file = False - else: - output_file.write("\n") - - output_file.write(line.replace("{{{version:full}}}", VERSION_FULL)) - - -def minify_css(input_file, output_file): - if not USE_MINIFICATION: - if input_file != output_file: - shutil.copyfile(input_file, output_file) - - return - - with open(input_file, "r") as fin: - css = fin.read() - - css = re.sub(r"^\s+(.+?):\s*(.+?)(?:\s*(!important))?;\n", r"\1:\2\3;", css, flags = re.M) # remove spaces after colons - css = re.sub(r"\{\n", r"{", css, flags = re.M) # remove new lines after { - css = re.sub(r"\n\}", r"}", css, flags = re.M) # remove new lines before } - css = re.sub(r"\n\n", r"\n", css, flags = re.M) # remove empty lines - css = re.sub(r";\}$", r"}", css, flags = re.M) # remove last semicolons - css = re.sub(r"rgb\((.*?),\s*(.*?),\s*(.*?)\)", r"rgb(\1,\2,\3)", css, flags = re.M) # remove spaces after commas in rgb() - css = re.sub(r"rgba\((.*?),\s*(.*?),\s*(.*?),\s*(.*?)\)", r"rgba(\1,\2,\3,\4)", css, flags = re.M) # remove spaces after commas in rgba() - - with open(output_file, "w") as out: - out.write(css) - - -# Build System - -def build_tracker_html(): - output_file_raw = "bld/track.js" - output_file_html = "bld/track.html" - - output_file_tmp = "bld/track.tmp.js" - input_pattern = "src/tracker/*.js" - - with open(output_file_raw, "w") as out: - if not USE_UGLIFYJS: - out.write("(function(){\n") - - combine_files(input_pattern, out) - - if not USE_UGLIFYJS: - out.write("})()") - - if USE_UGLIFYJS: - os.system(EXEC_UGLIFYJS.format(output_file_raw, output_file_tmp, WORKING_DIR, RESERVED_PROPS)) - - with open(output_file_raw, "w") as out: - out.write("javascript:(function(){") - - with open(output_file_tmp, "r") as minified: - out.write(minified.read().replace("\n", " ").replace("\r", "")) - - out.write("})()") - - os.remove(output_file_tmp) - - with open(output_file_raw, "r") as raw: - script_contents = raw.read().replace("&", "&").replace('"', """).replace("'", "'").replace("<", "<").replace(">", ">") - - with open(output_file_html, "w") as out: - out.write(script_contents) - - -def build_tracker_userscript(): - output_file = "bld/track.user.js" - - input_pattern = "src/tracker/*.js" - userscript_base = "src/base/track.user.js" - - with open(userscript_base, "r") as base: - userscript_contents = base.read().replace("{{{version}}}", VERSION_SHORT).split("{{{contents}}}") - - with open(output_file, "w") as out: - out.write(userscript_contents[0]) - combine_files(input_pattern, out) - out.write(userscript_contents[1]) - - -def build_viewer(): - output_file = "bld/viewer.html" - input_html = "src/viewer/index.html" - - input_css_pattern = "src/viewer/styles/*.css" - tmp_css_file_combined = "bld/viewer.tmp.css" - tmp_css_file_minified = "bld/viewer.min.css" - - with open(tmp_css_file_combined, "w") as out: - combine_files(input_css_pattern, out) - - minify_css(tmp_css_file_combined, tmp_css_file_minified) - os.remove(tmp_css_file_combined) - - input_js_pattern = "src/viewer/scripts/*.js" - tmp_js_file_combined = "bld/viewer.tmp.js" - tmp_js_file_minified = "bld/viewer.min.js" - - with open(tmp_js_file_combined, "w") as out: - combine_files(input_js_pattern, out) - - if USE_UGLIFYJS: - os.system(EXEC_UGLIFYJS.format(tmp_js_file_combined, tmp_js_file_minified, WORKING_DIR, RESERVED_PROPS)) - else: - shutil.copyfile(tmp_js_file_combined, tmp_js_file_minified) - - os.remove(tmp_js_file_combined) - - tokens = { - "/*{js}*/": tmp_js_file_minified, - "/*{css}*/": tmp_css_file_minified - } - - with open(output_file, "w") as out: - with open(input_html, "r") as fin: - for line in fin: - token = None - - for token in (token for token in tokens if token in line): - with open(tokens[token], "r") as token_file: - embedded = token_file.read() - - out.write(embedded) - os.remove(tokens[token]) - - if token is None: - out.write(line) - - -def build_website(): - tracker_file_html = "bld/track.html" - tracker_file_userscript = "bld/track.user.js" - viewer_file = "bld/viewer.html" - web_style_file = "bld/web/style.css" - - distutils.dir_util.copy_tree("web", "bld/web") - index_file = "bld/web/index.php" - - with open(index_file, "r") as index: - index_contents = index.read() - - with open(index_file, "w") as index: - index.write(index_contents.replace("{{{version:web}}}", VERSION_SHORT.replace(" ", " "))) - - shutil.copyfile(tracker_file_html, "bld/web/build/track.html") - shutil.copyfile(tracker_file_userscript, "bld/web/build/track.user.js") - shutil.copyfile(viewer_file, "bld/web/build/viewer.html") - minify_css(web_style_file, web_style_file) - - -# Build Process - -os.makedirs("bld", exist_ok = True) - -print("Building tracker html...") -build_tracker_html() - -print("Building tracker userscript...") -build_tracker_userscript() - -print("Building viewer...") -build_viewer() - -if BUILD_WEBSITE: - print("Building website...") - build_website() - -if CLIPBOARD_TRACKER: - if os.name == "nt": - print("Copying to clipboard...") - os.system("clip < bld/track.js") - else: - print("Clipboard is only supported on Windows") - -print("Done") diff --git a/reserve.txt b/reserve.txt deleted file mode 100644 index f82f8e69..00000000 --- a/reserve.txt +++ /dev/null @@ -1,73 +0,0 @@ -autoscroll -_autoscroll -afterFirstMsg -_afterFirstMsg -afterSavedMsg -_afterSavedMsg -enableImagePreviews -_enableImagePreviews -enableFormatting -_enableFormatting -enableAnimatedEmoji -_enableAnimatedEmoji -enableUserAvatars -_enableUserAvatars -DHT_LOADED -DHT_EMBEDDED -meta -data -users -userindex -servers -channels -u -t -m -f -e -a -t -te -d -r -re -c -n -an -tag -avatar -author -type -state -name -position -topic -nsfw -id -username -bot -discriminator -timestamp -content -editedTimestamp -mentions -embeds -attachments -title -description -reply -reactions -emoji -count -animated -ext -toDate -memoizedProps -props -children -channel -messages -msSaveBlob -messageReference -message_id -guild_id diff --git a/src/base/track.user.js b/src/base/track.user.js deleted file mode 100644 index 9c091c5c..00000000 --- a/src/base/track.user.js +++ /dev/null @@ -1,50 +0,0 @@ -// ==UserScript== -// @name Discord History Tracker -// @version {{{version}}} -// @license MIT -// @namespace https://chylex.com -// @homepageURL https://dht.chylex.com/ -// @supportURL https://github.com/chylex/Discord-History-Tracker/issues -// @include https://discord.com/* -// @run-at document-idle -// @grant none -// ==/UserScript== - -const start = function(){ - -{{{contents}}} - -}; - -const css = document.createElement("style"); - -css.innerText = ` -#dht-userscript-trigger { cursor: pointer; margin-top: 5px } -#dht-userscript-trigger svg { opacity: 0.6 } -#dht-userscript-trigger:hover svg { opacity: 1 } -`; - -document.head.appendChild(css); - -window.setInterval(function(){ - if (document.getElementById("dht-userscript-trigger")){ - return; - } - - const help = document.querySelector("section[class^='title'] a[href*='support.discord.com']"); - - if (help){ - help.insertAdjacentHTML("afterend", ` - - - - - - - - -`); - - document.getElementById("dht-userscript-trigger").addEventListener("click", start); - } -}, 200); diff --git a/src/tracker/discord.js b/src/tracker/discord.js deleted file mode 100644 index ed4ff315..00000000 --- a/src/tracker/discord.js +++ /dev/null @@ -1,312 +0,0 @@ -var DISCORD = (function(){ - var getMessageOuterElement = function(){ - return DOM.queryReactClass("messagesWrapper"); - }; - - var getMessageScrollerElement = function(){ - return getMessageOuterElement().querySelector("[class*='scroller-']"); - }; - - var getMessageElements = function() { - return getMessageOuterElement().querySelectorAll("[class*='message-']"); - }; - - var getReactProps = function(ele) { - var keys = Object.keys(ele || {}); - var key = keys.find(key => key.startsWith("__reactInternalInstance")); - - if (key){ - return ele[key].memoizedProps; - } - - key = keys.find(key => key.startsWith("__reactProps$")); - return key ? ele[key] : null; - }; - - var getMessageElementProps = function(ele) { - const props = getReactProps(ele); - - if (props.children && props.children.length >= 4) { - const childProps = props.children[3].props; - - if ("message" in childProps && "channel" in childProps) { - return childProps; - } - } - - return null; - }; - - var hasMoreMessages = function() { - return document.querySelector("#messagesNavigationDescription + [class^=container]") === null; - }; - - var getMessages = function() { - try { - const messages = []; - - for (const ele of getMessageElements()) { - const props = getMessageElementProps(ele); - - if (props != null) { - messages.push(props.message); - } - } - - return messages; - } catch (e) { - console.error(e); - return []; - } - }; - - return { - /** - * Calls the provided function with a list of messages whenever the currently loaded messages change, - * or with `false` if there are no more messages. - */ - setupMessageCallback: function(callback) { - let skipsLeft = 0; - let waitForCleanup = false; - let hasReachedStart = false; - const previousMessages = new Set(); - - const intervalId = window.setInterval(() => { - if (skipsLeft > 0) { - --skipsLeft; - return; - } - - const view = getMessageOuterElement(); - - if (!view) { - skipsLeft = 2; - return; - } - - const anyMessage = DOM.queryReactClass("message", getMessageOuterElement()); - const messageCount = anyMessage ? anyMessage.parentElement.children.length : 0; - - if (messageCount > 300) { - if (waitForCleanup) { - return; - } - - skipsLeft = 3; - waitForCleanup = true; - - window.setTimeout(() => { - const view = getMessageScrollerElement(); - view.scrollTop = view.scrollHeight / 2; - }, 1); - } - else { - waitForCleanup = false; - } - - const messages = getMessages(); - let hasChanged = false; - - for (const message of messages) { - if (!previousMessages.has(message.id)) { - hasChanged = true; - break; - } - } - - if (!hasChanged) { - if (!hasReachedStart && !hasMoreMessages()) { - hasReachedStart = true; - callback(false); - } - - return; - } - - previousMessages.clear(); - for (const message of messages) { - previousMessages.add(message.id); - } - - hasReachedStart = false; - callback(messages); - }, 200); - - window.DHT_ON_UNLOAD.push(() => window.clearInterval(intervalId)); - }, - - /* - * Returns internal React state object of an element. - */ - getReactProps: function(ele){ - return getReactProps(ele); - }, - - /* - * Returns an object containing the selected server name, selected channel name and ID, and the object type. - * For types DM and GROUP, the server and channel names are identical. - * For SERVER type, the channel has to be in view, otherwise Discord unloads it. - */ - getSelectedChannel: function() { - try { - let obj; - - for (const ele of getMessageElements()) { - const props = getMessageElementProps(ele); - - if (props != null) { - obj = props.channel; - break; - } - } - - if (!obj) { - return null; - } - - var dms = DOM.queryReactClass("privateChannels"); - - if (dms){ - let name; - - for (const ele of dms.querySelectorAll("[class*='channel-'] [class*='selected-'] [class^='name-'] *, [class*='channel-'][class*='selected-'] [class^='name-'] *")) { - const node = Array.prototype.find.call(ele.childNodes, node => node.nodeType === Node.TEXT_NODE); - - if (node) { - name = node.nodeValue; - break; - } - } - - if (!name) { - return null; - } - - let type; - - // https://discord.com/developers/docs/resources/channel#channel-object-channel-types - switch (obj.type) { - case 1: type = "DM"; break; - case 3: type = "GROUP"; break; - default: return null; - } - - return { - "server": name, - "channel": name, - "id": obj.id, - "type": type, - "extra": {} - }; - } - else if (obj.guild_id) { - return { - "server": document.querySelector("nav header > h1").innerText, - "channel": obj.name, - "id": obj.id, - "type": "SERVER", - "extra": { - "position": obj.position, - "topic": obj.topic, - "nsfw": obj.nsfw - } - }; - } - else { - return null; - } - } catch(e) { - console.error(e); - return null; - } - }, - - /* - * Returns an array containing currently loaded messages. - */ - getMessages: function(){ - return getMessages(); - }, - - /* - * Returns true if the message view is visible. - */ - isInMessageView: () => !!getMessageOuterElement(), - - /* - * Returns true if there are more messages available or if they're still loading. - */ - hasMoreMessages: function(){ - return hasMoreMessages(); - }, - - /* - * Forces the message view to load older messages by scrolling all the way up. - */ - loadOlderMessages: function(){ - let view = getMessageScrollerElement(); - - if (view.scrollTop > 0){ - view.scrollTop = 0; - } - }, - - /* - * Selects the next text channel and returns true, otherwise returns false if there are no more channels. - */ - selectNextTextChannel: function(){ - var dms = DOM.queryReactClass("privateChannels"); - - if (dms){ - var currentChannel = DOM.queryReactClass("selected", dms); - var nextChannel = currentChannel && currentChannel.nextElementSibling; - - if (!nextChannel || !nextChannel.getAttribute("class").includes("channel-") || !("href" in nextChannel) || !nextChannel.href.includes("/@me/")){ - return false; - } - else{ - nextChannel.click(); - nextChannel.scrollIntoView(true); - return true; - } - } - else{ - var channelIconNormal = "M5.88657 21C5.57547 21 5.3399 20.7189 5.39427 20.4126L6.00001 17H2.59511C2.28449 17 2.04905 16.7198 2.10259 16.4138L2.27759 15.4138C2.31946 15.1746 2.52722 15 2.77011 15H6.35001L7.41001 9H4.00511C3.69449 9 3.45905 8.71977 3.51259 8.41381L3.68759 7.41381C3.72946 7.17456 3.93722 7 4.18011 7H7.76001L8.39677 3.41262C8.43914 3.17391 8.64664 3 8.88907 3H9.87344C10.1845 3 10.4201 3.28107 10.3657 3.58738L9.76001 7H15.76L16.3968 3.41262C16.4391 3.17391 16.6466 3 16.8891 3H17.8734C18.1845 3 18.4201 3.28107 18.3657 3.58738L17.76 7H21.1649C21.4755 7 21.711 7.28023 21.6574 7.58619L21.4824 8.58619C21.4406 8.82544 21.2328 9 20.9899 9H17.41L16.35 15H19.7549C20.0655 15 20.301 15.2802 20.2474 15.5862L20.0724 16.5862C20.0306 16.8254 19.8228 17 19.5799 17H16L15.3632 20.5874C15.3209 20.8261 15.1134 21 14.8709 21H13.8866C13.5755 21 13.3399 20.7189 13.3943 20.4126L14 17H8.00001L7.36325 20.5874C7.32088 20.8261 7.11337 21 6.87094 21H5.88657ZM9.41045 9L8.35045 15H14.3504L15.4104 9H9.41045Z"; - var channelIconSpecial = "M14 8C14 7.44772 13.5523 7 13 7H9.76001L10.3657 3.58738C10.4201 3.28107 10.1845 3 9.87344 3H8.88907C8.64664 3 8.43914 3.17391 8.39677 3.41262L7.76001 7H4.18011C3.93722 7 3.72946 7.17456 3.68759 7.41381L3.51259 8.41381C3.45905 8.71977 3.69449 9 4.00511 9H7.41001L6.35001 15H2.77011C2.52722 15 2.31946 15.1746 2.27759 15.4138L2.10259 16.4138C2.04905 16.7198 2.28449 17 2.59511 17H6.00001L5.39427 20.4126C5.3399 20.7189 5.57547 21 5.88657 21H6.87094C7.11337 21 7.32088 20.8261 7.36325 20.5874L8.00001 17H14L13.3943 20.4126C13.3399 20.7189 13.5755 21 13.8866 21H14.8709C15.1134 21 15.3209 20.8261 15.3632 20.5874L16 17H19.5799C19.8228 17 20.0306 16.8254 20.0724 16.5862L20.2474 15.5862C20.301 15.2802 20.0655 15 19.7549 15H16.35L16.6758 13.1558C16.7823 12.5529 16.3186 12 15.7063 12C15.2286 12 14.8199 12.3429 14.7368 12.8133L14.3504 15H8.35045L9.41045 9H13C13.5523 9 14 8.55228 14 8Z"; - - var isValidChannelClass = cls => cls.includes("wrapper-") && !cls.includes("clickable-"); - var isValidChannelType = ele => !!ele.querySelector('path[d="' + channelIconNormal + '"]') || !!ele.querySelector('path[d="' + channelIconSpecial + '"]'); - var isValidChannel = ele => ele.childElementCount > 0 && isValidChannelClass(ele.children[0].className) && isValidChannelType(ele); - - var channelListEle = document.querySelector("div[class*='sidebar'] > nav[class*='container'] > div[class*='scroller']"); - - if (!channelListEle){ - return false; - } - - var allChannels = Array.prototype.filter.call(channelListEle.querySelectorAll("[class*='containerDefault']"), isValidChannel); - var nextChannel = null; - - for(var index = 0; index < allChannels.length-1; index++){ - if (allChannels[index].children[0].className.includes("modeSelected")){ - nextChannel = allChannels[index+1]; - break; - } - } - - if (nextChannel === null){ - return false; - } - - const nextChannelLink = nextChannel.querySelector("a[href^='/channels/']"); - if (!nextChannelLink) { - return false; - } - - nextChannelLink.click(); - nextChannel.scrollIntoView(true); - return true; - } - } - }; -})(); diff --git a/src/tracker/dom.js b/src/tracker/dom.js deleted file mode 100644 index d355c55a..00000000 --- a/src/tracker/dom.js +++ /dev/null @@ -1,85 +0,0 @@ -var DOM = (function(){ - var createElement = (tag, parent, id, html) => { - var ele = document.createElement(tag); - ele.id = id || ""; - ele.innerHTML = html || ""; - parent.appendChild(ele); - return ele; - }; - - return { - /* - * Returns a child element by its ID. Parent defaults to the entire document. - */ - id: (id, parent) => (parent || document).getElementById(id), - - /* - * Returns the first child element containing the specified obfuscated class. Parent defaults to the entire document. - */ - queryReactClass: (cls, parent) => (parent || document).querySelector(`[class*="${cls}-"]`), - - /* - * Creates an element, adds it to the DOM, and returns it. - */ - createElement: (tag, parent, id, html) => createElement(tag, parent, id, html), - - /* - * Removes an element from the DOM. - */ - removeElement: (ele) => ele.parentNode.removeChild(ele), - - /* - * Creates a new style element with the specified CSS and returns it. - */ - createStyle: (styles) => createElement("style", document.head, "", styles), - - /* - * Convenience setTimeout function to save space after minification. - */ - setTimer: (callback, timeout) => window.setTimeout(callback, timeout), - - /* - * Convenience addEventListener function to save space after minification. - */ - listen: (ele, event, callback) => ele.addEventListener(event, callback), - - /* - * Utility function to save an object into a cookie. - */ - saveToCookie: (name, obj, expiresInSeconds) => { - var expires = new Date(Date.now()+1000*expiresInSeconds).toUTCString(); - document.cookie = name+"="+encodeURIComponent(JSON.stringify(obj))+";path=/;expires="+expires; - }, - - /* - * Utility function to load an object from a cookie. - */ - loadFromCookie: (name) => { - var value = document.cookie.replace(new RegExp("(?:(?:^|.*;\\s*)"+name+"\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1"); - return value.length ? JSON.parse(decodeURIComponent(value)) : null; - }, - - /* - * Triggers a UTF-8 text file download. - */ - downloadTextFile: (fileName, fileContents) => { - var blob = new Blob([fileContents], { "type": "octet/stream" }); - - if ("msSaveBlob" in window.navigator){ - return window.navigator.msSaveBlob(blob, fileName); - } - - var url = window.URL.createObjectURL(blob); - - var ele = createElement("a", document.body); - ele.href = url; - ele.download = fileName; - ele.style.display = "none"; - - ele.click(); - - document.body.removeChild(ele); - window.URL.revokeObjectURL(url); - } - }; -})(); diff --git a/src/tracker/gui.js b/src/tracker/gui.js deleted file mode 100644 index a241a2e7..00000000 --- a/src/tracker/gui.js +++ /dev/null @@ -1,277 +0,0 @@ -var GUI = (function(){ - var controller; - var settings; - - var updateButtonState = () => { - if (STATE.isTracking()){ - controller.ui.btnUpload.disabled = true; - controller.ui.btnSettings.disabled = true; - controller.ui.btnReset.disabled = true; - } - else{ - controller.ui.btnUpload.disabled = false; - controller.ui.btnSettings.disabled = false; - controller.ui.btnDownload.disabled = controller.ui.btnReset.disabled = !STATE.hasSavedData(); - } - }; - - var stateChangedEvent = (type, detail) => { - if (controller){ - var force = type === "gui" && detail === "controller"; - - if (type === "data" || force){ - updateButtonState(); - } - - if (type === "tracking" || force){ - updateButtonState(); - controller.ui.btnToggleTracking.innerHTML = STATE.isTracking() ? "Pause Tracking" : "Start Tracking"; - } - - if (type === "data" || force){ - var messageCount = 0; - var channelCount = 0; - - if (STATE.hasSavedData()){ - messageCount = STATE.getSavefile().countMessages(); - channelCount = STATE.getSavefile().countChannels(); - } - - controller.ui.textStatus.innerHTML = [ - messageCount, " message", (messageCount === 1 ? "" : "s"), - " from ", - channelCount, " channel", (channelCount === 1 ? "" : "s") - ].join(""); - } - } - - if (settings){ - var force = type === "gui" && detail === "settings"; - - if (force){ - settings.ui.cbAutoscroll.checked = SETTINGS.autoscroll; - settings.ui.optsAfterFirstMsg[SETTINGS.afterFirstMsg].checked = true; - settings.ui.optsAfterSavedMsg[SETTINGS.afterSavedMsg].checked = true; - } - - if (type === "setting" || force){ - var autoscrollRev = !SETTINGS.autoscroll; - - // discord polyfills Object.values - Object.values(settings.ui.optsAfterFirstMsg).forEach(ele => ele.disabled = autoscrollRev); - Object.values(settings.ui.optsAfterSavedMsg).forEach(ele => ele.disabled = autoscrollRev); - } - } - }; - - var registeredEvent = false; - - var setupStateChanged = function(detail){ - if (!registeredEvent){ - STATE.onStateChanged(stateChangedEvent); - SETTINGS.onSettingsChanged(stateChangedEvent); - registeredEvent = true; - } - - stateChangedEvent("gui", detail); - }; - - var root = { - showController: function(){ - controller = {}; - - // styles - - controller.styles = DOM.createStyle(` -#app-mount > div[class*="app-"] { margin-bottom: 48px !important; } -#dht-ctrl { position: absolute; bottom: 0; width: 100%; height: 48px; background-color: #FFF; } -#dht-ctrl button { height: 32px; margin: 8px 0 8px 8px; font-size: 16px; padding: 0 12px; background-color: #7289DA; color: #FFF; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.75); } -#dht-ctrl button:disabled { background-color: #7A7A7A; cursor: default; } -#dht-ctrl-close { margin: 8px 8px 8px 0 !important; float: right; } -#dht-ctrl p { display: inline-block; margin: 14px 12px; } -#dht-ctrl input { display: none; }`); - - // main - - var btn = (id, title) => ""; - - controller.ele = DOM.createElement("div", document.body, "dht-ctrl", ` -${btn("upload", "Upload & Combine")} -${btn("settings", "Settings")} -${btn("track", "")} -${btn("download", "Download")} -${btn("reset", "Reset")} -

- -${btn("close", "X")}`); - - // elements - - controller.ui = { - btnUpload: DOM.id("dht-ctrl-upload"), - btnSettings: DOM.id("dht-ctrl-settings"), - btnToggleTracking: DOM.id("dht-ctrl-track"), - btnDownload: DOM.id("dht-ctrl-download"), - btnReset: DOM.id("dht-ctrl-reset"), - btnClose: DOM.id("dht-ctrl-close"), - textStatus: DOM.id("dht-ctrl-status"), - inputUpload: DOM.id("dht-ctrl-upload-input") - }; - - // events - - DOM.listen(controller.ui.btnUpload, "click", () => { - controller.ui.inputUpload.click(); - }); - - DOM.listen(controller.ui.btnSettings, "click", () => { - root.showSettings(); - }); - - DOM.listen(controller.ui.btnToggleTracking, "click", () => { - STATE.setIsTracking(!STATE.isTracking()); - }); - - DOM.listen(controller.ui.btnDownload, "click", () => { - STATE.downloadSavefile(); - }); - - DOM.listen(controller.ui.btnReset, "click", () => { - STATE.resetState(); - }); - - DOM.listen(controller.ui.btnClose, "click", () => { - root.hideController(); - window.DHT_ON_UNLOAD.forEach(f => f()); - window.DHT_LOADED = false; - }); - - DOM.listen(controller.ui.inputUpload, "change", () => { - Array.prototype.forEach.call(controller.ui.inputUpload.files, file => { - var reader = new FileReader(); - - reader.onload = function(){ - var obj = {}; - - try{ - obj = JSON.parse(reader.result); - }catch(e){ - alert("Could not parse '"+file.name+"', see console for details."); - console.error(e); - return; - } - - if (SAVEFILE.isValid(obj)){ - STATE.uploadSavefile(file.name, new SAVEFILE(obj)); - } - else{ - alert("File '"+file.name+"' has an invalid format."); - } - }; - - reader.readAsText(file, "UTF-8"); - }); - - controller.ui.inputUpload.value = null; - }); - - setupStateChanged("controller"); - }, - - hideController: function(){ - if (controller){ - DOM.removeElement(controller.ele); - DOM.removeElement(controller.styles); - controller = null; - } - }, - - showSettings: function(){ - settings = {}; - - // styles - - settings.styles = DOM.createStyle(` -#dht-cfg-overlay { position: absolute; left: 0; top: 0; width: 100%; height: 100%; background-color: #000; opacity: 0.5; display: block; z-index: 1000; } -#dht-cfg { position: absolute; left: 50%; top: 50%; width: 800px; height: 262px; margin-left: -400px; margin-top: -131px; padding: 8px; background-color: #fff; z-index: 1001; } -#dht-cfg-note { margin-top: 22px; } -#dht-cfg sub { color: #666; font-size: 13px; }`); - - // overlay - - settings.overlay = DOM.createElement("div", document.body, "dht-cfg-overlay"); - - DOM.listen(settings.overlay, "click", () => { - root.hideSettings(); - }); - - // main - - var radio = (type, id, label) => "
"; - - settings.ele = DOM.createElement("div", document.body, "dht-cfg", ` -
-
-
-${radio("afm", "nothing", "Do Nothing")} -${radio("afm", "pause", "Pause Tracking")} -${radio("afm", "switch", "Switch to Next Channel")} -
-
-${radio("asm", "nothing", "Do Nothing")} -${radio("asm", "pause", "Pause Tracking")} -${radio("asm", "switch", "Switch to Next Channel")} -

-It is recommended to disable link and image previews to avoid putting unnecessary strain on your browser.

-{{{version:full}}} -

`); - - // elements - - settings.ui = { - cbAutoscroll: DOM.id("dht-cfg-autoscroll"), - optsAfterFirstMsg: {}, - optsAfterSavedMsg: {} - }; - - settings.ui.optsAfterFirstMsg[CONSTANTS.AUTOSCROLL_ACTION_NOTHING] = DOM.id("dht-cfg-afm-nothing"); - settings.ui.optsAfterFirstMsg[CONSTANTS.AUTOSCROLL_ACTION_PAUSE] = DOM.id("dht-cfg-afm-pause"); - settings.ui.optsAfterFirstMsg[CONSTANTS.AUTOSCROLL_ACTION_SWITCH] = DOM.id("dht-cfg-afm-switch"); - - settings.ui.optsAfterSavedMsg[CONSTANTS.AUTOSCROLL_ACTION_NOTHING] = DOM.id("dht-cfg-asm-nothing"); - settings.ui.optsAfterSavedMsg[CONSTANTS.AUTOSCROLL_ACTION_PAUSE] = DOM.id("dht-cfg-asm-pause"); - settings.ui.optsAfterSavedMsg[CONSTANTS.AUTOSCROLL_ACTION_SWITCH] = DOM.id("dht-cfg-asm-switch"); - - // events - - settings.ui.cbAutoscroll.addEventListener("change", () => { - SETTINGS.autoscroll = settings.ui.cbAutoscroll.checked; - }); - - Object.keys(settings.ui.optsAfterFirstMsg).forEach(key => { - DOM.listen(settings.ui.optsAfterFirstMsg[key], "click", () => { - SETTINGS.afterFirstMsg = key; - }); - }); - - Object.keys(settings.ui.optsAfterSavedMsg).forEach(key => { - DOM.listen(settings.ui.optsAfterSavedMsg[key], "click", () => { - SETTINGS.afterSavedMsg = key; - }); - }); - - setupStateChanged("settings"); - }, - - hideSettings: function(){ - if (settings){ - DOM.removeElement(settings.overlay); - DOM.removeElement(settings.ele); - DOM.removeElement(settings.styles); - settings = null; - } - } - }; - - return root; -})(); diff --git a/src/tracker/savefile.js b/src/tracker/savefile.js deleted file mode 100644 index b5936b64..00000000 --- a/src/tracker/savefile.js +++ /dev/null @@ -1,349 +0,0 @@ -/* - * SAVEFILE STRUCTURE - * ================== - * - * { - * meta: { - * users: { - * : { - * name: , - * avatar: , - * tag: // only present if not a bot - * }, ... - * }, - * - * // the user index is an array of discord user ids, - * // these indexes are used in the message objects to save space - * userindex: [ - * , ... - * ], - * - * servers: [ - * { - * name: , - * type: <"SERVER"|"GROUP"|DM"> - * }, ... - * ], - * - * channels: { - * : { - * server: , - * name: , - * position: , // only present if server type == SERVER - * topic: , // only present if server type == SERVER - * nsfw: // only present if server type == SERVER - * }, ... - * } - * }, - * - * data: { - * : { - * : { - * u: , - * t: , - * m: , // only present if not empty - * f: , // only present if edited in which case it equals 1, deprecated (use 'te' instead) - * te: , // only present if edited - * e: [ // omit for no embeds - * { - * url: , - * type: , - * t: , // only present if type == rich, and if not empty - * d: // only present if type == rich, and if the embed has a simple description text - * }, ... - * ], - * a: [ // omit for no attachments - * { - * url: - * }, ... - * ], - * r: , // only present if referencing another message (reply) - * re: [ // omit for no reactions - * { - * c: - * n: , - * id: , // only present for custom emoji - * an: , // only present for custom animated emoji - * }, ... - * ] - * }, ... - * }, ... - * } - * } - * - * - * TEMPORARY OBJECT STRUCTURE - * ========================== - * - * { - * userlookup: { - * : - * }, - * channelkeys: Set, - * messagekeys: Set, - * freshmsgs: Set // only messages which were newly added to the savefile in the current session - * } - */ - -class SAVEFILE{ - constructor(parsedObj){ - var me = this; - - if (!SAVEFILE.isValid(parsedObj)){ - parsedObj = { - meta: {}, - data: {} - }; - } - - me.meta = parsedObj.meta; - me.data = parsedObj.data; - - me.meta.users = me.meta.users || {}; - me.meta.userindex = me.meta.userindex || []; - me.meta.servers = me.meta.servers || []; - me.meta.channels = me.meta.channels || {}; - - me.tmp = { - userlookup: {}, - channelkeys: new Set(), - messagekeys: new Set(), - freshmsgs: new Set() - }; - } - - static isValid(parsedObj){ - return parsedObj && typeof parsedObj.meta === "object" && typeof parsedObj.data === "object"; - } - - findOrRegisterUser(userId, userName, userDiscriminator, userAvatar){ - var wasPresent = userId in this.meta.users; - var userObj = wasPresent ? this.meta.users[userId] : {}; - - userObj.name = userName; - - if (userDiscriminator){ - userObj.tag = userDiscriminator; - } - - if (userAvatar){ - userObj.avatar = userAvatar; - } - - if (!wasPresent){ - this.meta.users[userId] = userObj; - this.meta.userindex.push(userId); - return this.tmp.userlookup[userId] = this.meta.userindex.length-1; - } - else if (!(userId in this.tmp.userlookup)){ - return this.tmp.userlookup[userId] = this.meta.userindex.findIndex(id => id == userId); - } - else{ - return this.tmp.userlookup[userId]; - } - } - - findOrRegisterServer(serverName, serverType){ - var index = this.meta.servers.findIndex(server => server.name === serverName && server.type === serverType); - - if (index === -1){ - this.meta.servers.push({ - "name": serverName, - "type": serverType - }); - - return this.meta.servers.length-1; - } - else{ - return index; - } - } - - tryRegisterChannel(serverIndex, channelId, channelName, extraInfo){ - if (!this.meta.servers[serverIndex]){ - return undefined; - } - - var wasPresent = channelId in this.meta.channels; - var channelObj = wasPresent ? this.meta.channels[channelId] : { "server": serverIndex }; - - channelObj.name = channelName; - - if (extraInfo.position){ - channelObj.position = extraInfo.position; - } - - if (extraInfo.topic){ - channelObj.topic = extraInfo.topic; - } - - if (extraInfo.nsfw){ - channelObj.nsfw = extraInfo.nsfw; - } - - if (wasPresent){ - return false; - } - else{ - this.meta.channels[channelId] = channelObj; - this.tmp.channelkeys.add(channelId); - return true; - } - } - - addMessage(channelId, messageId, messageObject){ - var container = this.data[channelId] || (this.data[channelId] = {}); - var wasPresent = messageId in container; - - container[messageId] = messageObject; - this.tmp.messagekeys.add(messageId); - return !wasPresent; - } - - convertToMessageObject(discordMessage){ - var author = discordMessage.author; - - var obj = { - u: this.findOrRegisterUser(author.id, author.username, author.bot ? null : author.discriminator, author.avatar), - t: discordMessage.timestamp.toDate().getTime() - }; - - if (discordMessage.content.length > 0){ - obj.m = discordMessage.content; - } - - if (discordMessage.editedTimestamp !== null){ - obj.te = discordMessage.editedTimestamp.toDate().getTime(); - } - - if (discordMessage.embeds.length > 0){ - obj.e = discordMessage.embeds.map(embed => { - let conv = { - url: embed.url, - type: embed.type - }; - - if (embed.type === "rich"){ - if (Array.isArray(embed.title) && embed.title.length === 1 && typeof embed.title[0] === "string"){ - conv.t = embed.title[0]; - - if (Array.isArray(embed.description) && embed.description.length === 1 && typeof embed.description[0] === "string"){ - conv.d = embed.description[0]; - } - } - } - - return conv; - }); - } - - if (discordMessage.attachments.length > 0){ - obj.a = discordMessage.attachments.map(attachment => ({ - url: attachment.url - })); - } - - if (discordMessage.messageReference !== null){ - obj.r = discordMessage.messageReference.message_id; - } - - if (discordMessage.reactions.length > 0) { - obj.re = discordMessage.reactions.map(reaction => { - let conv = { - c: reaction.count, - n: reaction.emoji.name - }; - - if (reaction.emoji.id !== null) { - conv.id = reaction.emoji.id; - } - - if (reaction.emoji.animated) { - conv.an = true; - } - - return conv; - }); - } - - return obj; - } - - isMessageFresh(id){ - return this.tmp.freshmsgs.has(id); - } - - addMessagesFromDiscord(channelId, discordMessageArray){ - var hasNewMessages = false; - - for(var discordMessage of discordMessageArray){ - var type = discordMessage.type; - - // https://discord.com/developers/docs/resources/channel#message-object-message-reference-structure - if ((type === 0 || type === 19) && discordMessage.state === "SENT" && this.addMessage(channelId, discordMessage.id, this.convertToMessageObject(discordMessage))){ - this.tmp.freshmsgs.add(discordMessage.id); - hasNewMessages = true; - } - } - - return hasNewMessages; - } - - countChannels(){ - return this.tmp.channelkeys.size; - } - - countMessages(){ - return this.tmp.messagekeys.size; - } - - combineWith(obj){ - var userMap = {}; - var shownError = false; - - for(var userId in obj.meta.users){ - var oldUser = obj.meta.users[userId]; - userMap[obj.meta.userindex.findIndex(id => id == userId)] = this.findOrRegisterUser(userId, oldUser.name, oldUser.tag, oldUser.avatar); - } - - for(var channelId in obj.meta.channels){ - var oldServer = obj.meta.servers[obj.meta.channels[channelId].server]; - var oldChannel = obj.meta.channels[channelId]; - this.tryRegisterChannel(this.findOrRegisterServer(oldServer.name, oldServer.type), channelId, oldChannel.name, oldChannel /* filtered later */); - } - - for(var channelId in obj.data){ - var oldChannel = obj.data[channelId]; - - for(var messageId in oldChannel){ - var oldMessage = oldChannel[messageId]; - var oldUser = oldMessage.u; - - if (oldUser in userMap){ - oldMessage.u = userMap[oldUser]; - this.addMessage(channelId, messageId, oldMessage); - } - else{ - if (!shownError){ - shownError = true; - alert("The uploaded archive appears to be corrupted, some messages will be skipped. See console for details."); - - console.error("User list:", obj.meta.users); - console.error("User index:", obj.meta.userindex); - console.error("Generated mapping:", userMap); - console.error("Missing user for the following messages:"); - } - - console.error(oldMessage); - } - } - } - } - - toJson(){ - return JSON.stringify({ - "meta": this.meta, - "data": this.data - }); - } -} diff --git a/src/tracker/settings.js b/src/tracker/settings.js deleted file mode 100644 index 8326cf9c..00000000 --- a/src/tracker/settings.js +++ /dev/null @@ -1,64 +0,0 @@ -var CONSTANTS = { - AUTOSCROLL_ACTION_NOTHING: "optNothing", - AUTOSCROLL_ACTION_PAUSE: "optPause", - AUTOSCROLL_ACTION_SWITCH: "optSwitch" -}; - -var IS_FIRST_RUN = false; - -var SETTINGS = (function(){ - var root = {}; - var settingsChangedEvents = []; - - var saveSettings = function(){ - DOM.saveToCookie("DHT_SETTINGS", root, 60*60*24*365*5); - }; - - var triggerSettingsChanged = function(changeType, changeDetail){ - for(var callback of settingsChangedEvents){ - callback(changeType, changeDetail); - } - - saveSettings(); - }; - - var defineTriggeringProperty = function(obj, property, value){ - var name = "_"+property; - - Object.defineProperty(obj, property, { - get: (() => obj[name]), - set: (value => { - obj[name] = value; - triggerSettingsChanged("setting", property); - }) - }); - - obj[name] = value; - }; - - var loaded = DOM.loadFromCookie("DHT_SETTINGS"); - - if (!loaded){ - loaded = { - "_autoscroll": true, - "_afterFirstMsg": CONSTANTS.AUTOSCROLL_ACTION_PAUSE, - "_afterSavedMsg": CONSTANTS.AUTOSCROLL_ACTION_PAUSE - }; - - IS_FIRST_RUN = true; - } - - defineTriggeringProperty(root, "autoscroll", loaded._autoscroll); - defineTriggeringProperty(root, "afterFirstMsg", loaded._afterFirstMsg); - defineTriggeringProperty(root, "afterSavedMsg", loaded._afterSavedMsg); - - root.onSettingsChanged = function(callback){ - settingsChangedEvents.push(callback); - }; - - if (IS_FIRST_RUN){ - saveSettings(); - } - - return root; -})(); diff --git a/src/tracker/state.js b/src/tracker/state.js deleted file mode 100644 index 46cbca2e..00000000 --- a/src/tracker/state.js +++ /dev/null @@ -1,119 +0,0 @@ -var STATE = (function(){ - var stateChangedEvents = []; - - var triggerStateChanged = function(changeType, changeDetail){ - for(var callback of stateChangedEvents){ - callback(changeType, changeDetail); - } - }; - - /* - * Internal class constructor. - */ - class CLS{ - constructor(){ - this.resetState(); - }; - - /* - * Resets the state to default values. - */ - resetState(){ - this._savefile = null; - this._isTracking = false; - this._lastFileName = null; - triggerStateChanged("data", "reset"); - } - - /* - * Returns the savefile object, creates a new one if needed. - */ - getSavefile(){ - if (!this._savefile){ - this._savefile = new SAVEFILE(); - } - - return this._savefile; - } - - /* - * Returns true if the database file contains any data. - */ - hasSavedData(){ - return this._savefile != null; - } - - /* - * Returns true if currently tracking message. - */ - isTracking(){ - return this._isTracking; - } - - /* - * Sets the tracking state. - */ - setIsTracking(state){ - this._isTracking = state; - triggerStateChanged("tracking", state); - } - - /* - * Combines current savefile with the provided one. - */ - uploadSavefile(fileName, fileObject){ - this._lastFileName = fileName; - this.getSavefile().combineWith(fileObject); - triggerStateChanged("data", "upload"); - } - - /* - * Triggers a savefile download, if available. - */ - downloadSavefile(){ - if (this.hasSavedData()){ - DOM.downloadTextFile(this._lastFileName || "dht.txt", this._savefile.toJson()); - } - } - - /* - * Registers a Discord server and channel. - */ - addDiscordChannel(serverName, serverType, channelId, channelName, extraInfo){ - var serverIndex = this.getSavefile().findOrRegisterServer(serverName, serverType); - - if (this.getSavefile().tryRegisterChannel(serverIndex, channelId, channelName, extraInfo) === true){ - triggerStateChanged("data", "channel"); - } - } - - /* - * Adds all messages from the array to the specified channel. Returns true if the savefile was updated. - */ - addDiscordMessages(channelId, discordMessageArray){ - if (this.getSavefile().addMessagesFromDiscord(channelId, discordMessageArray)){ - triggerStateChanged("data", "messages"); - return true; - } - else{ - return false; - } - } - - /* - * Returns true if the message was added during this session. - */ - isMessageFresh(id){ - return this.getSavefile().isMessageFresh(id); - } - - /* - * Adds a listener that is called whenever the state changes. The callback is a function that takes subject (generic type) and detail (specific type or data). - */ - onStateChanged(callback){ - stateChangedEvents.push(callback); - } - } - - return new CLS(); -})(); diff --git "a/src/tracker/\316\243.js" "b/src/tracker/\316\243.js" deleted file mode 100644 index 6bec8dcc..00000000 --- "a/src/tracker/\316\243.js" +++ /dev/null @@ -1,136 +0,0 @@ -const url = window.location.href; - -if (!url.includes("discord.com/") && !url.includes("discordapp.com/") && !confirm("Could not detect Discord in the URL, do you want to run the script anyway?")){ - return; -} - -if (window.DHT_LOADED){ - alert("Discord History Tracker is already loaded."); - return; -} - -window.DHT_LOADED = true; -window.DHT_ON_UNLOAD = []; - -// Execution - -let ignoreMessageCallback = new Set(); -let frozenMessageLoadingTimer = null; - -let stopTrackingDelayed = function(callback){ - ignoreMessageCallback.add("stopping"); - - DOM.setTimer(() => { - STATE.setIsTracking(false); - ignoreMessageCallback.delete("stopping"); - - if (callback){ - callback(); - } - }, 200); // give the user visual feedback after clicking the button before switching off -}; - -DISCORD.setupMessageCallback(messages => { - if (STATE.isTracking() && ignoreMessageCallback.size === 0){ - let info = DISCORD.getSelectedChannel(); - - if (!info){ - stopTrackingDelayed(); - return; - } - - STATE.addDiscordChannel(info.server, info.type, info.id, info.channel, info.extra); - - if (messages !== false && !messages.length){ - DISCORD.loadOlderMessages(); - return; - } - - let hasUpdatedFile = messages !== false && STATE.addDiscordMessages(info.id, messages); - - if (SETTINGS.autoscroll){ - let action = null; - - if (messages === false) { - action = SETTINGS.afterFirstMsg; - } - else if (!hasUpdatedFile && !STATE.isMessageFresh(messages[0].id)){ - action = SETTINGS.afterSavedMsg; - } - - if (action === null){ - if (hasUpdatedFile){ - DISCORD.loadOlderMessages(); - window.clearTimeout(frozenMessageLoadingTimer); - frozenMessageLoadingTimer = null; - } - else{ - frozenMessageLoadingTimer = window.setTimeout(DISCORD.loadOlderMessages, 2500); - } - } - else{ - ignoreMessageCallback.add("stalling"); - - DOM.setTimer(() => { - ignoreMessageCallback.delete("stalling"); - - let updatedInfo = DISCORD.getSelectedChannel(); - - if (updatedInfo && updatedInfo.id === info.id){ - let lastMessages = DISCORD.getMessages(); // sometimes needed to catch the last few messages before switching - - if (lastMessages != null){ - STATE.addDiscordMessages(info.id, lastMessages); - } - } - - if ((action === CONSTANTS.AUTOSCROLL_ACTION_SWITCH && !DISCORD.selectNextTextChannel()) || action === CONSTANTS.AUTOSCROLL_ACTION_PAUSE){ - STATE.setIsTracking(false); - } - }, 250); - } - } - } -}); - -STATE.onStateChanged((type, enabled) => { - if (type === "tracking" && enabled){ - let info = DISCORD.getSelectedChannel(); - - if (info){ - let messages = DISCORD.getMessages(); - - if (messages != null){ - STATE.addDiscordChannel(info.server, info.type, info.id, info.channel, info.extra); - STATE.addDiscordMessages(info.id, messages); - } - else{ - stopTrackingDelayed(() => alert("Cannot see any messages.")); - return; - } - } - else{ - stopTrackingDelayed(() => alert("The selected channel is not visible in the channel list.")); - return; - } - - if (SETTINGS.autoscroll && DISCORD.isInMessageView()){ - if (DISCORD.hasMoreMessages()){ - DISCORD.loadOlderMessages(); - } - else{ - let action = SETTINGS.afterFirstMsg; - - if ((action === CONSTANTS.AUTOSCROLL_ACTION_SWITCH && !DISCORD.selectNextTextChannel()) || action === CONSTANTS.AUTOSCROLL_ACTION_PAUSE){ - stopTrackingDelayed(); - } - } - } - } -}); - -GUI.showController(); - -if (IS_FIRST_RUN){ - GUI.showSettings(); -} diff --git a/src/viewer/index.html b/src/viewer/index.html deleted file mode 100644 index 471a64bb..00000000 --- a/src/viewer/index.html +++ /dev/null @@ -1,85 +0,0 @@ - - - - - Discord Offline History - - - - - - - -
-
-
-
- - - - diff --git a/src/viewer/scripts/discord.js b/src/viewer/scripts/discord.js deleted file mode 100644 index 0234cf1a..00000000 --- a/src/viewer/scripts/discord.js +++ /dev/null @@ -1,246 +0,0 @@ -var DISCORD = (function(){ - var REGEX = { - formatBold: /\*\*([\s\S]+?)\*\*(?!\*)/g, - formatItalic: /(.)?\*([\s\S]+?)\*(?!\*)/g, - formatUnderline: /__([\s\S]+?)__(?!_)/g, - formatStrike: /~~([\s\S]+?)~~(?!~)/g, - formatCodeInline: /(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/g, - formatCodeBlock: /```(?:([A-z0-9\-]+?)\n+)?\n*([^]+?)\n*```/g, - formatUrl: /(\b(?:https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig, - formatUrlNoEmbed: /<(\b(?:https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])>/ig, - specialEscapedBacktick: /\\`/g, - specialEscapedSingle: /\\([*\\])/g, - specialEscapedDouble: /\\__|_\\_|\\_\\_|\\~~|~\\~|\\~\\~/g, - specialUnescaped: /([*_~\\])/g, - mentionRole: /<@&(\d+?)>/g, - mentionUser: /<@!?(\d+?)>/g, - mentionChannel: /<#(\d+?)>/g, - customEmojiStatic: /<:([^:]+):(\d+?)>/g, - customEmojiAnimated: /<a:([^:]+):(\d+?)>/g - }; - - var isImageAttachment = function(attachment){ - var dot = attachment.url.lastIndexOf("."); - var ext = dot === -1 ? "" : attachment.url.substring(dot).toLowerCase(); - return ext === ".png" || ext === ".gif" || ext === ".jpg" || ext === ".jpeg"; - }; - - var getHumanReadableTime = function(timestamp){ - var date = new Date(timestamp); - return date.toLocaleDateString() + ", " + date.toLocaleTimeString(); - }; - - var templateChannelServer; - var templateChannelPrivate; - var templateMessageNoAvatar; - var templateMessageWithAvatar; - var templateUserAvatar; - var templateEmbedImage; - var templateEmbedRich; - var templateEmbedRichNoDescription; - var templateEmbedRichUnsupported; - var templateEmbedDownload; - - var processMessageContents = function(contents){ - var processed = DOM.escapeHTML(contents.replace(REGEX.formatUrlNoEmbed, "$1")); - - if (STATE.settings.enableFormatting){ - var escapeHtmlMatch = (full, match) => "&#"+match.charCodeAt(0)+";"; - - processed = processed - .replace(REGEX.specialEscapedBacktick, "`") - .replace(REGEX.formatCodeBlock, (full, ignore, match) => ""+match.replace(REGEX.specialUnescaped, escapeHtmlMatch)+"") - .replace(REGEX.formatCodeInline, (full, ignore, match) => ""+match.replace(REGEX.specialUnescaped, escapeHtmlMatch)+"") - .replace(REGEX.specialEscapedSingle, escapeHtmlMatch) - .replace(REGEX.specialEscapedDouble, full => full.replace(/\\/g, "").replace(/(.)/g, escapeHtmlMatch)) - .replace(REGEX.formatBold, "$1") - .replace(REGEX.formatItalic, (full, pre, match) => pre === '\\' ? full : (pre || "")+""+match+"") - .replace(REGEX.formatUnderline, "$1") - .replace(REGEX.formatStrike, "$1"); - } - - var animatedEmojiExtension = STATE.settings.enableAnimatedEmoji ? "gif" : "png"; - - processed = processed - .replace(REGEX.formatUrl, "$1") - .replace(REGEX.mentionChannel, (full, match) => "#"+STATE.getChannelName(match)+"") - .replace(REGEX.mentionUser, (full, match) => "@"+STATE.getUserName(match)+"") - .replace(REGEX.customEmojiStatic, ":$1:") - .replace(REGEX.customEmojiAnimated, ":$1:"); - - return "

"+processed+"

"; - }; - - return { - setup: function(){ - templateChannelServer = new TEMPLATE([ - "
", - "
#{name}{nsfw}{msgcount}
", - "{server.name} ({server.type})", - "
" - ].join("")); - - templateChannelPrivate = new TEMPLATE([ - "
", - "
{name}{msgcount}
", - "({server.type})", - "
" - ].join("")); - - templateMessageNoAvatar = new TEMPLATE([ - "
", - "
{reply}
", - "

{user.name}{timestamp}{edit}{jump}

", - "
{contents}{embeds}{attachments}
", - "
{reactions}
", - "
" - ].join("")); - - templateMessageWithAvatar = new TEMPLATE([ - "
", - "
{reply}
", - "
", - "
{avatar}
", - "
", - "

{user.name}{timestamp}{edit}{jump}

", - "
{contents}{embeds}{attachments}
", - "
{reactions}
", - "
", - "
", - "
" - ].join("")); - - templateUserAvatar = new TEMPLATE([ - "" - ].join("")); - - templateEmbedImage = new TEMPLATE([ - "(image attachment not found)
" - ].join("")); - - templateEmbedRich = new TEMPLATE([ - "
{t}

{d}

" - ].join("")); - - templateEmbedRichNoDescription = new TEMPLATE([ - "" - ].join("")); - - templateEmbedRichUnsupported = new TEMPLATE([ - "

(Formatted embeds are currently not supported)

" - ].join("")); - - templateEmbedDownload = new TEMPLATE([ - "Download {filename}" - ].join("")); - - templateReaction = new TEMPLATE([ - "{n}{c}" - ].join("")); - - templateReactionCustom = new TEMPLATE([ - ":{n}:{c}" - ].join("")); - }, - - isImageAttachment: isImageAttachment, - - getChannelHTML: function(channel){ - return (channel.server.type === "SERVER" ? templateChannelServer : templateChannelPrivate).apply(channel, (property, value) => { - if (property === "server.type"){ - switch(value){ - case "SERVER": return "server"; - case "GROUP": return "group"; - case "DM": return "user"; - } - } - else if (property === "nsfw"){ - return value ? "NSFW" : ""; - } - }); - }, - - getMessageHTML: function(message){ - return (STATE.settings.enableUserAvatars ? templateMessageWithAvatar : templateMessageNoAvatar).apply(message, (property, value) => { - if (property === "avatar"){ - return value ? templateUserAvatar.apply(value) : ""; - } - else if (property === "user.tag"){ - return value ? value : "????"; - } - else if (property === "timestamp"){ - return getHumanReadableTime(value); - } - else if (property === "contents"){ - return value == null || value.length === 0 ? "" : processMessageContents(value); - } - else if (property === "embeds"){ - if (!value){ - return ""; - } - - return value.map(embed => { - switch(embed.type){ - case "image": - return STATE.settings.enableImagePreviews ? templateEmbedImage.apply(embed) : ""; - - case "rich": - return (embed.t ? (embed.d ? templateEmbedRich : templateEmbedRichNoDescription) : templateEmbedRichUnsupported).apply(embed); - } - }).join(""); - } - else if (property === "attachments"){ - if (!value){ - return ""; - } - - return value.map(attachment => { - if (isImageAttachment(attachment) && STATE.settings.enableImagePreviews){ - return templateEmbedImage.apply(attachment); - } - else{ - var sliced = attachment.url.split("/"); - - return templateEmbedDownload.apply({ - "url": attachment.url, - "filename": sliced[sliced.length-1] - }); - } - }).join(""); - } - else if (property === "edit"){ - return value ? "Edited" + (value > 1 ? " " + getHumanReadableTime(value) : "") + "" : ""; - } - else if (property === "jump"){ - return STATE.hasActiveFilter ? "Jump to message" : ""; - } - else if (property === "reply"){ - if (value === null) { - return ""; - } - - var user = "" + value.user.name + ""; - var avatar = STATE.settings.enableUserAvatars && value.avatar ? "" + templateUserAvatar.apply(value.avatar) + "" : ""; - var contents = value.contents ? "" + processMessageContents(value.contents) + "" : ""; - - return "Jump to reply" + avatar + user + "" + contents; - } - else if (property === "reactions"){ - if (value === null){ - return ""; - } - - return value.map(reaction => { - if ("id" in reaction){ - reaction.ext = reaction.an && STATE.settings.enableAnimatedEmoji ? "gif" : "png"; - return templateReactionCustom.apply(reaction); - } - else { - return templateReaction.apply(reaction); - } - }).join(""); - } - }); - } - }; -})(); diff --git a/src/viewer/scripts/dom.js b/src/viewer/scripts/dom.js deleted file mode 100644 index b5ca92d0..00000000 --- a/src/viewer/scripts/dom.js +++ /dev/null @@ -1,77 +0,0 @@ -var DOM = (function(){ - var createElement = (tag, parent) => { - var ele = document.createElement(tag); - parent.appendChild(ele); - return ele; - }; - - var entityMap = { - "&": "&", - "<": "<", - ">": ">", - '"': '"', - "'": ''' - }; - - var entityRegex = /[&<>"']/g; - - return { - /* - * Returns a child element by its ID. Parent defaults to the entire document. - */ - id: (id, parent) => (parent || document).getElementById(id), - - /* - * Returns an array of all child elements containing the specified class. Parent defaults to the entire document. - */ - cls: (cls, parent) => Array.prototype.slice.call((parent || document).getElementsByClassName(cls)), - - /* - * Returns an array of all child elements that have the specified tag. Parent defaults to the entire document. - */ - tag: (tag, parent) => Array.prototype.slice.call((parent || document).getElementsByTagName(tag)), - - /* - * Returns the first child element containing the specified class. Parent defaults to the entire document. - */ - fcls: (cls, parent) => (parent || document).getElementsByClassName(cls)[0], - - /* - * Creates an element, adds it to the DOM, and returns it. - */ - createElement: (tag, parent) => createElement(tag, parent), - - /* - * Removes an element from the DOM. - */ - removeElement: (ele) => ele.parentNode.removeChild(ele), - - /* - * Converts characters to their HTML entity form. - */ - escapeHTML: (html) => String(html).replace(entityRegex, s => entityMap[s]), - - /* - * Triggers a UTF-8 text file download. - */ - downloadTextFile: (fileName, fileContents) => { - var blob = new Blob([fileContents], { "type": "octet/stream" }); - - if ("msSaveBlob" in window.navigator){ - return window.navigator.msSaveBlob(blob, fileName); - } - - var url = window.URL.createObjectURL(blob); - - var ele = createElement("a", document.body); - ele.href = url; - ele.download = fileName; - ele.style.display = "none"; - - ele.click(); - - document.body.removeChild(ele); - window.URL.revokeObjectURL(url); - } - }; -})(); diff --git a/src/viewer/scripts/embed.js b/src/viewer/scripts/embed.js deleted file mode 100644 index 88f02668..00000000 --- a/src/viewer/scripts/embed.js +++ /dev/null @@ -1,58 +0,0 @@ -var EMBED = (function(){ - var enabled = false; - - var html; - var generated; - - var downloadTextFile = function(fileName, fileContents){ - var blob = new Blob([fileContents], { "type": "octet/stream" }); - - if ("msSaveBlob" in window.navigator){ - return window.navigator.msSaveBlob(blob, fileName); - } - - var url = window.URL.createObjectURL(blob); - - var ele = DOM.createElement("a", document.body); - ele.href = url; - ele.download = fileName; - ele.style.display = "none"; - - ele.click(); - - document.body.removeChild(ele); - window.URL.revokeObjectURL(url); - }; - - var utoa = function(str){ - return window.btoa(unescape(encodeURIComponent(str))); - }; - - var atou = function(str){ - return decodeURIComponent(escape(window.atob(str))); - }; - - return { - setup: function(){ - enabled = true; - html = "\n" + document.documentElement.outerHTML; - - DOM.id("btn-upload-file").insertAdjacentHTML("afterend", ``); - DOM.id("btn-embed-file").addEventListener("click", () => downloadTextFile("embed.html", generated)); - }, - - onFileRead: function(json){ - if (!enabled){ - return; - } - - DOM.id("btn-embed-file").disabled = false; - generated = html.replace("", `\n diff --git a/web/style.css b/web/style.css index a57a10d4..08641bcb 100644 --- a/web/style.css +++ b/web/style.css @@ -132,14 +132,3 @@ code { overflow: hidden; text-overflow: ellipsis; } - -#tracker-copy-contents { - width: 100%; - height: 82px; - padding: 4px; - color: rgba(255, 255, 255, 0.8); - background: rgba(255, 255, 255, 0.2); - border: none; - resize: none; - box-sizing: border-box; -}