diff --git a/package-lock.json b/package-lock.json index 17c8eef..f9c2ba0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "txtdot", - "version": "1.4.0", + "version": "1.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "txtdot", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "dependencies": { "@fastify/static": "^6.10.2", diff --git a/package.json b/package.json index 69530c5..892d2b5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "txtdot", - "version": "1.4.0", + "version": "1.5.0", "private": true, "description": "", "main": "dist/app.js", diff --git a/src/app.ts b/src/app.ts index c5869ad..4cbb439 100644 --- a/src/app.ts +++ b/src/app.ts @@ -16,6 +16,7 @@ import rawHtml from './routes/api/raw-html'; import publicConfig from './publicConfig'; import errorHandler from './errors/handler'; import getConfig from './config/main'; +import searchRoute from './routes/browser/search'; class App { async init() { @@ -54,6 +55,10 @@ class App { fastify.register(indexRoute); fastify.register(getRoute); + if (config.search.enabled) { + fastify.register(searchRoute); + } + if (config.proxy_res) fastify.register(proxyRoute); fastify.register(parseRoute); diff --git a/src/config/config.service.ts b/src/config/config.service.ts index 415e246..870e24a 100644 --- a/src/config/config.service.ts +++ b/src/config/config.service.ts @@ -7,6 +7,7 @@ export class ConfigService { public readonly reverse_proxy: boolean; public readonly proxy_res: boolean; public readonly swagger: boolean; + public readonly search: SearchConfig; constructor() { config(); @@ -20,6 +21,11 @@ export class ConfigService { this.proxy_res = this.parseBool(process.env.PROXY_RES, true); this.swagger = this.parseBool(process.env.SWAGGER, false); + + this.search = { + enabled: this.parseBool(process.env.SEARCH_ENABLED, false), + searx_url: process.env.SEARX_URL, + }; } parseBool(value: string | undefined, def: boolean): boolean { @@ -27,3 +33,8 @@ export class ConfigService { return value === 'true' || value === '1'; } } + +interface SearchConfig { + enabled: boolean; + searx_url?: string; +} diff --git a/src/handlers/main.ts b/src/handlers/main.ts index 9dba929..81a3636 100644 --- a/src/handlers/main.ts +++ b/src/handlers/main.ts @@ -11,6 +11,7 @@ import { Readable } from 'stream'; import readability from './readability'; import google, { GoogleDomains } from './google'; import stackoverflow, { StackOverflowDomains } from './stackoverflow/main'; +import searx, { SearxDomains } from './searx'; import isLocalResource from '../utils/islocal'; @@ -75,6 +76,7 @@ export const engines: Engines = { readability, google, stackoverflow, + searx, }; export const engineList: string[] = Object.keys(engines); @@ -88,4 +90,8 @@ export const fallback: EnginesMatch = [ pattern: StackOverflowDomains, engine: engines.stackoverflow, }, + { + pattern: SearxDomains, + engine: engines.searx, + }, ]; diff --git a/src/handlers/searx.ts b/src/handlers/searx.ts new file mode 100644 index 0000000..2aeafe8 --- /dev/null +++ b/src/handlers/searx.ts @@ -0,0 +1,59 @@ +import { HandlerInput } from './handler-input'; +import { IHandlerOutput } from './handler.interface'; + +export default async function searx( + input: HandlerInput +): Promise { + const document = input.parseDom().window.document; + + const search = document.getElementById('q') as HTMLTextAreaElement; + + const url = new URL(input.getUrl()); + + const page = parseInt(url.searchParams.get('pageno') || '1'); + + const page_footer = `${ + page !== 1 + ? `Previous |` + : '' + } Next`; + + const articles = Array.from(document.querySelectorAll('.result')); + + const articles_parsed = articles.map((a) => { + const parsed = { + url: + (a.getElementsByClassName('url_wrapper')[0] as HTMLAnchorElement) + .href || '', + title: + (a.getElementsByTagName('h3')[0] as HTMLHeadingElement).textContent || + '', + content: + (a.getElementsByClassName('content')[0] as HTMLDivElement) + .textContent || '', + }; + + return { + html: `${parsed.title}

${parsed.content}


`, + text: `${parsed.title} (${parsed.url})\n${parsed.content}\n---\n\n`, + }; + }); + + const content = `${articles_parsed + .map((a) => a.html) + .join('')}${page_footer}`; + const textContent = articles_parsed.map((a) => a.text).join(''); + + return { + content, + textContent, + title: `${search.value} - Searx - Page ${page}`, + lang: document.documentElement.lang, + }; +} + +export const SearxDomains = ['searx.*']; diff --git a/src/publicConfig.ts b/src/publicConfig.ts index 04daf30..68aaee9 100644 --- a/src/publicConfig.ts +++ b/src/publicConfig.ts @@ -1,5 +1,5 @@ export default { - version: '1.4.0', + version: '1.5.0', description: 'txtdot is an HTTP proxy that parses only text, links and pictures from pages reducing internet bandwidth usage, removing ads and heavy scripts', }; diff --git a/src/routes/browser/get.ts b/src/routes/browser/get.ts index 4ee62f2..cefce11 100644 --- a/src/routes/browser/get.ts +++ b/src/routes/browser/get.ts @@ -4,6 +4,8 @@ import { GetSchema, IGetSchema } from '../../types/requests/browser'; import handlePage from '../../handlers/main'; import { generateRequestUrl } from '../../utils/generate'; +import getConfig from '../../config/main'; + export default async function getRoute(fastify: FastifyInstance) { fastify.get( '/get', @@ -27,7 +29,11 @@ export default async function getRoute(fastify: FastifyInstance) { return parsed.textContent; } else { reply.type('text/html; charset=utf-8'); - return reply.view('/templates/get.ejs', { parsed, remoteUrl }); + return reply.view('/templates/get.ejs', { + parsed, + remoteUrl, + config: getConfig(), + }); } } ); diff --git a/src/routes/browser/index.ts b/src/routes/browser/index.ts index 83ee81b..413de53 100644 --- a/src/routes/browser/index.ts +++ b/src/routes/browser/index.ts @@ -4,8 +4,14 @@ import publicConfig from '../../publicConfig'; import { engineList } from '../../handlers/main'; import { indexSchema } from '../../types/requests/browser'; +import getConfig from '../../config/main'; + export default async function indexRoute(fastify: FastifyInstance) { fastify.get('/', { schema: indexSchema }, async (_, reply) => { - return reply.view('/templates/index.ejs', { publicConfig, engineList }); + return reply.view('/templates/index.ejs', { + publicConfig, + engineList, + config: getConfig(), + }); }); } diff --git a/src/routes/browser/search.ts b/src/routes/browser/search.ts new file mode 100644 index 0000000..77f00c5 --- /dev/null +++ b/src/routes/browser/search.ts @@ -0,0 +1,24 @@ +import { FastifyInstance } from 'fastify'; + +import { searchSchema, ISearchSchema } from '../../types/requests/browser'; + +import getConfig from '../../config/main'; + +export default async function searchRoute(fastify: FastifyInstance) { + fastify.get( + '/search', + { schema: searchSchema }, + async (request, reply) => { + const query = request.query.q; + + const config = getConfig(); + + if (config.search.enabled) { + const searchUrl = `${config.search.searx_url}/search?q=${query}`; + reply.redirect(`/get?url=${encodeURI(searchUrl)}`); + } else { + throw new Error('Search is not enabled'); + } + } + ); +} diff --git a/src/types/requests/browser.ts b/src/types/requests/browser.ts index 4504b0c..45d1716 100644 --- a/src/types/requests/browser.ts +++ b/src/types/requests/browser.ts @@ -10,6 +10,22 @@ export interface IProxySchema { Querystring: IProxyQuerySchema; } +export interface ISearchSchema { + Querystring: ISearchQuerySchema; +} + +export const searchQuerySchema = { + type: 'object', + required: ['q'], + properties: { + q: { + type: 'string', + description: 'Search query', + }, + }, +} as const; +export type ISearchQuerySchema = FromSchema; + export const getQuerySchema = { type: 'object', required: ['url'], @@ -48,6 +64,12 @@ export const indexSchema = { produces: ['text/html'], }; +export const searchSchema: FastifySchema = { + description: 'Search redirection page', + hide: true, + querystring: searchQuerySchema, +}; + export const GetSchema: FastifySchema = { description: 'Get page', hide: true, diff --git a/static/form.css b/static/form.css index b5bed02..2bc681d 100644 --- a/static/form.css +++ b/static/form.css @@ -29,7 +29,8 @@ label { font-size: 0.9rem; } -#url { +#url, +#search { width: 100%; height: 100%; /* shrink to #submit height */ diff --git a/static/search.css b/static/search.css new file mode 100644 index 0000000..f880f94 --- /dev/null +++ b/static/search.css @@ -0,0 +1,25 @@ +.right { + display: flex; + margin-left: auto; +} + +#search { + width: 100%; + height: 100%; /* shrink to #submit height */ + + outline: none; + border: 0; + border-bottom: 0.125rem solid var(--fg2); + + background: var(--bg); + color: var(--fg); + font-size: 1rem; + + margin-right: 0.5rem; + margin-left: 0.5rem; +} + +#search::placeholder { + color: var(--fg2); + opacity: 1; +} diff --git a/templates/get.ejs b/templates/get.ejs index 6e7d18d..e7d84b6 100644 --- a/templates/get.ejs +++ b/templates/get.ejs @@ -8,12 +8,27 @@ <%= parsed.title %> + <% + if (config.search.enabled) { + %><% + } + %>

<%= parsed.title %> diff --git a/templates/index.ejs b/templates/index.ejs index c00d236..63e9a20 100644 --- a/templates/index.ejs +++ b/templates/index.ejs @@ -21,6 +21,23 @@

<%= publicConfig.description %>

+ <% + if (config.search.enabled) { + + %> +
+
+ +
+
+ +
+
+ <% + + %>
Advanced<% + } + %>
@@ -52,6 +69,11 @@
+ <% + if (config.search.enabled) { + %>
<% + } + %>