blob: c2b3a346a38a856ad1c2f9a3ef8b565ba7a29269 [file] [log] [blame]
Adrià Vilanova Martínez47c4c812024-12-05 15:34:40 +01001import MWOptionsWatcherClient from '../common/mainWorldOptionsWatcher/Client.js';
2import { OptionCodename } from '../common/options/optionsPrototype.js';
3import { ProtobufObject } from '../common/protojs.types.js';
4
5import createMessageRemoveParentRef from './responseModifiers/createMessageRemoveParentRef';
6import flattenThread from './responseModifiers/flattenThread';
7import loadMoreThread from './responseModifiers/loadMoreThread';
8import { Modifier } from './responseModifiers/types';
9
10export const responseModifiers = [
11 loadMoreThread,
12 flattenThread,
13 createMessageRemoveParentRef,
14] as Modifier[];
15
16// Content script target
17export const kCSTarget = 'TWPT-XHRInterceptorOptionsWatcher-CS';
18// Main world (AKA regular web page) target
19export const kMWTarget = 'TWPT-XHRInterceptorOptionsWatcher-MW';
20
21export default class ResponseModifier {
22 private optionsWatcher: MWOptionsWatcherClient;
23
24 constructor() {
25 this.optionsWatcher = new MWOptionsWatcherClient(
26 Array.from(this.watchingFeatures(responseModifiers)),
27 kCSTarget,
28 kMWTarget,
29 );
30 }
31
32 private watchingFeatures(modifiers: Modifier[]): Set<OptionCodename> {
33 const union = new Set<OptionCodename>();
34 for (const m of modifiers) {
35 if (!m.featureGated) continue;
36 for (const feature of m.features) union.add(feature);
37 }
38 return union;
39 }
40
41 private async getMatchingModifiers(requestUrl: string) {
42 // First filter modifiers which match the request URL regex.
43 const urlModifiers = responseModifiers.filter((modifier) =>
44 requestUrl.match(modifier.urlRegex),
45 );
46
47 // Now filter modifiers which require a certain feature to be enabled
48 // (feature-gated modifiers).
49 const featuresAreEnabled = await this.optionsWatcher.areEnabled(
50 Array.from(this.watchingFeatures(urlModifiers)),
51 );
52
53 // #!if !production
54 if (Object.keys(featuresAreEnabled).length > 0) {
55 console.debug(
56 '[XHR Interceptor - Response Modifier] Requested features',
57 featuresAreEnabled,
58 'for request',
59 requestUrl,
60 );
61 }
62 // #!endif
63
64 return urlModifiers.filter((modifier) => {
65 return !modifier.featureGated || modifier.isEnabled(featuresAreEnabled);
66 });
67 }
68
69 async intercept(interception: InterceptedResponse): Promise<Result> {
70 const matchingModifiers = await this.getMatchingModifiers(interception.url);
71
72 // If we didn't find any matching modifiers, return the response right away.
73 if (matchingModifiers.length === 0) return { wasModified: false };
74
75 // Otherwise, apply the modifiers sequentially and set the new response.
76 let json = interception.originalResponse;
77 for (const modifier of matchingModifiers) {
78 json = await modifier.interceptor(json, interception.url);
79 }
80 return {
81 wasModified: true,
82 modifiedResponse: json,
83 };
84 }
85}
86
87/**
88 * Represents an intercepted response.
89 */
90export interface InterceptedResponse {
91 /**
92 * URL of the original request.
93 */
94 url: string;
95
96 /**
97 * Object with the response as intercepted without any modification.
98 */
99 originalResponse: ProtobufObject;
100}
101
102export type Result =
103 | { wasModified: false }
104 | {
105 wasModified: true;
106 modifiedResponse: ProtobufObject;
107 };