/ src / entrypoints / background.ts
background.ts
  1  import { chain, ifilter, imap, uniqueEverseen } from 'itertools';
  2  import { nanoid } from 'nanoid';
  3  import { createRouter } from 'radix3';
  4  
  5  import '@/register';
  6  
  7  import { EventDispatcher, EventListener } from '@/events';
  8  import { registerChannelsHandlers } from '@/request/channels';
  9  import { registerCommentsHandlers } from '@/request/comments';
 10  import { Context } from '@/request/ctx';
 11  import { registerStoriesHandlers } from '@/request/stories';
 12  import { registerTimelineHandlers } from '@/request/timeline';
 13  import { setLocales, t } from '@/translation/js';
 14  import type {} from '@/types/tsPatch';
 15  import { Logger } from '@/utils/logger';
 16  
 17  declare global {
 18  	var ctx: InstanceType<typeof Context>;
 19  }
 20  
 21  enum MenuItemId {
 22  	TAG_LINK_COPY = 'tag-link/copy',
 23  	TAG_LINK_BLOCK = 'tag-link/block',
 24  }
 25  
 26  export default defineBackground(() => {
 27  	browser.browserAction.onClicked.addListener(() => browser.runtime.openOptionsPage());
 28  
 29  	const logger = Logger.create('bg');
 30  	const unloadStylesClassPrefix = `${browser.runtime.id}__unload-styles__`;
 31  	const unloadStylesClass = `${unloadStylesClassPrefix}${nanoid()}`;
 32  
 33  	const ctx = (globalThis.ctx = new Context(logger));
 34  
 35  	browser.menus.create({
 36  		id: MenuItemId.TAG_LINK_COPY,
 37  		contexts: ['link'],
 38  		targetUrlPatterns: [`${ctx.origin}/tags/*`],
 39  		title: t('copy-tag'),
 40  	});
 41  
 42  	browser.menus.create({
 43  		id: MenuItemId.TAG_LINK_BLOCK,
 44  		contexts: ['link'],
 45  		targetUrlPatterns: [`${ctx.origin}/tags/*`],
 46  		title: t('block-tag'),
 47  	});
 48  
 49  	browser.menus.onClicked.addListener((info, tab) => {
 50  		logger.debug('handling click on menu item', info.menuItemId, { info, tab });
 51  
 52  		switch (info.menuItemId) {
 53  			case MenuItemId.TAG_LINK_COPY: {
 54  				info.linkText &&
 55  					navigator.clipboard
 56  						.writeText(info.linkText)
 57  						.catch(err => logger.error('failed to copy tag', { info, tab }, err));
 58  				break;
 59  			}
 60  
 61  			case MenuItemId.TAG_LINK_BLOCK: {
 62  				info.linkText &&
 63  					ctx.blockedTags
 64  						.block(info.linkText)
 65  						.catch(err => logger.error('failed to block tag', { info, tab }, err));
 66  				break;
 67  			}
 68  		}
 69  	});
 70  
 71  	window.addEventListener('languagechange', () => {
 72  		logger.debug('languagechange', navigator.languages);
 73  		setLocales(navigator.languages);
 74  		browser.menus.update(MenuItemId.TAG_LINK_COPY, { title: t('copy-tag') });
 75  		browser.menus.update(MenuItemId.TAG_LINK_BLOCK, { title: t('block-tag') });
 76  	});
 77  
 78  	const eventListener = new EventListener(logger, (event, sender, sendResponse) => {
 79  		if (browser.runtime.id !== sender.id) {
 80  			return;
 81  		}
 82  
 83  		switch (event.type) {
 84  			case 'GetTabId':
 85  				return sendResponse(sender.tab?.id);
 86  
 87  			case 'GetUnloadStylesClassWithPrefix':
 88  				return sendResponse([unloadStylesClassPrefix, unloadStylesClass]);
 89  
 90  			case 'GetUnloadStylesClass': {
 91  				(async () => {
 92  					try {
 93  						logger.debug('inserting unload styles to tab', sender.tab?.id, 'frame', sender.frameId);
 94  						await browser.tabs.insertCSS(sender.tab?.id, {
 95  							code: `.${CSS.escape(unloadStylesClass)} { opacity: 0 !important; }`,
 96  							frameId: sender.frameId,
 97  							runAt: 'document_start',
 98  						});
 99  						sendResponse(unloadStylesClass);
100  					} catch (err) {
101  						logger.error('failed to insert unload styles:', err);
102  					}
103  				})();
104  
105  				return true;
106  			}
107  
108  			default:
109  				(async () => {
110  					try {
111  						const allCoubTabIds = imap(
112  							ifilter(
113  								await browser.tabs.query({ url: `${ctx.origin}/*` }),
114  								tab => tab.discarded !== true && (!tab.url || !prohibitedRouter.lookup(tab.url)),
115  							),
116  							tab => tab.id,
117  						);
118  
119  						for (const tabId of uniqueEverseen(chain([sender.tab?.id], allCoubTabIds))) {
120  							if (typeof tabId === 'number') {
121  								EventDispatcher.dispatch(`tab ${tabId}`, event, event =>
122  									browser.tabs.sendMessage(tabId, event),
123  								);
124  							}
125  						}
126  					} catch (err) {
127  						logger.error('failed to broadcast message:', err);
128  					}
129  				})();
130  		}
131  	});
132  
133  	registerTimelineHandlers(ctx);
134  	registerStoriesHandlers(ctx);
135  	registerCommentsHandlers(ctx);
136  	registerChannelsHandlers(ctx);
137  
138  	const prohibitedRouter = createRouter({
139  		routes: {
140  			[`${ctx.origin}/chat`]: void 0,
141  			[`${ctx.origin}/chat/*`]: void 0,
142  			[`${ctx.origin}/account/*`]: void 0,
143  			[`${ctx.origin}/official/*`]: void 0,
144  			[`${ctx.origin}/brand-assets`]: void 0,
145  			[`${ctx.origin}/tos`]: void 0,
146  			[`${ctx.origin}/privacy`]: void 0,
147  			[`${ctx.origin}/rules`]: void 0,
148  			[`${ctx.origin}/dmca`]: void 0,
149  		},
150  	});
151  
152  	browser.runtime.onSuspend.addListener(() => {
153  		eventListener[Symbol.dispose]();
154  		ctx[Symbol.dispose]();
155  	});
156  });