mastopod.js
edited
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})();