SnapEnhance Web

A userscript to Enhance the User experience on Snapchat Web

  1. // ==UserScript==
  2. // @name SnapEnhance Web
  3. // @namespace snapenhance-web
  4. // @description A userscript to Enhance the User experience on Snapchat Web
  5. // @version 1.1.1
  6. // @author SnapEnhance
  7. // @source https://github.com/SnapEnhance/web/
  8. // @license GPL-3.0-only
  9. // @supportURL https://github.com/SnapEnhance/web/issues
  10. // @match *://snapchat.com/web/*
  11. // @icon https://www.google.com/s2/favicons?sz=64&domain=snapchat.com
  12. // @grant unsafeWindow
  13. // @run-at document-start
  14. // ==/UserScript==
  15. (function (window) {
  16. function simpleHook(object, name, proxy) {
  17. const old = object[name];
  18. object[name] = proxy(old, object);
  19. }
  20. // Bypass upload size
  21. Object.defineProperty(File.prototype, "size", {
  22. get: function () {
  23. return 500;
  24. }
  25. });
  26. // hook main window requests
  27. const oldWindowFetch = window.fetch;
  28. window.fetch = (...args) => {
  29. const url = args[0].url;
  30. if (typeof url === "string") {
  31. if (url.endsWith("readreceipt-indexer/batchuploadreadreceipts")) {
  32. console.log("bypassed story read receipt");
  33. return new Promise((resolve) => resolve(new Response(null, { status: 200 })));
  34. }
  35. }
  36. return oldWindowFetch(...args);
  37. };
  38. // Inject into worker
  39. function workerInjected() {
  40. function hookPreRequest(request) {
  41. if (request.url.endsWith("messagingcoreservice.MessagingCoreService/SendTypingNotification")) {
  42. console.log("bypassed typing notification");
  43. return null;
  44. }
  45. if (request.url.endsWith("messagingcoreservice.MessagingCoreService/UpdateConversation")) {
  46. console.log("bypassed conversation read receipt");
  47. return null;
  48. }
  49. return request;
  50. }
  51. async function hookPostRequest(request, response) {
  52. if (request.headers && request.headers.get("content-type") === "application/grpc-web+proto") {
  53. const arrayBuffer = await response.arrayBuffer();
  54. response.arrayBuffer = async () => arrayBuffer;
  55. }
  56. return response;
  57. }
  58. // Hook websocket (hide bitmoji)
  59. WebSocket.prototype.send = new Proxy(WebSocket.prototype.send, {
  60. apply: function (target, thisArg, argumentsList) {
  61. console.log("WebSocket send", argumentsList[0]);
  62. // return target.apply(thisArg, argumentsList);
  63. }
  64. });
  65. // Hook worker web requests
  66. const oldFetch = fetch;
  67. // @ts-ignore
  68. // eslint-disable-next-line no-implicit-globals
  69. fetch = async (...args) => {
  70. args[0] = hookPreRequest(args[0]);
  71. if (args[0] == null) {
  72. return new Promise((resolve) => resolve(new Response(null, { status: 200 })));
  73. }
  74. const requestBody = args[0].body;
  75. if (requestBody && !requestBody.locked) {
  76. const buffer = await requestBody.getReader().read();
  77. args[0] = new Request(args[0], {
  78. body: buffer.value,
  79. headers: args[0].headers
  80. });
  81. }
  82. // @ts-ignore
  83. const result = oldFetch(...args);
  84. return new Promise(async (resolve, reject) => {
  85. try {
  86. resolve(await hookPostRequest(args[0], await result));
  87. }
  88. catch (e) {
  89. console.info("Fetch error", e);
  90. reject(e);
  91. }
  92. });
  93. };
  94. }
  95. const oldBlobClass = window.Blob;
  96. window.Blob = class HookedBlob extends Blob {
  97. constructor(...args) {
  98. const data = args[0][0];
  99. if (typeof data === "string" && data.startsWith("importScripts")) {
  100. args[0][0] += `${workerInjected.toString()};workerInjected();`;
  101. window.Blob = oldBlobClass;
  102. }
  103. super(...args);
  104. }
  105. };
  106. simpleHook(document, "createElement", (proxy, instance) => (...args) => {
  107. const result = proxy.call(instance, ...args);
  108. // Allow audio note and image download
  109. if (args[0] === "audio" || args[0] === "video" || args[0] === "img") {
  110. simpleHook(result, "setAttribute", (proxy2, instance2) => (...args2) => {
  111. result.style = "pointer-events: auto;";
  112. if (args2[0] === "controlsList")
  113. return;
  114. proxy2.call(instance2, ...args2);
  115. });
  116. result.addEventListener("load", (_) => {
  117. result.parentElement?.addEventListener("contextmenu", (event) => {
  118. event.stopImmediatePropagation();
  119. });
  120. });
  121. result.addEventListener("contextmenu", (event) => {
  122. event.stopImmediatePropagation();
  123. });
  124. }
  125. return result;
  126. });
  127. // Always focused
  128. simpleHook(document, "hasFocus", () => () => true);
  129. const oldAddEventListener = EventTarget.prototype.addEventListener;
  130. Object.defineProperty(EventTarget.prototype, "addEventListener", {
  131. value: function (...args) {
  132. const eventName = args[0];
  133. if (eventName === "keydown")
  134. return;
  135. if (eventName === "focus")
  136. return;
  137. return oldAddEventListener.call(this, ...args);
  138. }
  139. });
  140. })(window.unsafeWindow || window);