mastopod custom pod workaround
mastopod.js edited
243 lines 8.1 kB view raw
1// ==UserScript== 2// @name Replace activitypods & proxy OIDC path 3// @namespace http://tampermonkey.net/ 4// @version 1.2 5// @description Intercept fetch/XHR to activitypods pod-providers and proxy pds-sp.whey.party OIDC discovery path to .oidc/auth/... path 6// @author You 7// @match *://*/* 8// @grant none 9// @run-at document-start 10// ==/UserScript== 11 12(() => { 13 'use strict'; 14 15 // ======= EDIT THIS: your replacement JSON object ======= 16 const REPLACEMENT_JSON = { 17 "@context": [ 18 "https://www.w3.org/ns/activitystreams", 19 "https://activitypods.org/.well-known/context.json" 20 ], 21 "id": "https://activitypods.org/data/pod-providers", 22 "type": [ 23 "ldp:Container", 24 "ldp:BasicContainer" 25 ], 26 "ldp:contains": [ 27 { 28 "id": "https://activitypods.org/data/pod-providers/armoise.co", 29 "type": "apods:PodProvider", 30 "apods:baseUrl": "https://armoise.co", 31 "apods:area": "Oise, France", 32 "apods:locales": "fr", 33 "apods:providedBy": "Réseaux de Vie" 34 }, 35 { 36 "id": "https://activitypods.org/data/pod-providers/mypod.store", 37 "type": "apods:PodProvider", 38 "apods:baseUrl": "https://mypod.store", 39 "apods:area": "France", 40 "apods:locales": "en", 41 "apods:providedBy": "Assemblée Virtuelle" 42 }, 43 { 44 "id": "https://activitypods.org/data/pod-providers/pds-sp.whey.party", 45 "type": "apods:PodProvider", 46 "apods:baseUrl": "https://pds-sp.whey.party", 47 "apods:area": "TheWeb", 48 "apods:locales": "en", 49 "apods:providedBy": "Wowwww" 50 } 51 ] 52 }; 53 // ====================================================== 54 55 // ActivityPods target 56 const AP_ORIGIN = 'https://activitypods.org'; 57 const AP_PATH = '/data/pod-providers'; 58 const AP_FULL = AP_ORIGIN + AP_PATH; 59 60 // OIDC proxy: source -> forward to target 61 const OIDC_SOURCE = 'https://pds-sp.whey.party/.well-known/openid-configuration'; 62 const OIDC_SOURCE2 = 'https://pds-sp.whey.party/whey/.well-known/openid-configuration' 63 const OIDC_TARGET = 'https://pds-sp.whey.party/.oidc/auth/.well-known/openid-configuration'; 64 65 function urlMatches(targetUrl, origin, path) { 66 try { 67 const parsed = new URL(targetUrl, location.origin); 68 return parsed.origin === origin && parsed.pathname === path; 69 } catch (e) { 70 return false; 71 } 72 } 73 74 function isExactUrl(url, exact) { 75 try { 76 const parsed = new URL(url, location.origin); 77 return parsed.href === new URL(exact).href; 78 } catch (e) { 79 return false; 80 } 81 } 82 83 // Helper to create a Response object for fetch from an object 84 function makeResponseFromObject(obj) { 85 const body = JSON.stringify(obj); 86 return new Response(body, { 87 status: 200, 88 statusText: 'OK', 89 headers: { 90 'Content-Type': 'application/json; charset=utf-8', 91 'X-Userscript-Replaced': '1' 92 } 93 }); 94 } 95 96 // ---- Patch fetch ---- 97 const originalFetch = window.fetch.bind(window); 98 window.fetch = async function (input, init) { 99 try { 100 // Determine URL string from Request or string 101 let reqUrl = input; 102 if (input && typeof input === 'object' && input.url) { 103 reqUrl = input.url; 104 } 105 106 // If it's the ActivityPods providers URL -> return replacement JSON 107 if (isExactUrl(reqUrl, AP_FULL)) { 108 return makeResponseFromObject(REPLACEMENT_JSON); 109 } 110 111 // If it's the OIDC source URL -> proxy to OIDC target 112 if (isExactUrl(reqUrl, OIDC_SOURCE) || isExactUrl(reqUrl, OIDC_SOURCE2)) { 113 // forward the original init where possible 114 return originalFetch(OIDC_TARGET, init); 115 } 116 117 } catch (e) { 118 console.error('userscript fetch interception error', e); 119 // fallthrough to original fetch 120 } 121 return originalFetch(input, init); 122 }; 123 124 // Also patch globalThis.fetch if different 125 try { 126 if (globalThis.fetch !== window.fetch) { 127 globalThis.fetch = window.fetch; 128 } 129 } catch (e) {} 130 131 // ---- Patch XMLHttpRequest ---- 132 const OriginalXHR = window.XMLHttpRequest; 133 134 function fakeXHRResponse(xhr, replacementText, status = 200, statusText = 'OK', headers = {}) { 135 // Populate common XHR fields and then call handlers / dispatch events asynchronously 136 try { xhr.readyState = 4; } catch (e) {} 137 try { xhr.status = status; } catch (e) {} 138 try { xhr.statusText = statusText; } catch (e) {} 139 try { 140 // XHR often expects responseText and response 141 Object.defineProperty(xhr, 'responseText', { value: replacementText, writable: false }); 142 } catch (e) { 143 try { xhr.responseText = replacementText; } catch (err) {} 144 } 145 try { 146 Object.defineProperty(xhr, 'response', { value: replacementText, writable: false }); 147 } catch (e) { 148 try { xhr.response = replacementText; } catch (err) {} 149 } 150 151 // Optionally you could expose headers via getAllResponseHeaders, but that's more invasive. 152 setTimeout(() => { 153 try { 154 if (typeof xhr.onreadystatechange === 'function') { 155 xhr.onreadystatechange(); 156 } 157 if (typeof xhr.onload === 'function') { 158 xhr.onload(); 159 } 160 // dispatch events so addEventListener handlers run 161 try { xhr.dispatchEvent && xhr.dispatchEvent(new Event('readystatechange')); } catch (e) {} 162 try { xhr.dispatchEvent && xhr.dispatchEvent(new Event('load')); } catch (e) {} 163 } catch (err) { 164 console.error('userscript XHR callback error', err); 165 } 166 }, 0); 167 } 168 169 function PatchedXHR() { 170 const xhr = new OriginalXHR(); 171 172 // Keep references to original methods 173 const originalOpen = xhr.open; 174 const originalSend = xhr.send; 175 176 let _method = null; 177 let _url = null; 178 let _isAP = false; 179 let _isOidcSource = false; 180 181 xhr.open = function (method, url, async = true, user, password) { 182 _method = method; 183 _url = url; 184 try { 185 _isAP = isExactUrl(url, AP_FULL); 186 _isOidcSource = isExactUrl(url, OIDC_SOURCE) || isExactUrl(url, OIDC_SOURCE2); 187 } catch (e) { 188 _isAP = false; 189 _isOidcSource = false; 190 } 191 return originalOpen.apply(xhr, arguments); 192 }; 193 194 xhr.send = function (body) { 195 // If it's the ActivityPods providers URL -> return replacement JSON (simulate response) 196 if (_isAP) { 197 try { 198 const replacementText = JSON.stringify(REPLACEMENT_JSON); 199 fakeXHRResponse(xhr, replacementText, 200, 'OK'); 200 return; 201 } catch (e) { 202 console.error('userscript failed to simulate XHR response for AP', e); 203 // fallthrough to originalSend 204 } 205 } 206 207 // If it's the OIDC source URL -> fetch the OIDC target and simulate response 208 if (_isOidcSource) { 209 (async () => { 210 try { 211 const resp = await originalFetch(OIDC_TARGET, { 212 method: _method || 'GET', 213 // don't forward XHR body here; XHR discovery is GET normally 214 // but preserve credentials mode if possible by not specifying mode/credentials 215 }); 216 const text = await resp.text(); 217 fakeXHRResponse(xhr, text, resp.status || 200, resp.statusText || 'OK'); 218 } catch (err) { 219 console.error('userscript OIDC proxy fetch failed', err); 220 // simulate an error XHR (status 0) so page sees a failure 221 fakeXHRResponse(xhr, '', 0, 'Error'); 222 } 223 })(); 224 return; 225 } 226 227 // otherwise fall back to normal send 228 return originalSend.apply(xhr, arguments); 229 }; 230 231 return xhr; 232 } 233 234 // Keep prototype linkage to appear like a normal XHR 235 PatchedXHR.prototype = OriginalXHR.prototype; 236 window.XMLHttpRequest = PatchedXHR; 237 238 console.info('Userscript active: AP replacement and OIDC proxy enabled.'); 239 console.info('AP target:', AP_FULL); 240 console.info('OIDC source -> target:', OIDC_SOURCE, '->', OIDC_TARGET); 241 console.info('Current replacement JSON:', REPLACEMENT_JSON); 242 243})();