mirror of
https://github.com/searxng/searxng.git
synced 2024-12-11 11:56:28 +00:00
9a4fa7cc4f
A recent update from Mullvad Leta introduced the img_elem. This update broke the existing logic. Now, by checking the length of the dom_result to see if it was included in the return results, we can handle the logic accordingly.
207 lines
6.5 KiB
Python
207 lines
6.5 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
"""This is the implementation of the Mullvad-Leta meta-search engine.
|
|
|
|
This engine **REQUIRES** that searxng operate within a Mullvad VPN
|
|
|
|
If using docker, consider using gluetun for easily connecting to the Mullvad
|
|
|
|
- https://github.com/qdm12/gluetun
|
|
|
|
Otherwise, follow instructions provided by Mullvad for enabling the VPN on Linux
|
|
|
|
- https://mullvad.net/en/help/install-mullvad-app-linux
|
|
|
|
.. hint::
|
|
|
|
The :py:obj:`EngineTraits` is empty by default. Maintainers have to run
|
|
``make data.traits`` (in the Mullvad VPN / :py:obj:`fetch_traits`) and rebase
|
|
the modified JSON file ``searx/data/engine_traits.json`` on every single
|
|
update of SearXNG!
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
from httpx import Response
|
|
from lxml import html
|
|
from searx.enginelib.traits import EngineTraits
|
|
from searx.locales import region_tag, get_official_locales
|
|
from searx.utils import eval_xpath, extract_text, eval_xpath_list
|
|
from searx.exceptions import SearxEngineResponseException
|
|
|
|
if TYPE_CHECKING:
|
|
import logging
|
|
|
|
logger = logging.getLogger()
|
|
|
|
traits: EngineTraits
|
|
|
|
use_cache: bool = True # non-cache use only has 100 searches per day!
|
|
|
|
leta_engine: str = 'google'
|
|
|
|
search_url = "https://leta.mullvad.net"
|
|
|
|
# about
|
|
about = {
|
|
"website": search_url,
|
|
"wikidata_id": 'Q47008412', # the Mullvad id - not leta, but related
|
|
"official_api_documentation": 'https://leta.mullvad.net/faq',
|
|
"use_official_api": False,
|
|
"require_api_key": False,
|
|
"results": 'HTML',
|
|
}
|
|
|
|
# engine dependent config
|
|
categories = ['general', 'web']
|
|
paging = True
|
|
max_page = 50
|
|
time_range_support = True
|
|
time_range_dict = {
|
|
"day": "d1",
|
|
"week": "w1",
|
|
"month": "m1",
|
|
"year": "y1",
|
|
}
|
|
|
|
available_leta_engines = [
|
|
'google', # first will be default if provided engine is invalid
|
|
'brave',
|
|
]
|
|
|
|
|
|
def is_vpn_connected(dom: html.HtmlElement) -> bool:
|
|
"""Returns true if the VPN is connected, False otherwise"""
|
|
connected_text = extract_text(eval_xpath(dom, '//main/div/p[1]'))
|
|
return connected_text != 'You are not connected to Mullvad VPN.'
|
|
|
|
|
|
def assign_headers(headers: dict) -> dict:
|
|
"""Assigns the headers to make a request to Mullvad Leta"""
|
|
headers['Accept'] = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"
|
|
headers['Content-Type'] = "application/x-www-form-urlencoded"
|
|
headers['Host'] = "leta.mullvad.net"
|
|
headers['Origin'] = "https://leta.mullvad.net"
|
|
return headers
|
|
|
|
|
|
def request(query: str, params: dict):
|
|
country = traits.get_region(params.get('searxng_locale', 'all'), traits.all_locale) # type: ignore
|
|
|
|
result_engine = leta_engine
|
|
if leta_engine not in available_leta_engines:
|
|
result_engine = available_leta_engines[0]
|
|
logger.warning(
|
|
'Configured engine "%s" not one of the available engines %s, defaulting to "%s"',
|
|
leta_engine,
|
|
available_leta_engines,
|
|
result_engine,
|
|
)
|
|
|
|
params['url'] = search_url
|
|
params['method'] = 'POST'
|
|
params['data'] = {
|
|
"q": query,
|
|
"gl": country if country is str else '',
|
|
'engine': result_engine,
|
|
}
|
|
# pylint: disable=undefined-variable
|
|
if use_cache:
|
|
params['data']['oc'] = "on"
|
|
# pylint: enable=undefined-variable
|
|
|
|
if params['time_range'] in time_range_dict:
|
|
params['dateRestrict'] = time_range_dict[params['time_range']]
|
|
else:
|
|
params['dateRestrict'] = ''
|
|
|
|
if params['pageno'] > 1:
|
|
# Page 1 is n/a, Page 2 is 11, page 3 is 21, ...
|
|
params['data']['start'] = ''.join([str(params['pageno'] - 1), "1"])
|
|
|
|
if params['headers'] is None:
|
|
params['headers'] = {}
|
|
|
|
assign_headers(params['headers'])
|
|
return params
|
|
|
|
|
|
def extract_result(dom_result: list[html.HtmlElement]):
|
|
# Infoboxes sometimes appear in the beginning and will have a length of 0
|
|
if len(dom_result) == 3:
|
|
[a_elem, h3_elem, p_elem] = dom_result
|
|
elif len(dom_result) == 4:
|
|
[_, a_elem, h3_elem, p_elem] = dom_result
|
|
else:
|
|
return None
|
|
|
|
return {
|
|
'url': extract_text(a_elem.text),
|
|
'title': extract_text(h3_elem),
|
|
'content': extract_text(p_elem),
|
|
}
|
|
|
|
|
|
def extract_results(search_results: html.HtmlElement):
|
|
for search_result in search_results:
|
|
dom_result = eval_xpath_list(search_result, 'div/div/*')
|
|
result = extract_result(dom_result)
|
|
if result is not None:
|
|
yield result
|
|
|
|
|
|
def response(resp: Response):
|
|
"""Checks if connected to Mullvad VPN, then extracts the search results from
|
|
the DOM resp: requests response object"""
|
|
|
|
dom = html.fromstring(resp.text)
|
|
if not is_vpn_connected(dom):
|
|
raise SearxEngineResponseException('Not connected to Mullvad VPN')
|
|
search_results = eval_xpath(dom.body, '//main/div[2]/div')
|
|
return list(extract_results(search_results))
|
|
|
|
|
|
def fetch_traits(engine_traits: EngineTraits):
|
|
"""Fetch languages and regions from Mullvad-Leta
|
|
|
|
.. warning::
|
|
|
|
Fetching the engine traits also requires a Mullvad VPN connection. If
|
|
not connected, then an error message will print and no traits will be
|
|
updated.
|
|
"""
|
|
# pylint: disable=import-outside-toplevel
|
|
# see https://github.com/searxng/searxng/issues/762
|
|
from searx.network import post as http_post
|
|
|
|
# pylint: enable=import-outside-toplevel
|
|
resp = http_post(search_url, headers=assign_headers({}))
|
|
if not isinstance(resp, Response):
|
|
print("ERROR: failed to get response from mullvad-leta. Are you connected to the VPN?")
|
|
return
|
|
if not resp.ok:
|
|
print("ERROR: response from mullvad-leta is not OK. Are you connected to the VPN?")
|
|
return
|
|
dom = html.fromstring(resp.text)
|
|
if not is_vpn_connected(dom):
|
|
print('ERROR: Not connected to Mullvad VPN')
|
|
return
|
|
# supported region codes
|
|
options = eval_xpath_list(dom.body, '//main/div/form/div[2]/div/select[1]/option')
|
|
if options is None or len(options) <= 0:
|
|
print('ERROR: could not find any results. Are you connected to the VPN?')
|
|
for x in options:
|
|
eng_country = x.get("value")
|
|
|
|
sxng_locales = get_official_locales(eng_country, engine_traits.languages.keys(), regional=True)
|
|
|
|
if not sxng_locales:
|
|
print(
|
|
"ERROR: can't map from Mullvad-Leta country %s (%s) to a babel region."
|
|
% (x.get('data-name'), eng_country)
|
|
)
|
|
continue
|
|
|
|
for sxng_locale in sxng_locales:
|
|
engine_traits.regions[region_tag(sxng_locale)] = eng_country
|