mirror of
https://github.com/searxng/searxng.git
synced 2025-02-20 12:20:04 +00:00
Compare commits
42 Commits
52dbf20a9e
...
9e02ba83f9
Author | SHA1 | Date | |
---|---|---|---|
|
9e02ba83f9 | ||
|
738906358b | ||
|
fc8938c968 | ||
|
3b9e06fbd2 | ||
|
4934922156 | ||
|
bee39e4ec0 | ||
|
3f4e0b0859 | ||
|
a235c54f8c | ||
|
df3344e5d5 | ||
|
906b9e7d4c | ||
|
36a1ef1239 | ||
|
edfbf1e118 | ||
|
9079d0cac0 | ||
|
70f1b65008 | ||
|
06f6ee4e36 | ||
|
9beff8212b | ||
|
cf4e183790 | ||
|
c1fcee9d9f | ||
|
176079977d | ||
|
f3f13519ff | ||
|
04a6ab12fb | ||
|
edf6d96625 | ||
|
fa50324dae | ||
|
bee2677929 | ||
|
e7081bb2c1 | ||
|
1fde000499 | ||
|
8731e37796 | ||
|
e92d1bc6af | ||
|
f766faca3f | ||
|
c020a964e4 | ||
|
46b16e6ff1 | ||
|
98c66c0ae6 | ||
|
c06ec65b2a | ||
|
e581921c92 | ||
|
073d9549a0 | ||
|
601ffcb8a3 | ||
|
d9115b8d48 | ||
|
c760ad0808 | ||
|
2f087a3a22 | ||
|
3333d9f385 | ||
|
1a885b70ce | ||
|
4d28669beb |
6
Makefile
6
Makefile
@ -50,8 +50,8 @@ search.checker.%: install
|
||||
$(Q)./manage pyenv.cmd searxng-checker -v "$(subst _, ,$(patsubst search.checker.%,%,$@))"
|
||||
|
||||
PHONY += test ci.test test.shell
|
||||
ci.test: test.yamllint test.black test.pyright test.pylint test.unit test.robot test.rst test.pybabel test.themes
|
||||
test: test.yamllint test.black test.pyright test.pylint test.unit test.robot test.rst test.shell
|
||||
ci.test: test.yamllint test.black test.types.ci test.pylint test.unit test.robot test.rst test.shell test.pybabel test.themes
|
||||
test: test.yamllint test.black test.types.dev test.pylint test.unit test.robot test.rst test.shell
|
||||
test.shell:
|
||||
$(Q)shellcheck -x -s dash \
|
||||
dockerfiles/docker-entrypoint.sh
|
||||
@ -83,7 +83,7 @@ MANAGE += node.env node.env.dev node.clean
|
||||
MANAGE += py.build py.clean
|
||||
MANAGE += pyenv pyenv.install pyenv.uninstall
|
||||
MANAGE += format.python
|
||||
MANAGE += test.yamllint test.pylint test.pyright test.black test.pybabel test.unit test.coverage test.robot test.rst test.clean test.themes
|
||||
MANAGE += test.yamllint test.pylint test.black test.pybabel test.unit test.coverage test.robot test.rst test.clean test.themes test.types.dev test.types.ci
|
||||
MANAGE += themes.all themes.fix themes.test
|
||||
MANAGE += themes.simple themes.simple.pygments themes.simple.fix
|
||||
MANAGE += static.build.commit static.build.drop static.build.restore
|
||||
|
@ -1,12 +1,14 @@
|
||||
.. _plugins generic:
|
||||
.. _plugins admin:
|
||||
|
||||
===============
|
||||
Plugins builtin
|
||||
List of plugins
|
||||
===============
|
||||
|
||||
.. sidebar:: Further reading ..
|
||||
|
||||
- :ref:`SearXNG settings <settings plugins>`
|
||||
- :ref:`dev plugin`
|
||||
- :ref:`builtin plugins`
|
||||
|
||||
Configuration defaults (at built time):
|
||||
|
||||
@ -25,15 +27,10 @@ Configuration defaults (at built time):
|
||||
- DO
|
||||
- Description
|
||||
|
||||
JS & CSS dependencies
|
||||
{% for plg in plugins %}
|
||||
|
||||
{% for plgin in plugins %}
|
||||
|
||||
* - {{plgin.name}}
|
||||
- {{(plgin.default_on and "y") or ""}}
|
||||
- {{plgin.description}}
|
||||
|
||||
{% for dep in (plgin.js_dependencies + plgin.css_dependencies) %}
|
||||
| ``{{dep}}`` {% endfor %}
|
||||
* - {{plg.info.name}}
|
||||
- {{(plg.default_on and "y") or ""}}
|
||||
- {{plg.info.description}}
|
||||
|
||||
{% endfor %}
|
||||
|
@ -22,6 +22,6 @@ Settings
|
||||
settings_redis
|
||||
settings_outgoing
|
||||
settings_categories_as_tabs
|
||||
|
||||
settings_plugins
|
||||
|
||||
|
||||
|
67
docs/admin/settings/settings_plugins.rst
Normal file
67
docs/admin/settings/settings_plugins.rst
Normal file
@ -0,0 +1,67 @@
|
||||
.. _settings plugins:
|
||||
|
||||
=======
|
||||
Plugins
|
||||
=======
|
||||
|
||||
.. sidebar:: Further reading ..
|
||||
|
||||
- :ref:`plugins admin`
|
||||
- :ref:`dev plugin`
|
||||
- :ref:`builtin plugins`
|
||||
|
||||
|
||||
The built-in plugins can be activated or deactivated via the settings
|
||||
(:ref:`settings enabled_plugins`) and external plugins can be integrated into
|
||||
SearXNG (:ref:`settings external_plugins`).
|
||||
|
||||
|
||||
.. _settings enabled_plugins:
|
||||
|
||||
``enabled_plugins:`` (internal)
|
||||
===============================
|
||||
|
||||
In :ref:`plugins admin` you find a complete list of all plugins, the default
|
||||
configuration looks like:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
enabled_plugins:
|
||||
- 'Basic Calculator'
|
||||
- 'Hash plugin'
|
||||
- 'Self Information'
|
||||
- 'Tracker URL remover'
|
||||
- 'Unit converter plugin'
|
||||
- 'Ahmia blacklist'
|
||||
|
||||
|
||||
.. _settings external_plugins:
|
||||
|
||||
``plugins:`` (external)
|
||||
=======================
|
||||
|
||||
SearXNG supports *external plugins* / there is no need to install one, SearXNG
|
||||
runs out of the box. But to demonstrate; in the example below we install the
|
||||
SearXNG plugins from *The Green Web Foundation* `[ref]
|
||||
<https://www.thegreenwebfoundation.org/news/searching-the-green-web-with-searx/>`__:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ sudo utils/searxng.sh instance cmd bash -c
|
||||
(searxng-pyenv)$ pip install git+https://github.com/return42/tgwf-searx-plugins
|
||||
|
||||
In the :ref:`settings.yml` activate the ``plugins:`` section and add module
|
||||
``only_show_green_results`` from ``tgwf-searx-plugins``.
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
plugins:
|
||||
- only_show_green_results
|
||||
# - mypackage.mymodule.MyPlugin
|
||||
# - mypackage.mymodule.MyOtherPlugin
|
||||
|
||||
.. hint::
|
||||
|
||||
``only_show_green_results`` is an old plugin that was still implemented in
|
||||
the old style. There is a legacy treatment for backward compatibility, but
|
||||
new plugins should be implemented as a :py:obj:`searx.plugins.Plugin` class.
|
@ -33,14 +33,19 @@
|
||||
``autocomplete``:
|
||||
Existing autocomplete backends, leave blank to turn it off.
|
||||
|
||||
- ``dbpedia``
|
||||
- ``duckduckgo``
|
||||
- ``google``
|
||||
- ``mwmbl``
|
||||
- ``startpage``
|
||||
- ``swisscows``
|
||||
- ``qwant``
|
||||
- ``wikipedia``
|
||||
- ``baidu```
|
||||
- ``brave```
|
||||
- ``dbpedia```
|
||||
- ``duckduckgo```
|
||||
- ``google```
|
||||
- ``mwmbl```
|
||||
- ``qwant```
|
||||
- ``seznam```
|
||||
- ``startpage```
|
||||
- ``stract```
|
||||
- ``swisscows```
|
||||
- ``wikipedia```
|
||||
- ``yandex```
|
||||
|
||||
``favicon_resolver``:
|
||||
To activate favicons in SearXNG's result list select a default
|
||||
|
@ -54,7 +54,7 @@ searx.engines.load_engines(searx.settings['engines'])
|
||||
jinja_contexts = {
|
||||
'searx': {
|
||||
'engines': searx.engines.engines,
|
||||
'plugins': searx.plugins.plugins,
|
||||
'plugins': searx.plugins.STORAGE,
|
||||
'version': {
|
||||
'node': os.getenv('NODE_MINIMUM_VERSION')
|
||||
},
|
||||
@ -129,8 +129,9 @@ extensions = [
|
||||
'notfound.extension', # https://github.com/readthedocs/sphinx-notfound-page
|
||||
]
|
||||
|
||||
# autodoc_typehints = "description"
|
||||
autodoc_default_options = {
|
||||
'member-order': 'groupwise',
|
||||
'member-order': 'bysource',
|
||||
}
|
||||
|
||||
myst_enable_extensions = [
|
||||
|
11
docs/dev/answerers/builtins.rst
Normal file
11
docs/dev/answerers/builtins.rst
Normal file
@ -0,0 +1,11 @@
|
||||
.. _builtin answerers:
|
||||
|
||||
==================
|
||||
Built-in Answerers
|
||||
==================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
random
|
||||
statistics
|
7
docs/dev/answerers/development.rst
Normal file
7
docs/dev/answerers/development.rst
Normal file
@ -0,0 +1,7 @@
|
||||
.. _dev answerers:
|
||||
|
||||
====================
|
||||
Answerer Development
|
||||
====================
|
||||
|
||||
.. automodule:: searx.answerers
|
9
docs/dev/answerers/index.rst
Normal file
9
docs/dev/answerers/index.rst
Normal file
@ -0,0 +1,9 @@
|
||||
=========
|
||||
Answerers
|
||||
=========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
development
|
||||
builtins
|
8
docs/dev/answerers/random.rst
Normal file
8
docs/dev/answerers/random.rst
Normal file
@ -0,0 +1,8 @@
|
||||
.. _answerer.random:
|
||||
|
||||
======
|
||||
Random
|
||||
======
|
||||
|
||||
.. autoclass:: searx.answerers.random.SXNGAnswerer
|
||||
:members:
|
8
docs/dev/answerers/statistics.rst
Normal file
8
docs/dev/answerers/statistics.rst
Normal file
@ -0,0 +1,8 @@
|
||||
.. _answerer.statistics:
|
||||
|
||||
==========
|
||||
Statistics
|
||||
==========
|
||||
|
||||
.. autoclass:: searx.answerers.statistics.SXNGAnswerer
|
||||
:members:
|
@ -237,335 +237,18 @@ following parameters can be used to specify a search request:
|
||||
=================== =========== ==========================================================================
|
||||
|
||||
|
||||
.. _engine results:
|
||||
.. _engine media types:
|
||||
Making a Response
|
||||
=================
|
||||
|
||||
Result Types (``template``)
|
||||
===========================
|
||||
In the ``response`` function of the engine, the HTTP response (``resp``) is
|
||||
parsed and a list of results is returned.
|
||||
|
||||
Each result item of an engine can be of different media-types. Currently the
|
||||
following media-types are supported. To set another media-type as
|
||||
:ref:`template default`, the parameter ``template`` must be set to the desired
|
||||
type.
|
||||
A engine can append result-items of different media-types and different
|
||||
result-types to the result list. The list of the result items is render to HTML
|
||||
by templates. For more details read section:
|
||||
|
||||
.. _template default:
|
||||
- :ref:`simple theme templates`
|
||||
- :ref:`result types`
|
||||
|
||||
``default``
|
||||
-----------
|
||||
|
||||
.. table:: Parameter of the **default** media type:
|
||||
:width: 100%
|
||||
|
||||
========================= =====================================================
|
||||
result-parameter information
|
||||
========================= =====================================================
|
||||
url string, url of the result
|
||||
title string, title of the result
|
||||
content string, general result-text
|
||||
publishedDate :py:class:`datetime.datetime`, time of publish
|
||||
========================= =====================================================
|
||||
|
||||
|
||||
.. _template images:
|
||||
|
||||
``images``
|
||||
----------
|
||||
|
||||
.. list-table:: Parameter of the **images** media type
|
||||
:header-rows: 2
|
||||
:width: 100%
|
||||
|
||||
* - result-parameter
|
||||
- Python type
|
||||
- information
|
||||
|
||||
* - template
|
||||
- :py:class:`str`
|
||||
- is set to ``images.html``
|
||||
|
||||
* - url
|
||||
- :py:class:`str`
|
||||
- url to the result site
|
||||
|
||||
* - title
|
||||
- :py:class:`str`
|
||||
- title of the result
|
||||
|
||||
* - content
|
||||
- :py:class:`str`
|
||||
- description of the image
|
||||
|
||||
* - publishedDate
|
||||
- :py:class:`datetime <datetime.datetime>`
|
||||
- time of publish
|
||||
|
||||
* - img_src
|
||||
- :py:class:`str`
|
||||
- url to the result image
|
||||
|
||||
* - thumbnail_src
|
||||
- :py:class:`str`
|
||||
- url to a small-preview image
|
||||
|
||||
* - resolution
|
||||
- :py:class:`str`
|
||||
- the resolution of the image (e.g. ``1920 x 1080`` pixel)
|
||||
|
||||
* - img_format
|
||||
- :py:class:`str`
|
||||
- the format of the image (e.g. ``png``)
|
||||
|
||||
* - filesize
|
||||
- :py:class:`str`
|
||||
- size of bytes in :py:obj:`human readable <searx.humanize_bytes>` notation
|
||||
(e.g. ``MB`` for 1024 \* 1024 Bytes filesize).
|
||||
|
||||
|
||||
.. _template videos:
|
||||
|
||||
``videos``
|
||||
----------
|
||||
|
||||
.. table:: Parameter of the **videos** media type:
|
||||
:width: 100%
|
||||
|
||||
========================= =====================================================
|
||||
result-parameter information
|
||||
------------------------- -----------------------------------------------------
|
||||
template is set to ``videos.html``
|
||||
========================= =====================================================
|
||||
url string, url of the result
|
||||
title string, title of the result
|
||||
content *(not implemented yet)*
|
||||
publishedDate :py:class:`datetime.datetime`, time of publish
|
||||
thumbnail string, url to a small-preview image
|
||||
length :py:class:`datetime.timedelta`, duration of result
|
||||
views string, view count in humanized number format
|
||||
========================= =====================================================
|
||||
|
||||
|
||||
.. _template torrent:
|
||||
|
||||
``torrent``
|
||||
-----------
|
||||
|
||||
.. _magnetlink: https://en.wikipedia.org/wiki/Magnet_URI_scheme
|
||||
|
||||
.. table:: Parameter of the **torrent** media type:
|
||||
:width: 100%
|
||||
|
||||
========================= =====================================================
|
||||
result-parameter information
|
||||
------------------------- -----------------------------------------------------
|
||||
template is set to ``torrent.html``
|
||||
========================= =====================================================
|
||||
url string, url of the result
|
||||
title string, title of the result
|
||||
content string, general result-text
|
||||
publishedDate :py:class:`datetime.datetime`,
|
||||
time of publish *(not implemented yet)*
|
||||
seed int, number of seeder
|
||||
leech int, number of leecher
|
||||
filesize int, size of file in bytes
|
||||
files int, number of files
|
||||
magnetlink string, magnetlink_ of the result
|
||||
torrentfile string, torrentfile of the result
|
||||
========================= =====================================================
|
||||
|
||||
|
||||
.. _template map:
|
||||
|
||||
``map``
|
||||
-------
|
||||
|
||||
.. table:: Parameter of the **map** media type:
|
||||
:width: 100%
|
||||
|
||||
========================= =====================================================
|
||||
result-parameter information
|
||||
------------------------- -----------------------------------------------------
|
||||
template is set to ``map.html``
|
||||
========================= =====================================================
|
||||
url string, url of the result
|
||||
title string, title of the result
|
||||
content string, general result-text
|
||||
publishedDate :py:class:`datetime.datetime`, time of publish
|
||||
latitude latitude of result (in decimal format)
|
||||
longitude longitude of result (in decimal format)
|
||||
boundingbox boundingbox of result (array of 4. values
|
||||
``[lat-min, lat-max, lon-min, lon-max]``)
|
||||
geojson geojson of result (https://geojson.org/)
|
||||
osm.type type of osm-object (if OSM-Result)
|
||||
osm.id id of osm-object (if OSM-Result)
|
||||
address.name name of object
|
||||
address.road street name of object
|
||||
address.house_number house number of object
|
||||
address.locality city, place of object
|
||||
address.postcode postcode of object
|
||||
address.country country of object
|
||||
========================= =====================================================
|
||||
|
||||
|
||||
.. _template paper:
|
||||
|
||||
``paper``
|
||||
---------
|
||||
|
||||
.. _BibTeX format: https://www.bibtex.com/g/bibtex-format/
|
||||
.. _BibTeX field types: https://en.wikipedia.org/wiki/BibTeX#Field_types
|
||||
|
||||
.. list-table:: Parameter of the **paper** media type /
|
||||
see `BibTeX field types`_ and `BibTeX format`_
|
||||
:header-rows: 2
|
||||
:width: 100%
|
||||
|
||||
* - result-parameter
|
||||
- Python type
|
||||
- information
|
||||
|
||||
* - template
|
||||
- :py:class:`str`
|
||||
- is set to ``paper.html``
|
||||
|
||||
* - title
|
||||
- :py:class:`str`
|
||||
- title of the result
|
||||
|
||||
* - content
|
||||
- :py:class:`str`
|
||||
- abstract
|
||||
|
||||
* - comments
|
||||
- :py:class:`str`
|
||||
- free text display in italic below the content
|
||||
|
||||
* - tags
|
||||
- :py:class:`List <list>`\ [\ :py:class:`str`\ ]
|
||||
- free tag list
|
||||
|
||||
* - publishedDate
|
||||
- :py:class:`datetime <datetime.datetime>`
|
||||
- last publication date
|
||||
|
||||
* - type
|
||||
- :py:class:`str`
|
||||
- short description of medium type, e.g. *book*, *pdf* or *html* ...
|
||||
|
||||
* - authors
|
||||
- :py:class:`List <list>`\ [\ :py:class:`str`\ ]
|
||||
- list of authors of the work (authors with a "s")
|
||||
|
||||
* - editor
|
||||
- :py:class:`str`
|
||||
- list of editors of a book
|
||||
|
||||
* - publisher
|
||||
- :py:class:`str`
|
||||
- name of the publisher
|
||||
|
||||
* - journal
|
||||
- :py:class:`str`
|
||||
- name of the journal or magazine the article was
|
||||
published in
|
||||
|
||||
* - volume
|
||||
- :py:class:`str`
|
||||
- volume number
|
||||
|
||||
* - pages
|
||||
- :py:class:`str`
|
||||
- page range where the article is
|
||||
|
||||
* - number
|
||||
- :py:class:`str`
|
||||
- number of the report or the issue number for a journal article
|
||||
|
||||
* - doi
|
||||
- :py:class:`str`
|
||||
- DOI number (like ``10.1038/d41586-018-07848-2``)
|
||||
|
||||
* - issn
|
||||
- :py:class:`List <list>`\ [\ :py:class:`str`\ ]
|
||||
- ISSN number like ``1476-4687``
|
||||
|
||||
* - isbn
|
||||
- :py:class:`List <list>`\ [\ :py:class:`str`\ ]
|
||||
- ISBN number like ``9780201896831``
|
||||
|
||||
* - pdf_url
|
||||
- :py:class:`str`
|
||||
- URL to the full article, the PDF version
|
||||
|
||||
* - html_url
|
||||
- :py:class:`str`
|
||||
- URL to full article, HTML version
|
||||
|
||||
|
||||
.. _template packages:
|
||||
|
||||
``packages``
|
||||
------------
|
||||
|
||||
.. list-table:: Parameter of the **packages** media type
|
||||
:header-rows: 2
|
||||
:width: 100%
|
||||
|
||||
* - result-parameter
|
||||
- Python type
|
||||
- information
|
||||
|
||||
* - template
|
||||
- :py:class:`str`
|
||||
- is set to ``packages.html``
|
||||
|
||||
* - title
|
||||
- :py:class:`str`
|
||||
- title of the result
|
||||
|
||||
* - content
|
||||
- :py:class:`str`
|
||||
- abstract
|
||||
|
||||
* - package_name
|
||||
- :py:class:`str`
|
||||
- the name of the package
|
||||
|
||||
* - version
|
||||
- :py:class:`str`
|
||||
- the current version of the package
|
||||
|
||||
* - maintainer
|
||||
- :py:class:`str`
|
||||
- the maintainer or author of the project
|
||||
|
||||
* - publishedDate
|
||||
- :py:class:`datetime <datetime.datetime>`
|
||||
- date of latest update or release
|
||||
|
||||
* - tags
|
||||
- :py:class:`List <list>`\ [\ :py:class:`str`\ ]
|
||||
- free tag list
|
||||
|
||||
* - popularity
|
||||
- :py:class:`str`
|
||||
- the popularity of the package, e.g. rating or download count
|
||||
|
||||
* - license_name
|
||||
- :py:class:`str`
|
||||
- the name of the license
|
||||
|
||||
* - license_url
|
||||
- :py:class:`str`
|
||||
- the web location of a license copy
|
||||
|
||||
* - homepage
|
||||
- :py:class:`str`
|
||||
- the url of the project's homepage
|
||||
|
||||
* - source_code_url
|
||||
- :py:class:`str`
|
||||
- the location of the project's source code
|
||||
|
||||
* - links
|
||||
- :py:class:`dict`
|
||||
- additional links in the form of ``{'link_name': 'http://example.com'}``
|
||||
|
@ -19,6 +19,14 @@ Engine Implementations
|
||||
engine_overview
|
||||
|
||||
|
||||
ResultList and engines
|
||||
======================
|
||||
|
||||
.. autoclass:: searx.result_types.ResultList
|
||||
|
||||
.. autoclass:: searx.result_types.EngineResults
|
||||
|
||||
|
||||
Engine Types
|
||||
============
|
||||
|
||||
|
7
docs/dev/extended_types.rst
Normal file
7
docs/dev/extended_types.rst
Normal file
@ -0,0 +1,7 @@
|
||||
.. _extended_types.:
|
||||
|
||||
==============
|
||||
Extended Types
|
||||
==============
|
||||
|
||||
.. automodule:: searx.extended_types
|
@ -8,9 +8,13 @@ Developer documentation
|
||||
quickstart
|
||||
rtm_asdf
|
||||
contribution_guide
|
||||
extended_types
|
||||
engines/index
|
||||
result_types/index
|
||||
templates
|
||||
search_api
|
||||
plugins
|
||||
plugins/index
|
||||
answerers/index
|
||||
translation
|
||||
lxcdev
|
||||
makefile
|
||||
|
@ -1,106 +0,0 @@
|
||||
.. _dev plugin:
|
||||
|
||||
=======
|
||||
Plugins
|
||||
=======
|
||||
|
||||
.. sidebar:: Further reading ..
|
||||
|
||||
- :ref:`plugins generic`
|
||||
|
||||
Plugins can extend or replace functionality of various components of searx.
|
||||
|
||||
Example plugin
|
||||
==============
|
||||
|
||||
.. code:: python
|
||||
|
||||
name = 'Example plugin'
|
||||
description = 'This plugin extends the suggestions with the word "example"'
|
||||
default_on = False # disabled by default
|
||||
|
||||
# attach callback to the post search hook
|
||||
# request: flask request object
|
||||
# ctx: the whole local context of the post search hook
|
||||
def post_search(request, search):
|
||||
search.result_container.suggestions.add('example')
|
||||
return True
|
||||
|
||||
External plugins
|
||||
================
|
||||
|
||||
SearXNG supports *external plugins* / there is no need to install one, SearXNG
|
||||
runs out of the box. But to demonstrate; in the example below we install the
|
||||
SearXNG plugins from *The Green Web Foundation* `[ref]
|
||||
<https://www.thegreenwebfoundation.org/news/searching-the-green-web-with-searx/>`__:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ sudo utils/searxng.sh instance cmd bash -c
|
||||
(searxng-pyenv)$ pip install git+https://github.com/return42/tgwf-searx-plugins
|
||||
|
||||
In the :ref:`settings.yml` activate the ``plugins:`` section and add module
|
||||
``only_show_green_results`` from ``tgwf-searx-plugins``.
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
plugins:
|
||||
...
|
||||
- only_show_green_results
|
||||
...
|
||||
|
||||
|
||||
Plugin entry points
|
||||
===================
|
||||
|
||||
Entry points (hooks) define when a plugin runs. Right now only three hooks are
|
||||
implemented. So feel free to implement a hook if it fits the behaviour of your
|
||||
plugin. A plugin doesn't need to implement all the hooks.
|
||||
|
||||
|
||||
.. py:function:: pre_search(request, search) -> bool
|
||||
|
||||
Runs BEFORE the search request.
|
||||
|
||||
`search.result_container` can be changed.
|
||||
|
||||
Return a boolean:
|
||||
|
||||
* True to continue the search
|
||||
* False to stop the search
|
||||
|
||||
:param flask.request request:
|
||||
:param searx.search.SearchWithPlugins search:
|
||||
:return: False to stop the search
|
||||
:rtype: bool
|
||||
|
||||
|
||||
.. py:function:: post_search(request, search) -> None
|
||||
|
||||
Runs AFTER the search request.
|
||||
|
||||
:param flask.request request: Flask request.
|
||||
:param searx.search.SearchWithPlugins search: Context.
|
||||
|
||||
|
||||
.. py:function:: on_result(request, search, result) -> bool
|
||||
|
||||
Runs for each result of each engine.
|
||||
|
||||
`result` can be changed.
|
||||
|
||||
If `result["url"]` is defined, then `result["parsed_url"] = urlparse(result['url'])`
|
||||
|
||||
.. warning::
|
||||
`result["url"]` can be changed, but `result["parsed_url"]` must be updated too.
|
||||
|
||||
Return a boolean:
|
||||
|
||||
* True to keep the result
|
||||
* False to remove the result
|
||||
|
||||
:param flask.request request:
|
||||
:param searx.search.SearchWithPlugins search:
|
||||
:param typing.Dict result: Result, see - :ref:`engine results`
|
||||
:return: True to keep the result
|
||||
:rtype: bool
|
15
docs/dev/plugins/builtins.rst
Normal file
15
docs/dev/plugins/builtins.rst
Normal file
@ -0,0 +1,15 @@
|
||||
.. _builtin plugins:
|
||||
|
||||
================
|
||||
Built-in Plugins
|
||||
================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
calculator
|
||||
hash_plugin
|
||||
hostnames
|
||||
self_info
|
||||
tor_check
|
||||
unit_converter
|
8
docs/dev/plugins/calculator.rst
Normal file
8
docs/dev/plugins/calculator.rst
Normal file
@ -0,0 +1,8 @@
|
||||
.. _plugins.calculator:
|
||||
|
||||
==========
|
||||
Calculator
|
||||
==========
|
||||
|
||||
.. automodule:: searx.plugins.calculator
|
||||
:members:
|
7
docs/dev/plugins/development.rst
Normal file
7
docs/dev/plugins/development.rst
Normal file
@ -0,0 +1,7 @@
|
||||
.. _dev plugin:
|
||||
|
||||
==================
|
||||
Plugin Development
|
||||
==================
|
||||
|
||||
.. automodule:: searx.plugins
|
8
docs/dev/plugins/hash_plugin.rst
Normal file
8
docs/dev/plugins/hash_plugin.rst
Normal file
@ -0,0 +1,8 @@
|
||||
.. _hash_plugin plugin:
|
||||
|
||||
===========
|
||||
Hash Values
|
||||
===========
|
||||
|
||||
.. autoclass:: searx.plugins.hash_plugin.SXNGPlugin
|
||||
:members:
|
@ -1,9 +1,8 @@
|
||||
.. _hostnames plugin:
|
||||
|
||||
================
|
||||
Hostnames plugin
|
||||
================
|
||||
=========
|
||||
Hostnames
|
||||
=========
|
||||
|
||||
.. automodule:: searx.plugins.hostnames
|
||||
:members:
|
||||
|
||||
:members:
|
9
docs/dev/plugins/index.rst
Normal file
9
docs/dev/plugins/index.rst
Normal file
@ -0,0 +1,9 @@
|
||||
=======
|
||||
Plugins
|
||||
=======
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
development
|
||||
builtins
|
8
docs/dev/plugins/self_info.rst
Normal file
8
docs/dev/plugins/self_info.rst
Normal file
@ -0,0 +1,8 @@
|
||||
.. _self_info plugin:
|
||||
|
||||
=========
|
||||
Self-Info
|
||||
=========
|
||||
|
||||
.. autoclass:: searx.plugins.self_info.SXNGPlugin
|
||||
:members:
|
@ -1,9 +1,8 @@
|
||||
.. _tor check plugin:
|
||||
|
||||
================
|
||||
Tor check plugin
|
||||
================
|
||||
=========
|
||||
Tor check
|
||||
=========
|
||||
|
||||
.. automodule:: searx.plugins.tor_check
|
||||
:members:
|
||||
|
||||
:members:
|
@ -1,9 +1,8 @@
|
||||
.. _unit converter plugin:
|
||||
|
||||
=====================
|
||||
Unit converter plugin
|
||||
=====================
|
||||
==============
|
||||
Unit Converter
|
||||
==============
|
||||
|
||||
.. automodule:: searx.plugins.unit_converter
|
||||
:members:
|
||||
|
7
docs/dev/result_types/answer.rst
Normal file
7
docs/dev/result_types/answer.rst
Normal file
@ -0,0 +1,7 @@
|
||||
.. _result_types.answer:
|
||||
|
||||
==============
|
||||
Answer Results
|
||||
==============
|
||||
|
||||
.. automodule:: searx.result_types.answer
|
5
docs/dev/result_types/base_result.rst
Normal file
5
docs/dev/result_types/base_result.rst
Normal file
@ -0,0 +1,5 @@
|
||||
======
|
||||
Result
|
||||
======
|
||||
|
||||
.. automodule:: searx.result_types._base
|
34
docs/dev/result_types/correction.rst
Normal file
34
docs/dev/result_types/correction.rst
Normal file
@ -0,0 +1,34 @@
|
||||
.. _result_types.corrections:
|
||||
|
||||
==================
|
||||
Correction Results
|
||||
==================
|
||||
|
||||
.. hint::
|
||||
|
||||
There is still no typing for these result items. The templates can be used as
|
||||
orientation until the final typing is complete.
|
||||
|
||||
The corrections area shows the user alternative search terms.
|
||||
|
||||
A result of this type is a very simple dictionary with only one key/value pair
|
||||
|
||||
.. code:: python
|
||||
|
||||
{"correction" : "lorem ipsum .."}
|
||||
|
||||
From this simple dict another dict is build up:
|
||||
|
||||
.. code:: python
|
||||
|
||||
# use RawTextQuery to get the corrections URLs with the same bang
|
||||
{"url" : "!bang lorem ipsum ..", "title": "lorem ipsum .." }
|
||||
|
||||
and used in the template :origin:`corrections.html
|
||||
<searx/templates/simple/elements/corrections.html>`:
|
||||
|
||||
title : :py:class:`str`
|
||||
Corrected search term.
|
||||
|
||||
url : :py:class:`str`
|
||||
Not really an URL, its the value to insert in a HTML form for a SearXNG query.
|
95
docs/dev/result_types/index.rst
Normal file
95
docs/dev/result_types/index.rst
Normal file
@ -0,0 +1,95 @@
|
||||
.. _result types:
|
||||
|
||||
============
|
||||
Result Types
|
||||
============
|
||||
|
||||
To understand the typification of the results, let's take a brief look at the
|
||||
structure of SearXNG .. At its core, SearXNG is nothing more than an aggregator
|
||||
that aggregates the results from various sources, renders them via templates and
|
||||
displays them to the user.
|
||||
|
||||
The **sources** can be:
|
||||
|
||||
1. :ref:`engines <engine implementations>`
|
||||
2. :ref:`plugins <dev plugin>`
|
||||
3. :ref:`answerers <dev answerers>`
|
||||
|
||||
The sources provide the results, which are displayed in different **areas**
|
||||
depending on the type of result. The areas are:
|
||||
|
||||
main results:
|
||||
It is the main area in which -- as is typical for search engines -- the
|
||||
results that a search engine has found for the search term are displayed.
|
||||
|
||||
answers:
|
||||
This area displays short answers that could be found for the search term.
|
||||
|
||||
info box:
|
||||
An area in which additional information can be displayed, e.g. excerpts from
|
||||
wikipedia or other sources such as maps.
|
||||
|
||||
suggestions:
|
||||
Suggestions for alternative search terms can be found in this area. These can
|
||||
be clicked on and a search is carried out with these search terms.
|
||||
|
||||
corrections:
|
||||
Results in this area are like the suggestion of alternative search terms,
|
||||
which usually result from spelling corrections
|
||||
|
||||
At this point it is important to note that all **sources** can contribute
|
||||
results to all of the areas mentioned above.
|
||||
|
||||
In most cases, however, the :ref:`engines <engine implementations>` will fill
|
||||
the *main results* and the :ref:`answerers <dev answerers>` will generally
|
||||
provide the contributions for the *answer* area. Not necessary to mention here
|
||||
but for a better understanding: the plugins can also filter out or change
|
||||
results from the main results area (e.g. the URL of the link).
|
||||
|
||||
The result items are organized in the :py:obj:`results.ResultContainer` and
|
||||
after all sources have delivered their results, this container is passed to the
|
||||
templating to build a HTML output. The output is usually HTML, but it is also
|
||||
possible to output the result lists as JSON or RSS feed. Thats quite all we need
|
||||
to know before we dive into typification of result items.
|
||||
|
||||
.. hint::
|
||||
|
||||
Typification of result items: we are at the very first beginng!
|
||||
|
||||
The first thing we have to realize is that there is no typification of the
|
||||
result items so far, we have to build it up first .. and that is quite a big
|
||||
task, which we will only be able to accomplish gradually.
|
||||
|
||||
The foundation for the typeless results was laid back in 2013 in the very first
|
||||
commit :commit:`ae9fb1d7d`, and the principle has not changed since then. At
|
||||
the time, the approach was perfectly adequate, but we have since evolved and the
|
||||
demands on SearXNG increase with every feature request.
|
||||
|
||||
**Motivation:** in the meantime, it has become very difficult to develop new
|
||||
features that require structural changes and it is especially hard for newcomers
|
||||
to find their way in this typeless world. As long as the results are only
|
||||
simple key/value dictionaries, it is not even possible for the IDEs to support
|
||||
the application developer in his work.
|
||||
|
||||
**Planning:** The procedure for subsequent typing will have to be based on the
|
||||
circumstances ..
|
||||
|
||||
.. attention::
|
||||
|
||||
As long as there is no type defined for a kind of result the HTML template
|
||||
specify what the properties of a type are.
|
||||
|
||||
In this sense, you will either find a type definition here in the
|
||||
documentation or, if this does not yet exist, a description of the HTML
|
||||
template.
|
||||
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
base_result
|
||||
main_result
|
||||
answer
|
||||
correction
|
||||
suggestion
|
||||
infobox
|
60
docs/dev/result_types/infobox.rst
Normal file
60
docs/dev/result_types/infobox.rst
Normal file
@ -0,0 +1,60 @@
|
||||
.. _result_types.infobox:
|
||||
|
||||
===============
|
||||
Infobox Results
|
||||
===============
|
||||
|
||||
.. hint::
|
||||
|
||||
There is still no typing for these result items. The templates can be used as
|
||||
orientation until the final typing is complete.
|
||||
|
||||
The infobox is an area where addtional infos shown to the user.
|
||||
|
||||
Fields used in the :origin:`infobox.html
|
||||
<searx/templates/simple/elements/infobox.html>`:
|
||||
|
||||
img_src: :py:class:`str`
|
||||
URL of a image or thumbnail that is displayed in the infobox.
|
||||
|
||||
infobox: :py:class:`str`
|
||||
Title of the info box.
|
||||
|
||||
content: :py:class:`str`
|
||||
Text of the info box.
|
||||
|
||||
The infobox has additional subsections for *attributes*, *urls* and
|
||||
*relatedTopics*:
|
||||
|
||||
attributes: :py:class:`List <list>`\ [\ :py:class:`dict`\ ]
|
||||
A list of attributes. An *attribute* is a dictionary with keys:
|
||||
|
||||
- label :py:class:`str`: (mandatory)
|
||||
|
||||
- value :py:class:`str`: (mandatory)
|
||||
|
||||
- image :py:class:`List <list>`\ [\ :py:class:`dict`\ ] (optional)
|
||||
|
||||
A list of images. An *image* is a dictionary with keys:
|
||||
|
||||
- src :py:class:`str`: URL of an image/thumbnail (mandatory)
|
||||
- alt :py:class:`str`: alternative text for the image (mandatory)
|
||||
|
||||
urls: :py:class:`List <list>`\ [\ :py:class:`dict`\ ]
|
||||
A list of links. An *link* is a dictionary with keys:
|
||||
|
||||
- url :py:class:`str`: URL of the link (mandatory)
|
||||
- title :py:class:`str`: Title of the link (mandatory)
|
||||
|
||||
relatedTopics: :py:class:`List <list>`\ [\ :py:class:`dict`\ ]
|
||||
A list of topics. An *topic* is a dictionary with keys:
|
||||
|
||||
- name: :py:class:`str`: (mandatory)
|
||||
|
||||
- suggestions: :py:class:`List <list>`\ [\ :py:class:`dict`\ ] (optional)
|
||||
|
||||
A list of suggestions. A *suggestion* is simple dictionary with just one
|
||||
key/value pair:
|
||||
|
||||
- suggestion: :py:class:`str`: suggested search term (mandatory)
|
||||
|
17
docs/dev/result_types/main_result.rst
Normal file
17
docs/dev/result_types/main_result.rst
Normal file
@ -0,0 +1,17 @@
|
||||
============
|
||||
Main Results
|
||||
============
|
||||
|
||||
There is still no typing for the items in the :ref:`main result list`. The
|
||||
templates can be used as orientation until the final typing is complete.
|
||||
|
||||
- :ref:`template default`
|
||||
- :ref:`template images`
|
||||
- :ref:`template videos`
|
||||
- :ref:`template torrent`
|
||||
- :ref:`template map`
|
||||
- :ref:`template paper`
|
||||
- :ref:`template packages`
|
||||
- :ref:`template code`
|
||||
- :ref:`template files`
|
||||
- :ref:`template products`
|
38
docs/dev/result_types/suggestion.rst
Normal file
38
docs/dev/result_types/suggestion.rst
Normal file
@ -0,0 +1,38 @@
|
||||
.. _result_types.suggestion:
|
||||
|
||||
==================
|
||||
Suggestion Results
|
||||
==================
|
||||
|
||||
.. hint::
|
||||
|
||||
There is still no typing for these result items. The templates can be used as
|
||||
orientation until the final typing is complete.
|
||||
|
||||
The suggestions area shows the user alternative search terms.
|
||||
|
||||
A result of this type is a very simple dictionary with only one key/value pair
|
||||
|
||||
.. code:: python
|
||||
|
||||
{"suggestion" : "lorem ipsum .."}
|
||||
|
||||
From this simple dict another dict is build up:
|
||||
|
||||
.. code:: python
|
||||
|
||||
{"url" : "!bang lorem ipsum ..", "title": "lorem ipsum" }
|
||||
|
||||
and used in the template :origin:`suggestions.html
|
||||
<searx/templates/simple/elements/suggestions.html>`:
|
||||
|
||||
.. code:: python
|
||||
|
||||
# use RawTextQuery to get the suggestion URLs with the same bang
|
||||
{"url" : "!bang lorem ipsum ..", "title": "lorem ipsum" }
|
||||
|
||||
title : :py:class:`str`
|
||||
Suggested search term
|
||||
|
||||
url : :py:class:`str`
|
||||
Not really an URL, its the value to insert in a HTML form for a SearXNG query.
|
@ -60,6 +60,7 @@ Scripts to update static data in :origin:`searx/data/`
|
||||
.. automodule:: searxng_extra.update.update_engine_traits
|
||||
:members:
|
||||
|
||||
.. _update_osm_keys_tags.py:
|
||||
|
||||
``update_osm_keys_tags.py``
|
||||
===========================
|
||||
|
577
docs/dev/templates.rst
Normal file
577
docs/dev/templates.rst
Normal file
@ -0,0 +1,577 @@
|
||||
.. _simple theme templates:
|
||||
|
||||
======================
|
||||
Simple Theme Templates
|
||||
======================
|
||||
|
||||
The simple template is complex, it consists of many different elements and also
|
||||
uses macros and include statements. The following is a rough overview that we
|
||||
would like to give the developerat hand, details must still be taken from the
|
||||
:origin:`sources <searx/templates/simple/>`.
|
||||
|
||||
A :ref:`result item <result types>` can be of different media types. The media
|
||||
type of a result is defined by the :py:obj:`result_type.Result.template`. To
|
||||
set another media-type as :ref:`template default`, the field ``template``
|
||||
in the result item must be set to the desired type.
|
||||
|
||||
.. contents:: Contents
|
||||
:depth: 2
|
||||
:local:
|
||||
:backlinks: entry
|
||||
|
||||
|
||||
.. _result template macros:
|
||||
|
||||
Result template macros
|
||||
======================
|
||||
|
||||
.. _macro result_header:
|
||||
|
||||
``result_header``
|
||||
-----------------
|
||||
|
||||
Execpt ``image.html`` and some others this macro is used in nearly all result
|
||||
types in the :ref:`main result list`.
|
||||
|
||||
Fields used in the template :origin:`macro result_header
|
||||
<searx/templates/simple/macros.html>`:
|
||||
|
||||
url : :py:class:`str`
|
||||
Link URL of the result item.
|
||||
|
||||
title : :py:class:`str`
|
||||
Link title of the result item.
|
||||
|
||||
img_src, thumbnail : :py:class:`str`
|
||||
URL of a image or thumbnail that is displayed in the result item.
|
||||
|
||||
|
||||
.. _macro result_sub_header:
|
||||
|
||||
``result_sub_header``
|
||||
---------------------
|
||||
|
||||
Execpt ``image.html`` and some others this macro is used in nearly all result
|
||||
types in the :ref:`main result list`.
|
||||
|
||||
Fields used in the template :origin:`macro result_sub_header
|
||||
<searx/templates/simple/macros.html>`:
|
||||
|
||||
publishedDate : :py:obj:`datetime.datetime`
|
||||
The date on which the object was published.
|
||||
|
||||
length: :py:obj:`time.struct_time`
|
||||
Playing duration in seconds.
|
||||
|
||||
views: :py:class:`str`
|
||||
View count in humanized number format.
|
||||
|
||||
author : :py:class:`str`
|
||||
Author of the title.
|
||||
|
||||
metadata : :py:class:`str`
|
||||
Miscellaneous metadata.
|
||||
|
||||
|
||||
.. _engine_data:
|
||||
|
||||
``engine_data_form``
|
||||
--------------------
|
||||
|
||||
The ``engine_data_form`` macro is used in :origin:`results,html
|
||||
<searx/templates/simple/results.html>` in a HTML ``<form/>`` element. The
|
||||
intention of this macro is to pass data of a engine from one :py:obj:`response
|
||||
<searx.engines.demo_online.response>` to the :py:obj:`searx.search.SearchQuery`
|
||||
of the next :py:obj:`request <searx.engines.demo_online.request>`.
|
||||
|
||||
To pass data, engine's response handler can append result items of typ
|
||||
``engine_data``. This is by example used to pass a token from the response to
|
||||
the next request:
|
||||
|
||||
.. code:: python
|
||||
|
||||
def response(resp):
|
||||
...
|
||||
results.append({
|
||||
'engine_data': token,
|
||||
'key': 'next_page_token',
|
||||
})
|
||||
...
|
||||
return results
|
||||
|
||||
def request(query, params):
|
||||
page_token = params['engine_data'].get('next_page_token')
|
||||
|
||||
|
||||
.. _main result list:
|
||||
|
||||
Main Result List
|
||||
================
|
||||
|
||||
The **media types** of the **main result type** are the template files in
|
||||
the :origin:`result_templates <searx/templates/simple/result_templates>`.
|
||||
|
||||
.. _template default:
|
||||
|
||||
``default.html``
|
||||
----------------
|
||||
|
||||
Displays result fields from:
|
||||
|
||||
- :ref:`macro result_header` and
|
||||
- :ref:`macro result_sub_header`
|
||||
|
||||
Additional fields used in the :origin:`default.html
|
||||
<searx/templates/simple/result_templates/default.html>`:
|
||||
|
||||
content : :py:class:`str`
|
||||
General text of the result item.
|
||||
|
||||
iframe_src : :py:class:`str`
|
||||
URL of an embedded ``<iframe>`` / the frame is collapsible.
|
||||
|
||||
audio_src : uri,
|
||||
URL of an embedded ``<audio controls>``.
|
||||
|
||||
|
||||
.. _template images:
|
||||
|
||||
``images.html``
|
||||
---------------
|
||||
|
||||
The images are displayed as small thumbnails in the main results list.
|
||||
|
||||
title : :py:class:`str`
|
||||
Title of the image.
|
||||
|
||||
thumbnail_src : :py:class:`str`
|
||||
URL of a preview of the image.
|
||||
|
||||
resolution :py:class:`str`
|
||||
The resolution of the image (e.g. ``1920 x 1080`` pixel)
|
||||
|
||||
|
||||
Image labels
|
||||
~~~~~~~~~~~~
|
||||
|
||||
Clicking on the preview opens a gallery view in which all further metadata for
|
||||
the image is displayed. Addition fields used in the :origin:`images.html
|
||||
<searx/templates/simple/result_templates/images.html>`:
|
||||
|
||||
img_src : :py:class:`str`
|
||||
URL of the full size image.
|
||||
|
||||
content: :py:class:`str`
|
||||
Description of the image.
|
||||
|
||||
author: :py:class:`str`
|
||||
Name of the author of the image.
|
||||
|
||||
img_format : :py:class:`str`
|
||||
The format of the image (e.g. ``png``).
|
||||
|
||||
source : :py:class:`str`
|
||||
Source of the image.
|
||||
|
||||
filesize: :py:class:`str`
|
||||
Size of bytes in :py:obj:`human readable <searx.humanize_bytes>` notation
|
||||
(e.g. ``MB`` for 1024 \* 1024 Bytes filesize).
|
||||
|
||||
url : :py:class:`str`
|
||||
URL of the page from where the images comes from (source).
|
||||
|
||||
|
||||
.. _template videos:
|
||||
|
||||
``videos.html``
|
||||
---------------
|
||||
|
||||
Displays result fields from:
|
||||
|
||||
- :ref:`macro result_header` and
|
||||
- :ref:`macro result_sub_header`
|
||||
|
||||
Additional fields used in the :origin:`videos.html
|
||||
<searx/templates/simple/result_templates/videos.html>`:
|
||||
|
||||
iframe_src : :py:class:`str`
|
||||
URL of an embedded ``<iframe>`` / the frame is collapsible.
|
||||
|
||||
The videos are displayed as small thumbnails in the main results list, there
|
||||
is an additional button to collaps/open the embeded video.
|
||||
|
||||
content : :py:class:`str`
|
||||
Description of the code fragment.
|
||||
|
||||
|
||||
.. _template torrent:
|
||||
|
||||
``torrent.html``
|
||||
----------------
|
||||
|
||||
.. _magnet link: https://en.wikipedia.org/wiki/Magnet_URI_scheme
|
||||
.. _torrent file: https://en.wikipedia.org/wiki/Torrent_file
|
||||
|
||||
Displays result fields from:
|
||||
|
||||
- :ref:`macro result_header` and
|
||||
- :ref:`macro result_sub_header`
|
||||
|
||||
Additional fields used in the :origin:`torrent.html
|
||||
<searx/templates/simple/result_templates/torrent.html>`:
|
||||
|
||||
magnetlink:
|
||||
URL of the `magnet link`_.
|
||||
|
||||
torrentfile
|
||||
URL of the `torrent file`_.
|
||||
|
||||
seed : ``int``
|
||||
Number of seeders.
|
||||
|
||||
leech : ``int``
|
||||
Number of leecher
|
||||
|
||||
filesize : ``int``
|
||||
Size in Bytes (rendered to human readable unit of measurement).
|
||||
|
||||
files : ``int``
|
||||
Number of files.
|
||||
|
||||
|
||||
.. _template map:
|
||||
|
||||
``map.html``
|
||||
------------
|
||||
|
||||
.. _GeoJSON: https://en.wikipedia.org/wiki/GeoJSON
|
||||
.. _Leaflet: https://github.com/Leaflet/Leaflet
|
||||
.. _bbox: https://wiki.openstreetmap.org/wiki/Bounding_Box
|
||||
.. _HTMLElement.dataset: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset
|
||||
.. _Nominatim: https://nominatim.org/release-docs/latest/
|
||||
.. _Lookup: https://nominatim.org/release-docs/latest/api/Lookup/
|
||||
.. _place_id is not a persistent id:
|
||||
https://nominatim.org/release-docs/latest/api/Output/#place_id-is-not-a-persistent-id
|
||||
.. _perma_id: https://wiki.openstreetmap.org/wiki/Permanent_ID
|
||||
.. _country code: https://wiki.openstreetmap.org/wiki/Country_code
|
||||
|
||||
Displays result fields from:
|
||||
|
||||
- :ref:`macro result_header` and
|
||||
- :ref:`macro result_sub_header`
|
||||
|
||||
Additional fields used in the :origin:`map.html
|
||||
<searx/templates/simple/result_templates/map.html>`:
|
||||
|
||||
content : :py:class:`str`
|
||||
Description of the item.
|
||||
|
||||
address_label : :py:class:`str`
|
||||
Label of the address / default ``_('address')``.
|
||||
|
||||
geojson : GeoJSON_
|
||||
Geometries mapped to HTMLElement.dataset_ (``data-map-geojson``) and used by
|
||||
Leaflet_.
|
||||
|
||||
boundingbox : ``[ min-lon, min-lat, max-lon, max-lat]``
|
||||
A bbox_ area defined by min longitude , min latitude , max longitude and max
|
||||
latitude. The bounding box is mapped to HTMLElement.dataset_
|
||||
(``data-map-boundingbox``) and is used by Leaflet_.
|
||||
|
||||
longitude, latitude : :py:class:`str`
|
||||
Geographical coordinates, mapped to HTMLElement.dataset_ (``data-map-lon``,
|
||||
``data-map-lat``) and is used by Leaflet_.
|
||||
|
||||
address : ``{...}``
|
||||
A dicticonary with the address data:
|
||||
|
||||
.. code:: python
|
||||
|
||||
address = {
|
||||
'name' : str, # name of object
|
||||
'road' : str, # street name of object
|
||||
'house_number' : str, # house number of object
|
||||
'postcode' : str, # postcode of object
|
||||
'country' : str, # country of object
|
||||
'country_code' : str,
|
||||
'locality' : str,
|
||||
}
|
||||
|
||||
country_code : :py:class:`str`
|
||||
`Country code`_ of the object.
|
||||
|
||||
locality : :py:class:`str`
|
||||
The name of the city, town, township, village, borough, etc. in which this
|
||||
object is located.
|
||||
|
||||
links : ``[link1, link2, ...]``
|
||||
A list of links with labels:
|
||||
|
||||
.. code:: python
|
||||
|
||||
links.append({
|
||||
'label' : str,
|
||||
'url' : str,
|
||||
'url_label' : str, # set by some engines but unused (oscar)
|
||||
})
|
||||
|
||||
data : ``[data1, data2, ...]``
|
||||
A list of additional data, shown in two columns and containing a label and
|
||||
value.
|
||||
|
||||
.. code:: python
|
||||
|
||||
data.append({
|
||||
'label' : str,
|
||||
'value' : str,
|
||||
'key' : str, # set by some engines but unused
|
||||
})
|
||||
|
||||
type : :py:class:`str` # set by some engines but unused (oscar)
|
||||
Tag label from :ref:`OSM_KEYS_TAGS['tags'] <update_osm_keys_tags.py>`.
|
||||
|
||||
type_icon : :py:class:`str` # set by some engines but unused (oscar)
|
||||
Type's icon.
|
||||
|
||||
osm : ``{...}``
|
||||
OSM-type and OSM-ID, can be used to Lookup_ OSM data (Nominatim_). There is
|
||||
also a discussion about "`place_id is not a persistent id`_" and the
|
||||
perma_id_.
|
||||
|
||||
.. code:: python
|
||||
|
||||
osm = {
|
||||
'type': str,
|
||||
'id': str,
|
||||
}
|
||||
|
||||
type : :py:class:`str`
|
||||
Type of osm-object (if OSM-Result).
|
||||
|
||||
id :
|
||||
ID of osm-object (if OSM-Result).
|
||||
|
||||
.. hint::
|
||||
|
||||
The ``osm`` property is set by engine ``openstreetmap.py``, but it is not
|
||||
used in the ``map.html`` template yet.
|
||||
|
||||
|
||||
|
||||
.. _template paper:
|
||||
|
||||
``paper.html``
|
||||
--------------
|
||||
|
||||
.. _BibTeX format: https://www.bibtex.com/g/bibtex-format/
|
||||
.. _BibTeX field types: https://en.wikipedia.org/wiki/BibTeX#Field_types
|
||||
|
||||
Displays result fields from:
|
||||
|
||||
- :ref:`macro result_header`
|
||||
|
||||
Additional fields used in the :origin:`paper.html
|
||||
<searx/templates/simple/result_templates/paper.html>`:
|
||||
|
||||
content : :py:class:`str`
|
||||
An abstract or excerpt from the document.
|
||||
|
||||
comments : :py:class:`str`
|
||||
Free text display in italic below the content.
|
||||
|
||||
tags : :py:class:`List <list>`\ [\ :py:class:`str`\ ]
|
||||
Free tag list.
|
||||
|
||||
type : :py:class:`str`
|
||||
Short description of medium type, e.g. *book*, *pdf* or *html* ...
|
||||
|
||||
authors : :py:class:`List <list>`\ [\ :py:class:`str`\ ]
|
||||
List of authors of the work (authors with a "s" suffix, the "author" is in the
|
||||
:ref:`macro result_sub_header`).
|
||||
|
||||
editor : :py:class:`str`
|
||||
Editor of the book/paper.
|
||||
|
||||
publisher : :py:class:`str`
|
||||
Name of the publisher.
|
||||
|
||||
journal : :py:class:`str`
|
||||
Name of the journal or magazine the article was published in.
|
||||
|
||||
volume : :py:class:`str`
|
||||
Volume number.
|
||||
|
||||
pages : :py:class:`str`
|
||||
Page range where the article is.
|
||||
|
||||
number : :py:class:`str`
|
||||
Number of the report or the issue number for a journal article.
|
||||
|
||||
doi : :py:class:`str`
|
||||
DOI number (like ``10.1038/d41586-018-07848-2``).
|
||||
|
||||
issn : :py:class:`List <list>`\ [\ :py:class:`str`\ ]
|
||||
ISSN number like ``1476-4687``
|
||||
|
||||
isbn : :py:class:`List <list>`\ [\ :py:class:`str`\ ]
|
||||
ISBN number like ``9780201896831``
|
||||
|
||||
pdf_url : :py:class:`str`
|
||||
URL to the full article, the PDF version
|
||||
|
||||
html_url : :py:class:`str`
|
||||
URL to full article, HTML version
|
||||
|
||||
|
||||
.. _template packages:
|
||||
|
||||
``packages``
|
||||
------------
|
||||
|
||||
Displays result fields from:
|
||||
|
||||
- :ref:`macro result_header`
|
||||
|
||||
Additional fields used in the :origin:`packages.html
|
||||
<searx/templates/simple/result_templates/packages.html>`:
|
||||
|
||||
package_name : :py:class:`str`
|
||||
The name of the package.
|
||||
|
||||
version : :py:class:`str`
|
||||
The current version of the package.
|
||||
|
||||
maintainer : :py:class:`str`
|
||||
The maintainer or author of the project.
|
||||
|
||||
publishedDate : :py:class:`datetime <datetime.datetime>`
|
||||
Date of latest update or release.
|
||||
|
||||
tags : :py:class:`List <list>`\ [\ :py:class:`str`\ ]
|
||||
Free tag list.
|
||||
|
||||
popularity : :py:class:`str`
|
||||
The popularity of the package, e.g. rating or download count.
|
||||
|
||||
license_name : :py:class:`str`
|
||||
The name of the license.
|
||||
|
||||
license_url : :py:class:`str`
|
||||
The web location of a license copy.
|
||||
|
||||
homepage : :py:class:`str`
|
||||
The url of the project's homepage.
|
||||
|
||||
source_code_url: :py:class:`str`
|
||||
The location of the project's source code.
|
||||
|
||||
links : :py:class:`dict`
|
||||
Additional links in the form of ``{'link_name': 'http://example.com'}``
|
||||
|
||||
|
||||
.. _template code:
|
||||
|
||||
``code.html``
|
||||
-------------
|
||||
|
||||
Displays result fields from:
|
||||
|
||||
- :ref:`macro result_header` and
|
||||
- :ref:`macro result_sub_header`
|
||||
|
||||
Additional fields used in the :origin:`code.html
|
||||
<searx/templates/simple/result_templates/code.html>`:
|
||||
|
||||
content : :py:class:`str`
|
||||
Description of the code fragment.
|
||||
|
||||
codelines : ``[line1, line2, ...]``
|
||||
Lines of the code fragment.
|
||||
|
||||
code_language : :py:class:`str`
|
||||
Name of the code language, the value is passed to
|
||||
:py:obj:`pygments.lexers.get_lexer_by_name`.
|
||||
|
||||
repository : :py:class:`str`
|
||||
URL of the repository of the code fragment.
|
||||
|
||||
|
||||
.. _template files:
|
||||
|
||||
``files.html``
|
||||
--------------
|
||||
|
||||
Displays result fields from:
|
||||
|
||||
- :ref:`macro result_header` and
|
||||
- :ref:`macro result_sub_header`
|
||||
|
||||
Additional fields used in the :origin:`code.html
|
||||
<searx/templates/simple/result_templates/files.html>`:
|
||||
|
||||
filename, size, time: :py:class:`str`
|
||||
Filename, Filesize and Date of the file.
|
||||
|
||||
mtype : ``audio`` | ``video`` | :py:class:`str`
|
||||
Mimetype type of the file.
|
||||
|
||||
subtype : :py:class:`str`
|
||||
Mimetype / subtype of the file.
|
||||
|
||||
abstract : :py:class:`str`
|
||||
Abstract of the file.
|
||||
|
||||
author : :py:class:`str`
|
||||
Name of the author of the file
|
||||
|
||||
embedded : :py:class:`str`
|
||||
URL of an embedded media type (``audio`` or ``video``) / is collapsible.
|
||||
|
||||
|
||||
.. _template products:
|
||||
|
||||
``products.html``
|
||||
-----------------
|
||||
|
||||
Displays result fields from:
|
||||
|
||||
- :ref:`macro result_header` and
|
||||
- :ref:`macro result_sub_header`
|
||||
|
||||
Additional fields used in the :origin:`products.html
|
||||
<searx/templates/simple/result_templates/products.html>`:
|
||||
|
||||
content : :py:class:`str`
|
||||
Description of the product.
|
||||
|
||||
price : :py:class:`str`
|
||||
The price must include the currency.
|
||||
|
||||
shipping : :py:class:`str`
|
||||
Shipping details.
|
||||
|
||||
source_country : :py:class:`str`
|
||||
Place from which the shipment is made.
|
||||
|
||||
|
||||
.. _template answer results:
|
||||
|
||||
Answer results
|
||||
==============
|
||||
|
||||
See :ref:`result_types.answer`
|
||||
|
||||
Suggestion results
|
||||
==================
|
||||
|
||||
See :ref:`result_types.suggestion`
|
||||
|
||||
Correction results
|
||||
==================
|
||||
|
||||
See :ref:`result_types.corrections`
|
||||
|
||||
Infobox results
|
||||
===============
|
||||
|
||||
See :ref:`result_types.infobox`
|
@ -2,9 +2,9 @@
|
||||
Source-Code
|
||||
===========
|
||||
|
||||
This is a partial documentation of our source code. We are not aiming to document
|
||||
every item from the source code, but we will add documentation when requested.
|
||||
|
||||
This is a partial documentation of our source code. We are not aiming to
|
||||
document every item from the source code, but we will add documentation when
|
||||
requested.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
@ -4,7 +4,7 @@ cov-core==1.15.0
|
||||
black==24.3.0
|
||||
pylint==3.3.3
|
||||
splinter==0.21.0
|
||||
selenium==4.27.1
|
||||
selenium==4.28.1
|
||||
Pallets-Sphinx-Themes==2.3.0
|
||||
Sphinx==7.4.7
|
||||
sphinx-issues==5.0.0
|
||||
|
@ -4,7 +4,7 @@ flask-babel==4.0.0
|
||||
flask==3.1.0
|
||||
jinja2==3.1.5
|
||||
lxml==5.3.0
|
||||
pygments==2.18.0
|
||||
pygments==2.19.1
|
||||
python-dateutil==2.9.0.post0
|
||||
pyyaml==6.0.2
|
||||
httpx[http2]==0.24.1
|
||||
|
@ -9,8 +9,7 @@ import logging
|
||||
|
||||
import searx.unixthreadname
|
||||
import searx.settings_loader
|
||||
from searx.settings_defaults import settings_set_defaults
|
||||
|
||||
from searx.settings_defaults import SCHEMA, apply_schema
|
||||
|
||||
# Debug
|
||||
LOG_FORMAT_DEBUG = '%(levelname)-7s %(name)-30.30s: %(message)s'
|
||||
@ -21,14 +20,52 @@ LOG_LEVEL_PROD = logging.WARNING
|
||||
|
||||
searx_dir = abspath(dirname(__file__))
|
||||
searx_parent_dir = abspath(dirname(dirname(__file__)))
|
||||
settings, settings_load_message = searx.settings_loader.load_settings()
|
||||
|
||||
if settings is not None:
|
||||
settings = settings_set_defaults(settings)
|
||||
settings = {}
|
||||
searx_debug = False
|
||||
logger = logging.getLogger('searx')
|
||||
|
||||
_unset = object()
|
||||
|
||||
|
||||
def init_settings():
|
||||
"""Initialize global ``settings`` and ``searx_debug`` variables and
|
||||
``logger`` from ``SEARXNG_SETTINGS_PATH``.
|
||||
"""
|
||||
|
||||
global settings, searx_debug # pylint: disable=global-variable-not-assigned
|
||||
|
||||
cfg, msg = searx.settings_loader.load_settings(load_user_settings=True)
|
||||
cfg = cfg or {}
|
||||
apply_schema(cfg, SCHEMA, [])
|
||||
|
||||
settings.clear()
|
||||
settings.update(cfg)
|
||||
|
||||
searx_debug = settings['general']['debug']
|
||||
if searx_debug:
|
||||
_logging_config_debug()
|
||||
else:
|
||||
logging.basicConfig(level=LOG_LEVEL_PROD, format=LOG_FORMAT_PROD)
|
||||
logging.root.setLevel(level=LOG_LEVEL_PROD)
|
||||
logging.getLogger('werkzeug').setLevel(level=LOG_LEVEL_PROD)
|
||||
logger.info(msg)
|
||||
|
||||
# log max_request_timeout
|
||||
max_request_timeout = settings['outgoing']['max_request_timeout']
|
||||
if max_request_timeout is None:
|
||||
logger.info('max_request_timeout=%s', repr(max_request_timeout))
|
||||
else:
|
||||
logger.info('max_request_timeout=%i second(s)', max_request_timeout)
|
||||
|
||||
if settings['server']['public_instance']:
|
||||
logger.warning(
|
||||
"Be aware you have activated features intended only for public instances. "
|
||||
"This force the usage of the limiter and link_token / "
|
||||
"see https://docs.searxng.org/admin/searx.limiter.html"
|
||||
)
|
||||
|
||||
|
||||
def get_setting(name, default=_unset):
|
||||
"""Returns the value to which ``name`` point. If there is no such name in the
|
||||
settings and the ``default`` is unset, a :py:obj:`KeyError` is raised.
|
||||
@ -50,20 +87,20 @@ def get_setting(name, default=_unset):
|
||||
return value
|
||||
|
||||
|
||||
def is_color_terminal():
|
||||
def _is_color_terminal():
|
||||
if os.getenv('TERM') in ('dumb', 'unknown'):
|
||||
return False
|
||||
return sys.stdout.isatty()
|
||||
|
||||
|
||||
def logging_config_debug():
|
||||
def _logging_config_debug():
|
||||
try:
|
||||
import coloredlogs # pylint: disable=import-outside-toplevel
|
||||
except ImportError:
|
||||
coloredlogs = None
|
||||
|
||||
log_level = os.environ.get('SEARXNG_DEBUG_LOG_LEVEL', 'DEBUG')
|
||||
if coloredlogs and is_color_terminal():
|
||||
if coloredlogs and _is_color_terminal():
|
||||
level_styles = {
|
||||
'spam': {'color': 'green', 'faint': True},
|
||||
'debug': {},
|
||||
@ -87,26 +124,4 @@ def logging_config_debug():
|
||||
logging.basicConfig(level=logging.getLevelName(log_level), format=LOG_FORMAT_DEBUG)
|
||||
|
||||
|
||||
searx_debug = settings['general']['debug']
|
||||
if searx_debug:
|
||||
logging_config_debug()
|
||||
else:
|
||||
logging.basicConfig(level=LOG_LEVEL_PROD, format=LOG_FORMAT_PROD)
|
||||
logging.root.setLevel(level=LOG_LEVEL_PROD)
|
||||
logging.getLogger('werkzeug').setLevel(level=LOG_LEVEL_PROD)
|
||||
logger = logging.getLogger('searx')
|
||||
logger.info(settings_load_message)
|
||||
|
||||
# log max_request_timeout
|
||||
max_request_timeout = settings['outgoing']['max_request_timeout']
|
||||
if max_request_timeout is None:
|
||||
logger.info('max_request_timeout=%s', repr(max_request_timeout))
|
||||
else:
|
||||
logger.info('max_request_timeout=%i second(s)', max_request_timeout)
|
||||
|
||||
if settings['server']['public_instance']:
|
||||
logger.warning(
|
||||
"Be aware you have activated features intended only for public instances. "
|
||||
"This force the usage of the limiter and link_token / "
|
||||
"see https://docs.searxng.org/admin/searx.limiter.html"
|
||||
)
|
||||
init_settings()
|
||||
|
@ -1,51 +1,49 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# pylint: disable=missing-module-docstring
|
||||
"""The *answerers* give instant answers related to the search query, they
|
||||
usually provide answers of type :py:obj:`Answer <searx.result_types.Answer>`.
|
||||
|
||||
import sys
|
||||
from os import listdir
|
||||
from os.path import realpath, dirname, join, isdir
|
||||
from collections import defaultdict
|
||||
Here is an example of a very simple answerer that adds a "Hello" into the answer
|
||||
area:
|
||||
|
||||
from searx.utils import load_module
|
||||
.. code::
|
||||
|
||||
answerers_dir = dirname(realpath(__file__))
|
||||
from flask_babel import gettext as _
|
||||
from searx.answerers import Answerer
|
||||
from searx.result_types import Answer
|
||||
|
||||
class MyAnswerer(Answerer):
|
||||
|
||||
keywords = [ "hello", "hello world" ]
|
||||
|
||||
def info(self):
|
||||
return AnswererInfo(name=_("Hello"), description=_("lorem .."), keywords=self.keywords)
|
||||
|
||||
def answer(self, request, search):
|
||||
return [ Answer(answer="Hello") ]
|
||||
|
||||
----
|
||||
|
||||
.. autoclass:: Answerer
|
||||
:members:
|
||||
|
||||
.. autoclass:: AnswererInfo
|
||||
:members:
|
||||
|
||||
.. autoclass:: AnswerStorage
|
||||
:members:
|
||||
|
||||
.. autoclass:: searx.answerers._core.ModuleAnswerer
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ["AnswererInfo", "Answerer", "AnswerStorage"]
|
||||
|
||||
|
||||
def load_answerers():
|
||||
answerers = [] # pylint: disable=redefined-outer-name
|
||||
from ._core import AnswererInfo, Answerer, AnswerStorage
|
||||
|
||||
for filename in listdir(answerers_dir):
|
||||
if not isdir(join(answerers_dir, filename)) or filename.startswith('_'):
|
||||
continue
|
||||
module = load_module('answerer.py', join(answerers_dir, filename))
|
||||
if not hasattr(module, 'keywords') or not isinstance(module.keywords, tuple) or not module.keywords:
|
||||
sys.exit(2)
|
||||
answerers.append(module)
|
||||
return answerers
|
||||
|
||||
|
||||
def get_answerers_by_keywords(answerers): # pylint:disable=redefined-outer-name
|
||||
by_keyword = defaultdict(list)
|
||||
for answerer in answerers:
|
||||
for keyword in answerer.keywords:
|
||||
for keyword in answerer.keywords:
|
||||
by_keyword[keyword].append(answerer.answer)
|
||||
return by_keyword
|
||||
|
||||
|
||||
def ask(query):
|
||||
results = []
|
||||
query_parts = list(filter(None, query.query.split()))
|
||||
|
||||
if not query_parts or query_parts[0] not in answerers_by_keywords:
|
||||
return results
|
||||
|
||||
for answerer in answerers_by_keywords[query_parts[0]]:
|
||||
result = answerer(query)
|
||||
if result:
|
||||
results.append(result)
|
||||
return results
|
||||
|
||||
|
||||
answerers = load_answerers()
|
||||
answerers_by_keywords = get_answerers_by_keywords(answerers)
|
||||
STORAGE: AnswerStorage = AnswerStorage()
|
||||
STORAGE.load_builtins()
|
||||
|
169
searx/answerers/_core.py
Normal file
169
searx/answerers/_core.py
Normal file
@ -0,0 +1,169 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# pylint: disable=too-few-public-methods, missing-module-docstring
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import importlib
|
||||
import logging
|
||||
import pathlib
|
||||
import warnings
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from searx.utils import load_module
|
||||
from searx.result_types.answer import BaseAnswer
|
||||
|
||||
|
||||
_default = pathlib.Path(__file__).parent
|
||||
log: logging.Logger = logging.getLogger("searx.answerers")
|
||||
|
||||
|
||||
@dataclass
|
||||
class AnswererInfo:
|
||||
"""Object that holds informations about an answerer, these infos are shown
|
||||
to the user in the Preferences menu.
|
||||
|
||||
To be able to translate the information into other languages, the text must
|
||||
be written in English and translated with :py:obj:`flask_babel.gettext`.
|
||||
"""
|
||||
|
||||
name: str
|
||||
"""Name of the *answerer*."""
|
||||
|
||||
description: str
|
||||
"""Short description of the *answerer*."""
|
||||
|
||||
examples: list[str]
|
||||
"""List of short examples of the usage / of query terms."""
|
||||
|
||||
keywords: list[str]
|
||||
"""See :py:obj:`Answerer.keywords`"""
|
||||
|
||||
|
||||
class Answerer(abc.ABC):
|
||||
"""Abstract base class of answerers."""
|
||||
|
||||
keywords: list[str]
|
||||
"""Keywords to which the answerer has *answers*."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def answer(self, query: str) -> list[BaseAnswer]:
|
||||
"""Function that returns a list of answers to the question/query."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def info(self) -> AnswererInfo:
|
||||
"""Informations about the *answerer*, see :py:obj:`AnswererInfo`."""
|
||||
|
||||
|
||||
class ModuleAnswerer(Answerer):
|
||||
"""A wrapper class for legacy *answerers* where the names (keywords, answer,
|
||||
info) are implemented on the module level (not in a class).
|
||||
|
||||
.. note::
|
||||
|
||||
For internal use only!
|
||||
"""
|
||||
|
||||
def __init__(self, mod):
|
||||
|
||||
for name in ["keywords", "self_info", "answer"]:
|
||||
if not getattr(mod, name, None):
|
||||
raise SystemExit(2)
|
||||
if not isinstance(mod.keywords, tuple):
|
||||
raise SystemExit(2)
|
||||
|
||||
self.module = mod
|
||||
self.keywords = mod.keywords # type: ignore
|
||||
|
||||
def answer(self, query: str) -> list[BaseAnswer]:
|
||||
return self.module.answer(query)
|
||||
|
||||
def info(self) -> AnswererInfo:
|
||||
kwargs = self.module.self_info()
|
||||
kwargs["keywords"] = self.keywords
|
||||
return AnswererInfo(**kwargs)
|
||||
|
||||
|
||||
class AnswerStorage(dict):
|
||||
"""A storage for managing the *answerers* of SearXNG. With the
|
||||
:py:obj:`AnswerStorage.ask`” method, a caller can ask questions to all
|
||||
*answerers* and receives a list of the results."""
|
||||
|
||||
answerer_list: set[Answerer]
|
||||
"""The list of :py:obj:`Answerer` in this storage."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.answerer_list = set()
|
||||
|
||||
def load_builtins(self):
|
||||
"""Loads ``answerer.py`` modules from the python packages in
|
||||
:origin:`searx/answerers`. The python modules are wrapped by
|
||||
:py:obj:`ModuleAnswerer`."""
|
||||
|
||||
for f in _default.iterdir():
|
||||
if f.name.startswith("_"):
|
||||
continue
|
||||
|
||||
if f.is_file() and f.suffix == ".py":
|
||||
self.register_by_fqn(f"searx.answerers.{f.stem}.SXNGAnswerer")
|
||||
continue
|
||||
|
||||
# for backward compatibility (if a fork has additional answerers)
|
||||
|
||||
if f.is_dir() and (f / "answerer.py").exists():
|
||||
warnings.warn(
|
||||
f"answerer module {f} is deprecated / migrate to searx.answerers.Answerer", DeprecationWarning
|
||||
)
|
||||
mod = load_module("answerer.py", str(f))
|
||||
self.register(ModuleAnswerer(mod))
|
||||
|
||||
def register_by_fqn(self, fqn: str):
|
||||
"""Register a :py:obj:`Answerer` via its fully qualified class namen(FQN)."""
|
||||
|
||||
mod_name, _, obj_name = fqn.rpartition('.')
|
||||
mod = importlib.import_module(mod_name)
|
||||
code_obj = getattr(mod, obj_name, None)
|
||||
|
||||
if code_obj is None:
|
||||
msg = f"answerer {fqn} is not implemented"
|
||||
log.critical(msg)
|
||||
raise ValueError(msg)
|
||||
|
||||
self.register(code_obj())
|
||||
|
||||
def register(self, answerer: Answerer):
|
||||
"""Register a :py:obj:`Answerer`."""
|
||||
|
||||
self.answerer_list.add(answerer)
|
||||
for _kw in answerer.keywords:
|
||||
self[_kw] = self.get(_kw, [])
|
||||
self[_kw].append(answerer)
|
||||
|
||||
def ask(self, query: str) -> list[BaseAnswer]:
|
||||
"""An answerer is identified via keywords, if there is a keyword at the
|
||||
first position in the ``query`` for which there is one or more
|
||||
answerers, then these are called, whereby the entire ``query`` is passed
|
||||
as argument to the answerer function."""
|
||||
|
||||
results = []
|
||||
keyword = None
|
||||
for keyword in query.split():
|
||||
if keyword:
|
||||
break
|
||||
|
||||
if not keyword or keyword not in self:
|
||||
return results
|
||||
|
||||
for answerer in self[keyword]:
|
||||
for answer in answerer.answer(query):
|
||||
# In case of *answers* prefix ``answerer:`` is set, see searx.result_types.Result
|
||||
answer.engine = f"answerer: {keyword}"
|
||||
results.append(answer)
|
||||
|
||||
return results
|
||||
|
||||
@property
|
||||
def info(self) -> list[AnswererInfo]:
|
||||
return [a.info() for a in self.answerer_list]
|
80
searx/answerers/random.py
Normal file
80
searx/answerers/random.py
Normal file
@ -0,0 +1,80 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# pylint: disable=missing-module-docstring
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import random
|
||||
import string
|
||||
import uuid
|
||||
from flask_babel import gettext
|
||||
|
||||
from searx.result_types import Answer
|
||||
from searx.result_types.answer import BaseAnswer
|
||||
|
||||
from . import Answerer, AnswererInfo
|
||||
|
||||
|
||||
def random_characters():
|
||||
random_string_letters = string.ascii_lowercase + string.digits + string.ascii_uppercase
|
||||
return [random.choice(random_string_letters) for _ in range(random.randint(8, 32))]
|
||||
|
||||
|
||||
def random_string():
|
||||
return ''.join(random_characters())
|
||||
|
||||
|
||||
def random_float():
|
||||
return str(random.random())
|
||||
|
||||
|
||||
def random_int():
|
||||
random_int_max = 2**31
|
||||
return str(random.randint(-random_int_max, random_int_max))
|
||||
|
||||
|
||||
def random_sha256():
|
||||
m = hashlib.sha256()
|
||||
m.update(''.join(random_characters()).encode())
|
||||
return str(m.hexdigest())
|
||||
|
||||
|
||||
def random_uuid():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
def random_color():
|
||||
color = "%06x" % random.randint(0, 0xFFFFFF)
|
||||
return f"#{color.upper()}"
|
||||
|
||||
|
||||
class SXNGAnswerer(Answerer):
|
||||
"""Random value generator"""
|
||||
|
||||
keywords = ["random"]
|
||||
|
||||
random_types = {
|
||||
"string": random_string,
|
||||
"int": random_int,
|
||||
"float": random_float,
|
||||
"sha256": random_sha256,
|
||||
"uuid": random_uuid,
|
||||
"color": random_color,
|
||||
}
|
||||
|
||||
def info(self):
|
||||
|
||||
return AnswererInfo(
|
||||
name=gettext(self.__doc__),
|
||||
description=gettext("Generate different random values"),
|
||||
keywords=self.keywords,
|
||||
examples=[f"random {x}" for x in self.random_types],
|
||||
)
|
||||
|
||||
def answer(self, query: str) -> list[BaseAnswer]:
|
||||
|
||||
parts = query.split()
|
||||
if len(parts) != 2 or parts[1] not in self.random_types:
|
||||
return []
|
||||
|
||||
return [Answer(answer=self.random_types[parts[1]]())]
|
@ -1,2 +0,0 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# pylint: disable=missing-module-docstring
|
@ -1,79 +0,0 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# pylint: disable=missing-module-docstring
|
||||
|
||||
import hashlib
|
||||
import random
|
||||
import string
|
||||
import uuid
|
||||
from flask_babel import gettext
|
||||
|
||||
# required answerer attribute
|
||||
# specifies which search query keywords triggers this answerer
|
||||
keywords = ('random',)
|
||||
|
||||
random_int_max = 2**31
|
||||
random_string_letters = string.ascii_lowercase + string.digits + string.ascii_uppercase
|
||||
|
||||
|
||||
def random_characters():
|
||||
return [random.choice(random_string_letters) for _ in range(random.randint(8, 32))]
|
||||
|
||||
|
||||
def random_string():
|
||||
return ''.join(random_characters())
|
||||
|
||||
|
||||
def random_float():
|
||||
return str(random.random())
|
||||
|
||||
|
||||
def random_int():
|
||||
return str(random.randint(-random_int_max, random_int_max))
|
||||
|
||||
|
||||
def random_sha256():
|
||||
m = hashlib.sha256()
|
||||
m.update(''.join(random_characters()).encode())
|
||||
return str(m.hexdigest())
|
||||
|
||||
|
||||
def random_uuid():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
def random_color():
|
||||
color = "%06x" % random.randint(0, 0xFFFFFF)
|
||||
return f"#{color.upper()}"
|
||||
|
||||
|
||||
random_types = {
|
||||
'string': random_string,
|
||||
'int': random_int,
|
||||
'float': random_float,
|
||||
'sha256': random_sha256,
|
||||
'uuid': random_uuid,
|
||||
'color': random_color,
|
||||
}
|
||||
|
||||
|
||||
# required answerer function
|
||||
# can return a list of results (any result type) for a given query
|
||||
def answer(query):
|
||||
parts = query.query.split()
|
||||
if len(parts) != 2:
|
||||
return []
|
||||
|
||||
if parts[1] not in random_types:
|
||||
return []
|
||||
|
||||
return [{'answer': random_types[parts[1]]()}]
|
||||
|
||||
|
||||
# required answerer function
|
||||
# returns information about the answerer
|
||||
def self_info():
|
||||
return {
|
||||
'name': gettext('Random value generator'),
|
||||
'description': gettext('Generate different random values'),
|
||||
'examples': ['random {}'.format(x) for x in random_types],
|
||||
}
|
64
searx/answerers/statistics.py
Normal file
64
searx/answerers/statistics.py
Normal file
@ -0,0 +1,64 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# pylint: disable=missing-module-docstring
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import reduce
|
||||
from operator import mul
|
||||
|
||||
import babel
|
||||
import babel.numbers
|
||||
from flask_babel import gettext
|
||||
|
||||
from searx.extended_types import sxng_request
|
||||
from searx.result_types import Answer
|
||||
from searx.result_types.answer import BaseAnswer
|
||||
|
||||
from . import Answerer, AnswererInfo
|
||||
|
||||
kw2func = [
|
||||
("min", min),
|
||||
("max", max),
|
||||
("avg", lambda args: sum(args) / len(args)),
|
||||
("sum", sum),
|
||||
("prod", lambda args: reduce(mul, args, 1)),
|
||||
]
|
||||
|
||||
|
||||
class SXNGAnswerer(Answerer):
|
||||
"""Statistics functions"""
|
||||
|
||||
keywords = [kw for kw, _ in kw2func]
|
||||
|
||||
def info(self):
|
||||
|
||||
return AnswererInfo(
|
||||
name=gettext(self.__doc__),
|
||||
description=gettext("Compute {func} of the arguments".format(func='/'.join(self.keywords))),
|
||||
keywords=self.keywords,
|
||||
examples=["avg 123 548 2.04 24.2"],
|
||||
)
|
||||
|
||||
def answer(self, query: str) -> list[BaseAnswer]:
|
||||
|
||||
results = []
|
||||
parts = query.split()
|
||||
if len(parts) < 2:
|
||||
return results
|
||||
|
||||
ui_locale = babel.Locale.parse(sxng_request.preferences.get_value('locale'), sep='-')
|
||||
|
||||
try:
|
||||
args = [babel.numbers.parse_decimal(num, ui_locale, numbering_system="latn") for num in parts[1:]]
|
||||
except: # pylint: disable=bare-except
|
||||
# seems one of the args is not a float type, can't be converted to float
|
||||
return results
|
||||
|
||||
for k, func in kw2func:
|
||||
if k == parts[0]:
|
||||
res = func(args)
|
||||
res = babel.numbers.format_decimal(res, locale=ui_locale)
|
||||
f_str = ', '.join(babel.numbers.format_decimal(arg, locale=ui_locale) for arg in args)
|
||||
results.append(Answer(answer=f"[{ui_locale}] {k}({f_str}) = {res} "))
|
||||
break
|
||||
|
||||
return results
|
@ -1,2 +0,0 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# pylint: disable=missing-module-docstring
|
@ -1,53 +0,0 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# pylint: disable=missing-module-docstring
|
||||
|
||||
from functools import reduce
|
||||
from operator import mul
|
||||
|
||||
from flask_babel import gettext
|
||||
|
||||
|
||||
keywords = ('min', 'max', 'avg', 'sum', 'prod')
|
||||
|
||||
|
||||
# required answerer function
|
||||
# can return a list of results (any result type) for a given query
|
||||
def answer(query):
|
||||
parts = query.query.split()
|
||||
|
||||
if len(parts) < 2:
|
||||
return []
|
||||
|
||||
try:
|
||||
args = list(map(float, parts[1:]))
|
||||
except: # pylint: disable=bare-except
|
||||
return []
|
||||
|
||||
func = parts[0]
|
||||
_answer = None
|
||||
|
||||
if func == 'min':
|
||||
_answer = min(args)
|
||||
elif func == 'max':
|
||||
_answer = max(args)
|
||||
elif func == 'avg':
|
||||
_answer = sum(args) / len(args)
|
||||
elif func == 'sum':
|
||||
_answer = sum(args)
|
||||
elif func == 'prod':
|
||||
_answer = reduce(mul, args, 1)
|
||||
|
||||
if _answer is None:
|
||||
return []
|
||||
|
||||
return [{'answer': str(_answer)}]
|
||||
|
||||
|
||||
# required answerer function
|
||||
# returns information about the answerer
|
||||
def self_info():
|
||||
return {
|
||||
'name': gettext('Statistics functions'),
|
||||
'description': gettext('Compute {functions} of the arguments').format(functions='/'.join(keywords)),
|
||||
'examples': ['avg 123 548 2.04 24.2'],
|
||||
}
|
@ -8,9 +8,11 @@ import json
|
||||
import html
|
||||
from urllib.parse import urlencode, quote_plus
|
||||
|
||||
import lxml
|
||||
import lxml.etree
|
||||
import lxml.html
|
||||
from httpx import HTTPError
|
||||
|
||||
from searx.extended_types import SXNG_Response
|
||||
from searx import settings
|
||||
from searx.engines import (
|
||||
engines,
|
||||
@ -26,16 +28,31 @@ def update_kwargs(**kwargs):
|
||||
kwargs['raise_for_httperror'] = True
|
||||
|
||||
|
||||
def get(*args, **kwargs):
|
||||
def get(*args, **kwargs) -> SXNG_Response:
|
||||
update_kwargs(**kwargs)
|
||||
return http_get(*args, **kwargs)
|
||||
|
||||
|
||||
def post(*args, **kwargs):
|
||||
def post(*args, **kwargs) -> SXNG_Response:
|
||||
update_kwargs(**kwargs)
|
||||
return http_post(*args, **kwargs)
|
||||
|
||||
|
||||
def baidu(query, _lang):
|
||||
# baidu search autocompleter
|
||||
base_url = "https://www.baidu.com/sugrec?"
|
||||
response = get(base_url + urlencode({'ie': 'utf-8', 'json': 1, 'prod': 'pc', 'wd': query}))
|
||||
|
||||
results = []
|
||||
|
||||
if response.ok:
|
||||
data = response.json()
|
||||
if 'g' in data:
|
||||
for item in data['g']:
|
||||
results.append(item['q'])
|
||||
return results
|
||||
|
||||
|
||||
def brave(query, _lang):
|
||||
# brave search autocompleter
|
||||
url = 'https://search.brave.com/api/suggest?'
|
||||
@ -111,7 +128,7 @@ def google_complete(query, sxng_locale):
|
||||
)
|
||||
results = []
|
||||
resp = get(url.format(subdomain=google_info['subdomain'], args=args))
|
||||
if resp.ok:
|
||||
if resp and resp.ok:
|
||||
json_txt = resp.text[resp.text.find('[') : resp.text.find(']', -3) + 1]
|
||||
data = json.loads(json_txt)
|
||||
for item in data[0]:
|
||||
@ -205,7 +222,7 @@ def wikipedia(query, sxng_locale):
|
||||
results = []
|
||||
eng_traits = engines['wikipedia'].traits
|
||||
wiki_lang = eng_traits.get_language(sxng_locale, 'en')
|
||||
wiki_netloc = eng_traits.custom['wiki_netloc'].get(wiki_lang, 'en.wikipedia.org')
|
||||
wiki_netloc = eng_traits.custom['wiki_netloc'].get(wiki_lang, 'en.wikipedia.org') # type: ignore
|
||||
|
||||
url = 'https://{wiki_netloc}/w/api.php?{args}'
|
||||
args = urlencode(
|
||||
@ -238,17 +255,18 @@ def yandex(query, _lang):
|
||||
|
||||
|
||||
backends = {
|
||||
'baidu': baidu,
|
||||
'brave': brave,
|
||||
'dbpedia': dbpedia,
|
||||
'duckduckgo': duckduckgo,
|
||||
'google': google_complete,
|
||||
'mwmbl': mwmbl,
|
||||
'qwant': qwant,
|
||||
'seznam': seznam,
|
||||
'startpage': startpage,
|
||||
'stract': stract,
|
||||
'swisscows': swisscows,
|
||||
'qwant': qwant,
|
||||
'wikipedia': wikipedia,
|
||||
'brave': brave,
|
||||
'yandex': yandex,
|
||||
}
|
||||
|
||||
|
@ -13,12 +13,14 @@ import flask
|
||||
import werkzeug
|
||||
|
||||
from searx import logger
|
||||
from searx.extended_types import SXNG_Request
|
||||
|
||||
from . import config
|
||||
|
||||
logger = logger.getChild('botdetection')
|
||||
|
||||
|
||||
def dump_request(request: flask.Request):
|
||||
def dump_request(request: SXNG_Request):
|
||||
return (
|
||||
request.path
|
||||
+ " || X-Forwarded-For: %s" % request.headers.get('X-Forwarded-For')
|
||||
@ -66,7 +68,7 @@ def _log_error_only_once(err_msg):
|
||||
_logged_errors.append(err_msg)
|
||||
|
||||
|
||||
def get_real_ip(request: flask.Request) -> str:
|
||||
def get_real_ip(request: SXNG_Request) -> str:
|
||||
"""Returns real IP of the request. Since not all proxies set all the HTTP
|
||||
headers and incoming headers can be faked it may happen that the IP cannot
|
||||
be determined correctly.
|
||||
|
@ -12,7 +12,6 @@ Accept_ header ..
|
||||
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept
|
||||
|
||||
"""
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
from __future__ import annotations
|
||||
from ipaddress import (
|
||||
@ -20,17 +19,18 @@ from ipaddress import (
|
||||
IPv6Network,
|
||||
)
|
||||
|
||||
import flask
|
||||
import werkzeug
|
||||
|
||||
from searx.extended_types import SXNG_Request
|
||||
|
||||
from . import config
|
||||
from ._helpers import too_many_requests
|
||||
|
||||
|
||||
def filter_request(
|
||||
network: IPv4Network | IPv6Network,
|
||||
request: flask.Request,
|
||||
cfg: config.Config,
|
||||
request: SXNG_Request,
|
||||
cfg: config.Config, # pylint: disable=unused-argument
|
||||
) -> werkzeug.Response | None:
|
||||
|
||||
if 'text/html' not in request.accept_mimetypes:
|
||||
|
@ -13,7 +13,6 @@ bot if the Accept-Encoding_ header ..
|
||||
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding
|
||||
|
||||
"""
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
from __future__ import annotations
|
||||
from ipaddress import (
|
||||
@ -21,17 +20,18 @@ from ipaddress import (
|
||||
IPv6Network,
|
||||
)
|
||||
|
||||
import flask
|
||||
import werkzeug
|
||||
|
||||
from searx.extended_types import SXNG_Request
|
||||
|
||||
from . import config
|
||||
from ._helpers import too_many_requests
|
||||
|
||||
|
||||
def filter_request(
|
||||
network: IPv4Network | IPv6Network,
|
||||
request: flask.Request,
|
||||
cfg: config.Config,
|
||||
request: SXNG_Request,
|
||||
cfg: config.Config, # pylint: disable=unused-argument
|
||||
) -> werkzeug.Response | None:
|
||||
|
||||
accept_list = [l.strip() for l in request.headers.get('Accept-Encoding', '').split(',')]
|
||||
|
@ -10,24 +10,25 @@ if the Accept-Language_ header is unset.
|
||||
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent
|
||||
|
||||
"""
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
from __future__ import annotations
|
||||
from ipaddress import (
|
||||
IPv4Network,
|
||||
IPv6Network,
|
||||
)
|
||||
|
||||
import flask
|
||||
import werkzeug
|
||||
|
||||
from searx.extended_types import SXNG_Request
|
||||
|
||||
from . import config
|
||||
from ._helpers import too_many_requests
|
||||
|
||||
|
||||
def filter_request(
|
||||
network: IPv4Network | IPv6Network,
|
||||
request: flask.Request,
|
||||
cfg: config.Config,
|
||||
request: SXNG_Request,
|
||||
cfg: config.Config, # pylint: disable=unused-argument
|
||||
) -> werkzeug.Response | None:
|
||||
if request.headers.get('Accept-Language', '').strip() == '':
|
||||
return too_many_requests(network, "missing HTTP header Accept-Language")
|
||||
|
@ -10,7 +10,6 @@ the Connection_ header is set to ``close``.
|
||||
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection
|
||||
|
||||
"""
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
from __future__ import annotations
|
||||
from ipaddress import (
|
||||
@ -18,17 +17,18 @@ from ipaddress import (
|
||||
IPv6Network,
|
||||
)
|
||||
|
||||
import flask
|
||||
import werkzeug
|
||||
|
||||
from searx.extended_types import SXNG_Request
|
||||
|
||||
from . import config
|
||||
from ._helpers import too_many_requests
|
||||
|
||||
|
||||
def filter_request(
|
||||
network: IPv4Network | IPv6Network,
|
||||
request: flask.Request,
|
||||
cfg: config.Config,
|
||||
request: SXNG_Request,
|
||||
cfg: config.Config, # pylint: disable=unused-argument
|
||||
) -> werkzeug.Response | None:
|
||||
|
||||
if request.headers.get('Connection', '').strip() == 'close':
|
||||
|
@ -11,7 +11,6 @@ the User-Agent_ header is unset or matches the regular expression
|
||||
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent
|
||||
|
||||
"""
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
from __future__ import annotations
|
||||
import re
|
||||
@ -20,9 +19,10 @@ from ipaddress import (
|
||||
IPv6Network,
|
||||
)
|
||||
|
||||
import flask
|
||||
import werkzeug
|
||||
|
||||
from searx.extended_types import SXNG_Request
|
||||
|
||||
from . import config
|
||||
from ._helpers import too_many_requests
|
||||
|
||||
@ -56,8 +56,8 @@ def regexp_user_agent():
|
||||
|
||||
def filter_request(
|
||||
network: IPv4Network | IPv6Network,
|
||||
request: flask.Request,
|
||||
cfg: config.Config,
|
||||
request: SXNG_Request,
|
||||
cfg: config.Config, # pylint: disable=unused-argument
|
||||
) -> werkzeug.Response | None:
|
||||
|
||||
user_agent = request.headers.get('User-Agent', 'unknown')
|
||||
|
@ -45,6 +45,7 @@ from ipaddress import (
|
||||
import flask
|
||||
import werkzeug
|
||||
|
||||
from searx.extended_types import SXNG_Request
|
||||
from searx import redisdb
|
||||
from searx.redislib import incr_sliding_window, drop_counter
|
||||
|
||||
@ -91,7 +92,7 @@ SUSPICIOUS_IP_MAX = 3
|
||||
|
||||
def filter_request(
|
||||
network: IPv4Network | IPv6Network,
|
||||
request: flask.Request,
|
||||
request: SXNG_Request,
|
||||
cfg: config.Config,
|
||||
) -> werkzeug.Response | None:
|
||||
|
||||
|
@ -43,11 +43,11 @@ from ipaddress import (
|
||||
|
||||
import string
|
||||
import random
|
||||
import flask
|
||||
|
||||
from searx import logger
|
||||
from searx import redisdb
|
||||
from searx.redislib import secret_hash
|
||||
from searx.extended_types import SXNG_Request
|
||||
|
||||
from ._helpers import (
|
||||
get_network,
|
||||
@ -69,7 +69,7 @@ TOKEN_KEY = 'SearXNG_limiter.token'
|
||||
logger = logger.getChild('botdetection.link_token')
|
||||
|
||||
|
||||
def is_suspicious(network: IPv4Network | IPv6Network, request: flask.Request, renew: bool = False):
|
||||
def is_suspicious(network: IPv4Network | IPv6Network, request: SXNG_Request, renew: bool = False):
|
||||
"""Checks whether a valid ping is exists for this (client) network, if not
|
||||
this request is rated as *suspicious*. If a valid ping exists and argument
|
||||
``renew`` is ``True`` the expire time of this ping is reset to
|
||||
@ -92,7 +92,7 @@ def is_suspicious(network: IPv4Network | IPv6Network, request: flask.Request, re
|
||||
return False
|
||||
|
||||
|
||||
def ping(request: flask.Request, token: str):
|
||||
def ping(request: SXNG_Request, token: str):
|
||||
"""This function is called by a request to URL ``/client<token>.css``. If
|
||||
``token`` is valid a :py:obj:`PING_KEY` for the client is stored in the DB.
|
||||
The expire time of this ping-key is :py:obj:`PING_LIVE_TIME`.
|
||||
@ -113,7 +113,7 @@ def ping(request: flask.Request, token: str):
|
||||
redis_client.set(ping_key, 1, ex=PING_LIVE_TIME)
|
||||
|
||||
|
||||
def get_ping_key(network: IPv4Network | IPv6Network, request: flask.Request) -> str:
|
||||
def get_ping_key(network: IPv4Network | IPv6Network, request: SXNG_Request) -> str:
|
||||
"""Generates a hashed key that fits (more or less) to a *WEB-browser
|
||||
session* in a network."""
|
||||
return (
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -5359,6 +5359,7 @@
|
||||
"pt": "pa'anga",
|
||||
"ru": "тонганская паанга",
|
||||
"sk": "Tonžská paʻanga",
|
||||
"sl": "tongovska paanga",
|
||||
"sr": "тонганска панга",
|
||||
"sv": "Tongansk pa'anga",
|
||||
"th": "ปาอางา",
|
||||
@ -5990,6 +5991,23 @@
|
||||
"uk": "Східно-карибський долар",
|
||||
"vi": "Đô la Đông Caribe"
|
||||
},
|
||||
"XCG": {
|
||||
"ar": "الجلدر الكاريبي",
|
||||
"ca": "florí caribeny",
|
||||
"de": "Karibischer Gulden",
|
||||
"en": "Caribbean guilder",
|
||||
"eo": "Karibia guldeno",
|
||||
"es": "florín caribeño",
|
||||
"fr": "Florin caribéen",
|
||||
"hr": "Karipski gulden",
|
||||
"hu": "karibi forint",
|
||||
"it": "fiorino caraibico",
|
||||
"nl": "Caribische gulden",
|
||||
"pap": "Florin karibense",
|
||||
"pt": "Florim do Caribe",
|
||||
"ru": "Карибский гульден",
|
||||
"sl": "karibski goldinar"
|
||||
},
|
||||
"XDR": {
|
||||
"ar": "حقوق السحب الخاصة",
|
||||
"bg": "Специални права на тираж",
|
||||
@ -6109,6 +6127,7 @@
|
||||
"vi": "Franc CFP"
|
||||
},
|
||||
"XPT": {
|
||||
"ar": "استثمار البلاتين",
|
||||
"de": "Platinpreis",
|
||||
"en": "platinum as an investment"
|
||||
},
|
||||
@ -6375,6 +6394,7 @@
|
||||
"CLP$": "CLP",
|
||||
"COL$": "COP",
|
||||
"COU$": "COU",
|
||||
"Cg": "XCG",
|
||||
"D": "GMD",
|
||||
"DA": "DZD",
|
||||
"DEN": "MKD",
|
||||
@ -6439,6 +6459,7 @@
|
||||
"NT$": "TWD",
|
||||
"NZ$": "NZD",
|
||||
"Nfk": "ERN",
|
||||
"Noha heinen krasss": "EUR",
|
||||
"Nu": "BTN",
|
||||
"N₨": "NPR",
|
||||
"P": "BWP",
|
||||
@ -6469,6 +6490,7 @@
|
||||
"Ush": "UGX",
|
||||
"VT": "VUV",
|
||||
"WS$": "WST",
|
||||
"XCG": "XCG",
|
||||
"XDR": "XDR",
|
||||
"Z$": "ZWL",
|
||||
"ZK": "ZMW",
|
||||
@ -7041,6 +7063,8 @@
|
||||
"canadiske dollar": "CAD",
|
||||
"cape verde escudo": "CVE",
|
||||
"cape verdean escudo": "CVE",
|
||||
"caribbean guilder": "XCG",
|
||||
"caribische gulden": "XCG",
|
||||
"cayman adaları doları": "KYD",
|
||||
"cayman islands dollar": "KYD",
|
||||
"caymaneilandse dollar": "KYD",
|
||||
@ -8382,6 +8406,7 @@
|
||||
"filler": "HUF",
|
||||
"fillér": "HUF",
|
||||
"fiorino arubano": "AWG",
|
||||
"fiorino caraibico": "XCG",
|
||||
"fiorino delle antille olandesi": "ANG",
|
||||
"fiorino di aruba": "AWG",
|
||||
"fiorino ungherese": "HUF",
|
||||
@ -8393,6 +8418,7 @@
|
||||
"florim das antilhas holandesas": "ANG",
|
||||
"florim das antilhas neerlandesas": "ANG",
|
||||
"florim de aruba": "AWG",
|
||||
"florim do caribe": "XCG",
|
||||
"florim húngaro": "HUF",
|
||||
"florin": "AWG",
|
||||
"florin antiano": "ANG",
|
||||
@ -8406,14 +8432,17 @@
|
||||
"florin arubeño": "AWG",
|
||||
"florin arubez": "AWG",
|
||||
"florin arubiano": "AWG",
|
||||
"florin caribéen": "XCG",
|
||||
"florin d'aruba": "AWG",
|
||||
"florin de las antilhas neerlandesas": "ANG",
|
||||
"florin des antilles néerlandaises": "ANG",
|
||||
"florin d’aruba": "AWG",
|
||||
"florin hungaro": "HUF",
|
||||
"florin húngaro": "HUF",
|
||||
"florin karibense": "XCG",
|
||||
"florint": "HUF",
|
||||
"florint húngaro": "HUF",
|
||||
"florí caribeny": "XCG",
|
||||
"florí d'aruba": "AWG",
|
||||
"florí de les antilles neerlandeses": "ANG",
|
||||
"florí d’aruba": "AWG",
|
||||
@ -8421,6 +8450,7 @@
|
||||
"florín antillano neerlandés": "ANG",
|
||||
"florín arubeno": "AWG",
|
||||
"florín arubeño": "AWG",
|
||||
"florín caribeño": "XCG",
|
||||
"florín das antillas neerlandesas": "ANG",
|
||||
"florín de aruba": "AWG",
|
||||
"florín hungaro": "HUF",
|
||||
@ -9162,6 +9192,11 @@
|
||||
"kapverdské escudo": "CVE",
|
||||
"kapverdski eskudo": "CVE",
|
||||
"karbovanet": "UAH",
|
||||
"karibi forint": "XCG",
|
||||
"karibia guldeno": "XCG",
|
||||
"karibischer gulden": "XCG",
|
||||
"karibski goldinar": "XCG",
|
||||
"karipski gulden": "XCG",
|
||||
"karod": "NPR",
|
||||
"kartvela lario": "GEL",
|
||||
"katar riyal": "QAR",
|
||||
@ -11966,6 +12001,7 @@
|
||||
"tongaška pa’anga": "TOP",
|
||||
"tongos pa'anga": "TOP",
|
||||
"tongos paanga": "TOP",
|
||||
"tongovska paanga": "TOP",
|
||||
"tonška pa’anga": "TOP",
|
||||
"tonžská pa'anga": "TOP",
|
||||
"tonžská paanga": "TOP",
|
||||
@ -13202,6 +13238,7 @@
|
||||
"канадски долар": "CAD",
|
||||
"канадский доллар": "CAD",
|
||||
"канадський долар": "CAD",
|
||||
"карибский гульден": "XCG",
|
||||
"катар риалы": "QAR",
|
||||
"катарски риал": "QAR",
|
||||
"катарски ријал": "QAR",
|
||||
@ -14234,6 +14271,7 @@
|
||||
"שקל ישראלי חדש": "ILS",
|
||||
"؋": "AFN",
|
||||
"ارياري": "MGA",
|
||||
"استثمار البلاتين": "XPT",
|
||||
"استثمار الذهب": "XAU",
|
||||
"استثمار الفضة": "XAG",
|
||||
"الاستثمار في الذهب": "XAU",
|
||||
@ -14243,6 +14281,7 @@
|
||||
"البوليفيانو": "BOB",
|
||||
"البيزو الكوبي": "CUP",
|
||||
"البيزو المكسيكي": "MXN",
|
||||
"الجلدر الكاريبي": "XCG",
|
||||
"الجنية البريطاني": "GBP",
|
||||
"الجنية المصري": "EGP",
|
||||
"الجنيه الاسترليني": "GBP",
|
||||
|
File diff suppressed because one or more lines are too long
@ -19,11 +19,11 @@
|
||||
"cbz",
|
||||
"djvu",
|
||||
"doc",
|
||||
"docx",
|
||||
"epub",
|
||||
"fb2",
|
||||
"htm",
|
||||
"html",
|
||||
"jpg",
|
||||
"lit",
|
||||
"lrf",
|
||||
"mht",
|
||||
@ -42,6 +42,7 @@
|
||||
"newest_added",
|
||||
"oldest",
|
||||
"oldest_added",
|
||||
"random",
|
||||
"smallest"
|
||||
]
|
||||
},
|
||||
@ -195,7 +196,7 @@
|
||||
"es": "es-es",
|
||||
"et": "et-et",
|
||||
"eu": "eu-eu",
|
||||
"fa": "fa-fa",
|
||||
"fa": "prs-prs",
|
||||
"fi": "fi-fi",
|
||||
"fil": "fil-fil",
|
||||
"fr": "fr-fr",
|
||||
@ -203,12 +204,14 @@
|
||||
"gd": "gd-gd",
|
||||
"gl": "gl-gl",
|
||||
"gu": "gu-gu",
|
||||
"ha": "ha-latn",
|
||||
"he": "he-he",
|
||||
"hi": "hi-hi",
|
||||
"hr": "hr-hr",
|
||||
"hu": "hu-hu",
|
||||
"hy": "hy-hy",
|
||||
"id": "id-id",
|
||||
"ig": "ig-ig",
|
||||
"is": "is-is",
|
||||
"it": "it-it",
|
||||
"ja": "ja-ja",
|
||||
@ -218,6 +221,8 @@
|
||||
"kn": "kn-kn",
|
||||
"ko": "ko-ko",
|
||||
"kok": "kok-kok",
|
||||
"ku": "ku-arab",
|
||||
"ky": "ky-ky",
|
||||
"lb": "lb-lb",
|
||||
"lo": "lo-lo",
|
||||
"lt": "lt-lt",
|
||||
@ -225,6 +230,7 @@
|
||||
"mi": "mi-mi",
|
||||
"mk": "mk-mk",
|
||||
"ml": "ml-ml",
|
||||
"mn": "mn-cyrl-mn",
|
||||
"mr": "mr-mr",
|
||||
"ms": "ms-ms",
|
||||
"mt": "mt-mt",
|
||||
@ -232,22 +238,33 @@
|
||||
"ne": "ne-ne",
|
||||
"nl": "nl-nl",
|
||||
"nn": "nn-nn",
|
||||
"nso": "nso-nso",
|
||||
"or": "or-or",
|
||||
"pa_Arab": "pa-arab",
|
||||
"pa_Guru": "pa-guru",
|
||||
"pl": "pl-pl",
|
||||
"pt": "pt-br",
|
||||
"qu": "quz-quz",
|
||||
"quc": "quc-quc",
|
||||
"ro": "ro-ro",
|
||||
"ru": "ru-ru",
|
||||
"rw": "rw-rw",
|
||||
"sd_Arab": "sd-arab",
|
||||
"si": "si-si",
|
||||
"sk": "sk-sk",
|
||||
"sl": "sl-sl",
|
||||
"sq": "sq-sq",
|
||||
"sr_Cyrl": "sr-cyrl",
|
||||
"sr_Latn": "sr-latn",
|
||||
"sv": "sv-sv",
|
||||
"sw": "sw-sw",
|
||||
"ta": "ta-ta",
|
||||
"te": "te-te",
|
||||
"tg": "tg-cyrl",
|
||||
"th": "th-th",
|
||||
"ti": "ti-ti",
|
||||
"tk": "tk-tk",
|
||||
"tn": "tn-tn",
|
||||
"tr": "tr-tr",
|
||||
"tt": "tt-tt",
|
||||
"ug": "ug-ug",
|
||||
@ -255,9 +272,13 @@
|
||||
"ur": "ur-ur",
|
||||
"uz_Latn": "uz-latn",
|
||||
"vi": "vi-vi",
|
||||
"wo": "wo-wo",
|
||||
"xh": "xh-xh",
|
||||
"yo": "yo-yo",
|
||||
"zh": "zh-hans",
|
||||
"zh_Hans": "zh-hans",
|
||||
"zh_Hant": "zh-hant"
|
||||
"zh_Hant": "zh-hant",
|
||||
"zu": "zu-zu"
|
||||
},
|
||||
"regions": {
|
||||
"am-ET": "am-et",
|
||||
@ -473,12 +494,14 @@
|
||||
"kk-KZ": "kk-kz",
|
||||
"km-KH": "km-kh",
|
||||
"ko-KR": "ko-kr",
|
||||
"ky-KG": "ky-kg",
|
||||
"lb-LU": "lb-lu",
|
||||
"lo-LA": "lo-la",
|
||||
"lt-LT": "lt-lt",
|
||||
"lv-LV": "lv-lv",
|
||||
"mi-NZ": "mi-nz",
|
||||
"mk-MK": "mk-mk",
|
||||
"mn-MN": "mn-mn",
|
||||
"ms-BN": "ms-bn",
|
||||
"ms-MY": "ms-my",
|
||||
"ms-SG": "ms-sg",
|
||||
@ -512,6 +535,8 @@
|
||||
"ru-KZ": "ru-kz",
|
||||
"ru-RU": "ru-ru",
|
||||
"ru-UA": "ru-ua",
|
||||
"rw-RW": "rw-rw",
|
||||
"si-LK": "si-lk",
|
||||
"sk-SK": "sk-sk",
|
||||
"sl-SI": "sl-si",
|
||||
"sq-AL": "sq-al",
|
||||
@ -520,14 +545,23 @@
|
||||
"sr-RS": "sr-rs",
|
||||
"sv-FI": "sv-fi",
|
||||
"sv-SE": "sv-se",
|
||||
"sw-KE": "sw-ke",
|
||||
"sw-TZ": "sw-tz",
|
||||
"sw-UG": "sw-ug",
|
||||
"ta-LK": "ta-lk",
|
||||
"ta-SG": "ta-sg",
|
||||
"tg-TJ": "tg-tj",
|
||||
"th-TH": "th-th",
|
||||
"ti-ER": "ti-er",
|
||||
"tk-TM": "tk-tm",
|
||||
"tn-BW": "tn-bw",
|
||||
"tr-CY": "tr-cy",
|
||||
"tr-TR": "tr-tr",
|
||||
"uk-UA": "uk-ua",
|
||||
"ur-PK": "ur-pk",
|
||||
"vi-VN": "vi-vn",
|
||||
"wo-SN": "wo-sn",
|
||||
"yo-NG": "yo-ng",
|
||||
"zh-CN": "zh-cn",
|
||||
"zh-HK": "en-hk",
|
||||
"zh-MO": "zh-mo",
|
||||
@ -560,7 +594,7 @@
|
||||
"es": "es-es",
|
||||
"et": "et-et",
|
||||
"eu": "eu-eu",
|
||||
"fa": "fa-fa",
|
||||
"fa": "prs-prs",
|
||||
"fi": "fi-fi",
|
||||
"fil": "fil-fil",
|
||||
"fr": "fr-fr",
|
||||
@ -568,12 +602,14 @@
|
||||
"gd": "gd-gd",
|
||||
"gl": "gl-gl",
|
||||
"gu": "gu-gu",
|
||||
"ha": "ha-latn",
|
||||
"he": "he-he",
|
||||
"hi": "hi-hi",
|
||||
"hr": "hr-hr",
|
||||
"hu": "hu-hu",
|
||||
"hy": "hy-hy",
|
||||
"id": "id-id",
|
||||
"ig": "ig-ig",
|
||||
"is": "is-is",
|
||||
"it": "it-it",
|
||||
"ja": "ja-ja",
|
||||
@ -583,6 +619,8 @@
|
||||
"kn": "kn-kn",
|
||||
"ko": "ko-ko",
|
||||
"kok": "kok-kok",
|
||||
"ku": "ku-arab",
|
||||
"ky": "ky-ky",
|
||||
"lb": "lb-lb",
|
||||
"lo": "lo-lo",
|
||||
"lt": "lt-lt",
|
||||
@ -590,6 +628,7 @@
|
||||
"mi": "mi-mi",
|
||||
"mk": "mk-mk",
|
||||
"ml": "ml-ml",
|
||||
"mn": "mn-cyrl-mn",
|
||||
"mr": "mr-mr",
|
||||
"ms": "ms-ms",
|
||||
"mt": "mt-mt",
|
||||
@ -597,22 +636,33 @@
|
||||
"ne": "ne-ne",
|
||||
"nl": "nl-nl",
|
||||
"nn": "nn-nn",
|
||||
"nso": "nso-nso",
|
||||
"or": "or-or",
|
||||
"pa_Arab": "pa-arab",
|
||||
"pa_Guru": "pa-guru",
|
||||
"pl": "pl-pl",
|
||||
"pt": "pt-br",
|
||||
"qu": "quz-quz",
|
||||
"quc": "quc-quc",
|
||||
"ro": "ro-ro",
|
||||
"ru": "ru-ru",
|
||||
"rw": "rw-rw",
|
||||
"sd_Arab": "sd-arab",
|
||||
"si": "si-si",
|
||||
"sk": "sk-sk",
|
||||
"sl": "sl-sl",
|
||||
"sq": "sq-sq",
|
||||
"sr_Cyrl": "sr-cyrl",
|
||||
"sr_Latn": "sr-latn",
|
||||
"sv": "sv-sv",
|
||||
"sw": "sw-sw",
|
||||
"ta": "ta-ta",
|
||||
"te": "te-te",
|
||||
"tg": "tg-cyrl",
|
||||
"th": "th-th",
|
||||
"ti": "ti-ti",
|
||||
"tk": "tk-tk",
|
||||
"tn": "tn-tn",
|
||||
"tr": "tr-tr",
|
||||
"tt": "tt-tt",
|
||||
"ug": "ug-ug",
|
||||
@ -620,9 +670,13 @@
|
||||
"ur": "ur-ur",
|
||||
"uz_Latn": "uz-latn",
|
||||
"vi": "vi-vi",
|
||||
"wo": "wo-wo",
|
||||
"xh": "xh-xh",
|
||||
"yo": "yo-yo",
|
||||
"zh": "zh-hans",
|
||||
"zh_Hans": "zh-hans",
|
||||
"zh_Hant": "zh-hant"
|
||||
"zh_Hant": "zh-hant",
|
||||
"zu": "zu-zu"
|
||||
},
|
||||
"regions": {
|
||||
"am-ET": "am-et",
|
||||
@ -838,12 +892,14 @@
|
||||
"kk-KZ": "kk-kz",
|
||||
"km-KH": "km-kh",
|
||||
"ko-KR": "ko-kr",
|
||||
"ky-KG": "ky-kg",
|
||||
"lb-LU": "lb-lu",
|
||||
"lo-LA": "lo-la",
|
||||
"lt-LT": "lt-lt",
|
||||
"lv-LV": "lv-lv",
|
||||
"mi-NZ": "mi-nz",
|
||||
"mk-MK": "mk-mk",
|
||||
"mn-MN": "mn-mn",
|
||||
"ms-BN": "ms-bn",
|
||||
"ms-MY": "ms-my",
|
||||
"ms-SG": "ms-sg",
|
||||
@ -877,6 +933,8 @@
|
||||
"ru-KZ": "ru-kz",
|
||||
"ru-RU": "ru-ru",
|
||||
"ru-UA": "ru-ua",
|
||||
"rw-RW": "rw-rw",
|
||||
"si-LK": "si-lk",
|
||||
"sk-SK": "sk-sk",
|
||||
"sl-SI": "sl-si",
|
||||
"sq-AL": "sq-al",
|
||||
@ -885,14 +943,23 @@
|
||||
"sr-RS": "sr-rs",
|
||||
"sv-FI": "sv-fi",
|
||||
"sv-SE": "sv-se",
|
||||
"sw-KE": "sw-ke",
|
||||
"sw-TZ": "sw-tz",
|
||||
"sw-UG": "sw-ug",
|
||||
"ta-LK": "ta-lk",
|
||||
"ta-SG": "ta-sg",
|
||||
"tg-TJ": "tg-tj",
|
||||
"th-TH": "th-th",
|
||||
"ti-ER": "ti-er",
|
||||
"tk-TM": "tk-tm",
|
||||
"tn-BW": "tn-bw",
|
||||
"tr-CY": "tr-cy",
|
||||
"tr-TR": "tr-tr",
|
||||
"uk-UA": "uk-ua",
|
||||
"ur-PK": "ur-pk",
|
||||
"vi-VN": "vi-vn",
|
||||
"wo-SN": "wo-sn",
|
||||
"yo-NG": "yo-ng",
|
||||
"zh-CN": "zh-cn",
|
||||
"zh-HK": "en-hk",
|
||||
"zh-MO": "zh-mo",
|
||||
@ -925,7 +992,7 @@
|
||||
"es": "es-es",
|
||||
"et": "et-et",
|
||||
"eu": "eu-eu",
|
||||
"fa": "fa-fa",
|
||||
"fa": "prs-prs",
|
||||
"fi": "fi-fi",
|
||||
"fil": "fil-fil",
|
||||
"fr": "fr-fr",
|
||||
@ -933,12 +1000,14 @@
|
||||
"gd": "gd-gd",
|
||||
"gl": "gl-gl",
|
||||
"gu": "gu-gu",
|
||||
"ha": "ha-latn",
|
||||
"he": "he-he",
|
||||
"hi": "hi-hi",
|
||||
"hr": "hr-hr",
|
||||
"hu": "hu-hu",
|
||||
"hy": "hy-hy",
|
||||
"id": "id-id",
|
||||
"ig": "ig-ig",
|
||||
"is": "is-is",
|
||||
"it": "it-it",
|
||||
"ja": "ja-ja",
|
||||
@ -948,6 +1017,8 @@
|
||||
"kn": "kn-kn",
|
||||
"ko": "ko-ko",
|
||||
"kok": "kok-kok",
|
||||
"ku": "ku-arab",
|
||||
"ky": "ky-ky",
|
||||
"lb": "lb-lb",
|
||||
"lo": "lo-lo",
|
||||
"lt": "lt-lt",
|
||||
@ -955,6 +1026,7 @@
|
||||
"mi": "mi-mi",
|
||||
"mk": "mk-mk",
|
||||
"ml": "ml-ml",
|
||||
"mn": "mn-cyrl-mn",
|
||||
"mr": "mr-mr",
|
||||
"ms": "ms-ms",
|
||||
"mt": "mt-mt",
|
||||
@ -962,22 +1034,33 @@
|
||||
"ne": "ne-ne",
|
||||
"nl": "nl-nl",
|
||||
"nn": "nn-nn",
|
||||
"nso": "nso-nso",
|
||||
"or": "or-or",
|
||||
"pa_Arab": "pa-arab",
|
||||
"pa_Guru": "pa-guru",
|
||||
"pl": "pl-pl",
|
||||
"pt": "pt-br",
|
||||
"qu": "quz-quz",
|
||||
"quc": "quc-quc",
|
||||
"ro": "ro-ro",
|
||||
"ru": "ru-ru",
|
||||
"rw": "rw-rw",
|
||||
"sd_Arab": "sd-arab",
|
||||
"si": "si-si",
|
||||
"sk": "sk-sk",
|
||||
"sl": "sl-sl",
|
||||
"sq": "sq-sq",
|
||||
"sr_Cyrl": "sr-cyrl",
|
||||
"sr_Latn": "sr-latn",
|
||||
"sv": "sv-sv",
|
||||
"sw": "sw-sw",
|
||||
"ta": "ta-ta",
|
||||
"te": "te-te",
|
||||
"tg": "tg-cyrl",
|
||||
"th": "th-th",
|
||||
"ti": "ti-ti",
|
||||
"tk": "tk-tk",
|
||||
"tn": "tn-tn",
|
||||
"tr": "tr-tr",
|
||||
"tt": "tt-tt",
|
||||
"ug": "ug-ug",
|
||||
@ -985,9 +1068,13 @@
|
||||
"ur": "ur-ur",
|
||||
"uz_Latn": "uz-latn",
|
||||
"vi": "vi-vi",
|
||||
"wo": "wo-wo",
|
||||
"xh": "xh-xh",
|
||||
"yo": "yo-yo",
|
||||
"zh": "zh-hans",
|
||||
"zh_Hans": "zh-hans",
|
||||
"zh_Hant": "zh-hant"
|
||||
"zh_Hant": "zh-hant",
|
||||
"zu": "zu-zu"
|
||||
},
|
||||
"regions": {
|
||||
"am-ET": "am-et",
|
||||
@ -1203,12 +1290,14 @@
|
||||
"kk-KZ": "kk-kz",
|
||||
"km-KH": "km-kh",
|
||||
"ko-KR": "ko-kr",
|
||||
"ky-KG": "ky-kg",
|
||||
"lb-LU": "lb-lu",
|
||||
"lo-LA": "lo-la",
|
||||
"lt-LT": "lt-lt",
|
||||
"lv-LV": "lv-lv",
|
||||
"mi-NZ": "mi-nz",
|
||||
"mk-MK": "mk-mk",
|
||||
"mn-MN": "mn-mn",
|
||||
"ms-BN": "ms-bn",
|
||||
"ms-MY": "ms-my",
|
||||
"ms-SG": "ms-sg",
|
||||
@ -1242,6 +1331,8 @@
|
||||
"ru-KZ": "ru-kz",
|
||||
"ru-RU": "ru-ru",
|
||||
"ru-UA": "ru-ua",
|
||||
"rw-RW": "rw-rw",
|
||||
"si-LK": "si-lk",
|
||||
"sk-SK": "sk-sk",
|
||||
"sl-SI": "sl-si",
|
||||
"sq-AL": "sq-al",
|
||||
@ -1250,14 +1341,23 @@
|
||||
"sr-RS": "sr-rs",
|
||||
"sv-FI": "sv-fi",
|
||||
"sv-SE": "sv-se",
|
||||
"sw-KE": "sw-ke",
|
||||
"sw-TZ": "sw-tz",
|
||||
"sw-UG": "sw-ug",
|
||||
"ta-LK": "ta-lk",
|
||||
"ta-SG": "ta-sg",
|
||||
"tg-TJ": "tg-tj",
|
||||
"th-TH": "th-th",
|
||||
"ti-ER": "ti-er",
|
||||
"tk-TM": "tk-tm",
|
||||
"tn-BW": "tn-bw",
|
||||
"tr-CY": "tr-cy",
|
||||
"tr-TR": "tr-tr",
|
||||
"uk-UA": "uk-ua",
|
||||
"ur-PK": "ur-pk",
|
||||
"vi-VN": "vi-vn",
|
||||
"wo-SN": "wo-sn",
|
||||
"yo-NG": "yo-ng",
|
||||
"zh-CN": "en-hk",
|
||||
"zh-HK": "en-hk",
|
||||
"zh-MO": "zh-mo",
|
||||
@ -1290,7 +1390,7 @@
|
||||
"es": "es-es",
|
||||
"et": "et-et",
|
||||
"eu": "eu-eu",
|
||||
"fa": "fa-fa",
|
||||
"fa": "prs-prs",
|
||||
"fi": "fi-fi",
|
||||
"fil": "fil-fil",
|
||||
"fr": "fr-fr",
|
||||
@ -1298,12 +1398,14 @@
|
||||
"gd": "gd-gd",
|
||||
"gl": "gl-gl",
|
||||
"gu": "gu-gu",
|
||||
"ha": "ha-latn",
|
||||
"he": "he-he",
|
||||
"hi": "hi-hi",
|
||||
"hr": "hr-hr",
|
||||
"hu": "hu-hu",
|
||||
"hy": "hy-hy",
|
||||
"id": "id-id",
|
||||
"ig": "ig-ig",
|
||||
"is": "is-is",
|
||||
"it": "it-it",
|
||||
"ja": "ja-ja",
|
||||
@ -1313,6 +1415,8 @@
|
||||
"kn": "kn-kn",
|
||||
"ko": "ko-ko",
|
||||
"kok": "kok-kok",
|
||||
"ku": "ku-arab",
|
||||
"ky": "ky-ky",
|
||||
"lb": "lb-lb",
|
||||
"lo": "lo-lo",
|
||||
"lt": "lt-lt",
|
||||
@ -1320,6 +1424,7 @@
|
||||
"mi": "mi-mi",
|
||||
"mk": "mk-mk",
|
||||
"ml": "ml-ml",
|
||||
"mn": "mn-cyrl-mn",
|
||||
"mr": "mr-mr",
|
||||
"ms": "ms-ms",
|
||||
"mt": "mt-mt",
|
||||
@ -1327,22 +1432,33 @@
|
||||
"ne": "ne-ne",
|
||||
"nl": "nl-nl",
|
||||
"nn": "nn-nn",
|
||||
"nso": "nso-nso",
|
||||
"or": "or-or",
|
||||
"pa_Arab": "pa-arab",
|
||||
"pa_Guru": "pa-guru",
|
||||
"pl": "pl-pl",
|
||||
"pt": "pt-br",
|
||||
"qu": "quz-quz",
|
||||
"quc": "quc-quc",
|
||||
"ro": "ro-ro",
|
||||
"ru": "ru-ru",
|
||||
"rw": "rw-rw",
|
||||
"sd_Arab": "sd-arab",
|
||||
"si": "si-si",
|
||||
"sk": "sk-sk",
|
||||
"sl": "sl-sl",
|
||||
"sq": "sq-sq",
|
||||
"sr_Cyrl": "sr-cyrl",
|
||||
"sr_Latn": "sr-latn",
|
||||
"sv": "sv-sv",
|
||||
"sw": "sw-sw",
|
||||
"ta": "ta-ta",
|
||||
"te": "te-te",
|
||||
"tg": "tg-cyrl",
|
||||
"th": "th-th",
|
||||
"ti": "ti-ti",
|
||||
"tk": "tk-tk",
|
||||
"tn": "tn-tn",
|
||||
"tr": "tr-tr",
|
||||
"tt": "tt-tt",
|
||||
"ug": "ug-ug",
|
||||
@ -1350,9 +1466,13 @@
|
||||
"ur": "ur-ur",
|
||||
"uz_Latn": "uz-latn",
|
||||
"vi": "vi-vi",
|
||||
"wo": "wo-wo",
|
||||
"xh": "xh-xh",
|
||||
"yo": "yo-yo",
|
||||
"zh": "zh-hans",
|
||||
"zh_Hans": "zh-hans",
|
||||
"zh_Hant": "zh-hant"
|
||||
"zh_Hant": "zh-hant",
|
||||
"zu": "zu-zu"
|
||||
},
|
||||
"regions": {
|
||||
"am-ET": "am-et",
|
||||
@ -1568,12 +1688,14 @@
|
||||
"kk-KZ": "kk-kz",
|
||||
"km-KH": "km-kh",
|
||||
"ko-KR": "ko-kr",
|
||||
"ky-KG": "ky-kg",
|
||||
"lb-LU": "lb-lu",
|
||||
"lo-LA": "lo-la",
|
||||
"lt-LT": "lt-lt",
|
||||
"lv-LV": "lv-lv",
|
||||
"mi-NZ": "mi-nz",
|
||||
"mk-MK": "mk-mk",
|
||||
"mn-MN": "mn-mn",
|
||||
"ms-BN": "ms-bn",
|
||||
"ms-MY": "ms-my",
|
||||
"ms-SG": "ms-sg",
|
||||
@ -1607,6 +1729,8 @@
|
||||
"ru-KZ": "ru-kz",
|
||||
"ru-RU": "ru-ru",
|
||||
"ru-UA": "ru-ua",
|
||||
"rw-RW": "rw-rw",
|
||||
"si-LK": "si-lk",
|
||||
"sk-SK": "sk-sk",
|
||||
"sl-SI": "sl-si",
|
||||
"sq-AL": "sq-al",
|
||||
@ -1615,14 +1739,23 @@
|
||||
"sr-RS": "sr-rs",
|
||||
"sv-FI": "sv-fi",
|
||||
"sv-SE": "sv-se",
|
||||
"sw-KE": "sw-ke",
|
||||
"sw-TZ": "sw-tz",
|
||||
"sw-UG": "sw-ug",
|
||||
"ta-LK": "ta-lk",
|
||||
"ta-SG": "ta-sg",
|
||||
"tg-TJ": "tg-tj",
|
||||
"th-TH": "th-th",
|
||||
"ti-ER": "ti-er",
|
||||
"tk-TM": "tk-tm",
|
||||
"tn-BW": "tn-bw",
|
||||
"tr-CY": "tr-cy",
|
||||
"tr-TR": "tr-tr",
|
||||
"uk-UA": "uk-ua",
|
||||
"ur-PK": "ur-pk",
|
||||
"vi-VN": "vi-vn",
|
||||
"wo-SN": "wo-sn",
|
||||
"yo-NG": "yo-ng",
|
||||
"zh-CN": "zh-cn",
|
||||
"zh-HK": "en-hk",
|
||||
"zh-MO": "zh-mo",
|
||||
|
@ -2574,11 +2574,6 @@
|
||||
"symbol": "ʰ",
|
||||
"to_si_factor": 0.2617993878
|
||||
},
|
||||
"Q11644875": {
|
||||
"si_name": "Q12438",
|
||||
"symbol": "Tf",
|
||||
"to_si_factor": 9806.65
|
||||
},
|
||||
"Q1165639": {
|
||||
"si_name": "Q89992008",
|
||||
"symbol": "daraf",
|
||||
@ -3179,6 +3174,21 @@
|
||||
"symbol": "₿",
|
||||
"to_si_factor": null
|
||||
},
|
||||
"Q131824443": {
|
||||
"si_name": "Q12438",
|
||||
"symbol": "tf",
|
||||
"to_si_factor": 9806.65
|
||||
},
|
||||
"Q131824444": {
|
||||
"si_name": "Q12438",
|
||||
"symbol": "LTf",
|
||||
"to_si_factor": 9964.01641818352
|
||||
},
|
||||
"Q131824445": {
|
||||
"si_name": "Q12438",
|
||||
"symbol": "STf",
|
||||
"to_si_factor": 8896.443230521
|
||||
},
|
||||
"Q1322380": {
|
||||
"si_name": "Q11574",
|
||||
"symbol": "Ts",
|
||||
@ -3420,7 +3430,7 @@
|
||||
"to_si_factor": null
|
||||
},
|
||||
"Q1628990": {
|
||||
"si_name": "Q12874593",
|
||||
"si_name": "Q12831618",
|
||||
"symbol": "hph",
|
||||
"to_si_factor": 745.7
|
||||
},
|
||||
|
@ -139,6 +139,7 @@ from searx.utils import (
|
||||
get_embeded_stream_url,
|
||||
)
|
||||
from searx.enginelib.traits import EngineTraits
|
||||
from searx.result_types import EngineResults
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import logging
|
||||
@ -248,7 +249,7 @@ def _extract_published_date(published_date_raw):
|
||||
return None
|
||||
|
||||
|
||||
def response(resp):
|
||||
def response(resp) -> EngineResults:
|
||||
|
||||
if brave_category in ('search', 'goggles'):
|
||||
return _parse_search(resp)
|
||||
@ -269,15 +270,19 @@ def response(resp):
|
||||
raise ValueError(f"Unsupported brave category: {brave_category}")
|
||||
|
||||
|
||||
def _parse_search(resp):
|
||||
def _parse_search(resp) -> EngineResults:
|
||||
result_list = EngineResults()
|
||||
|
||||
result_list = []
|
||||
dom = html.fromstring(resp.text)
|
||||
|
||||
# I doubt that Brave is still providing the "answer" class / I haven't seen
|
||||
# answers in brave for a long time.
|
||||
answer_tag = eval_xpath_getindex(dom, '//div[@class="answer"]', 0, default=None)
|
||||
if answer_tag:
|
||||
url = eval_xpath_getindex(dom, '//div[@id="featured_snippet"]/a[@class="result-header"]/@href', 0, default=None)
|
||||
result_list.append({'answer': extract_text(answer_tag), 'url': url})
|
||||
answer = extract_text(answer_tag)
|
||||
if answer is not None:
|
||||
result_list.add(result_list.types.Answer(answer=answer, url=url))
|
||||
|
||||
# xpath_results = '//div[contains(@class, "snippet fdb") and @data-type="web"]'
|
||||
xpath_results = '//div[contains(@class, "snippet ")]'
|
||||
@ -291,15 +296,21 @@ def _parse_search(resp):
|
||||
if url is None or title_tag is None or not urlparse(url).netloc: # partial url likely means it's an ad
|
||||
continue
|
||||
|
||||
content_tag = eval_xpath_getindex(result, './/div[contains(@class, "snippet-description")]', 0, default='')
|
||||
content: str = extract_text(
|
||||
eval_xpath_getindex(result, './/div[contains(@class, "snippet-description")]', 0, default='')
|
||||
) # type: ignore
|
||||
pub_date_raw = eval_xpath(result, 'substring-before(.//div[contains(@class, "snippet-description")], "-")')
|
||||
pub_date = _extract_published_date(pub_date_raw)
|
||||
if pub_date and content.startswith(pub_date_raw):
|
||||
content = content.lstrip(pub_date_raw).strip("- \n\t")
|
||||
|
||||
thumbnail = eval_xpath_getindex(result, './/img[contains(@class, "thumb")]/@src', 0, default='')
|
||||
|
||||
item = {
|
||||
'url': url,
|
||||
'title': extract_text(title_tag),
|
||||
'content': extract_text(content_tag),
|
||||
'publishedDate': _extract_published_date(pub_date_raw),
|
||||
'content': content,
|
||||
'publishedDate': pub_date,
|
||||
'thumbnail': thumbnail,
|
||||
}
|
||||
|
||||
@ -328,8 +339,8 @@ def _parse_search(resp):
|
||||
return result_list
|
||||
|
||||
|
||||
def _parse_news(json_resp):
|
||||
result_list = []
|
||||
def _parse_news(json_resp) -> EngineResults:
|
||||
result_list = EngineResults()
|
||||
|
||||
for result in json_resp["results"]:
|
||||
item = {
|
||||
@ -345,8 +356,8 @@ def _parse_news(json_resp):
|
||||
return result_list
|
||||
|
||||
|
||||
def _parse_images(json_resp):
|
||||
result_list = []
|
||||
def _parse_images(json_resp) -> EngineResults:
|
||||
result_list = EngineResults()
|
||||
|
||||
for result in json_resp["results"]:
|
||||
item = {
|
||||
@ -364,8 +375,8 @@ def _parse_images(json_resp):
|
||||
return result_list
|
||||
|
||||
|
||||
def _parse_videos(json_resp):
|
||||
result_list = []
|
||||
def _parse_videos(json_resp) -> EngineResults:
|
||||
result_list = EngineResults()
|
||||
|
||||
for result in json_resp["results"]:
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Deepl translation engine"""
|
||||
|
||||
from json import loads
|
||||
from searx.result_types import EngineResults
|
||||
|
||||
about = {
|
||||
"website": 'https://deepl.com',
|
||||
@ -39,18 +39,14 @@ def request(_query, params):
|
||||
return params
|
||||
|
||||
|
||||
def response(resp):
|
||||
results = []
|
||||
result = loads(resp.text)
|
||||
translations = result['translations']
|
||||
def response(resp) -> EngineResults:
|
||||
|
||||
infobox = "<dl>"
|
||||
res = EngineResults()
|
||||
data = resp.json()
|
||||
if not data.get('translations'):
|
||||
return res
|
||||
|
||||
for translation in translations:
|
||||
infobox += f"<dd>{translation['text']}</dd>"
|
||||
translations = [res.types.Translations.Item(text=t['text']) for t in data['translations']]
|
||||
res.add(res.types.Translations(translations=translations))
|
||||
|
||||
infobox += "</dl>"
|
||||
|
||||
results.append({'answer': infobox})
|
||||
|
||||
return results
|
||||
return res
|
||||
|
@ -13,6 +13,7 @@ close to the implementation, its just a simple example. To get in use of this
|
||||
"""
|
||||
|
||||
import json
|
||||
from searx.result_types import EngineResults
|
||||
|
||||
engine_type = 'offline'
|
||||
categories = ['general']
|
||||
@ -48,14 +49,14 @@ def init(engine_settings=None):
|
||||
)
|
||||
|
||||
|
||||
def search(query, request_params):
|
||||
def search(query, request_params) -> EngineResults:
|
||||
"""Query (offline) engine and return results. Assemble the list of results from
|
||||
your local engine. In this demo engine we ignore the 'query' term, usual
|
||||
you would pass the 'query' term to your local engine to filter out the
|
||||
results.
|
||||
|
||||
"""
|
||||
ret_val = []
|
||||
res = EngineResults()
|
||||
|
||||
result_list = json.loads(_my_offline_engine)
|
||||
|
||||
@ -67,6 +68,6 @@ def search(query, request_params):
|
||||
# choose a result template or comment out to use the *default*
|
||||
'template': 'key-value.html',
|
||||
}
|
||||
ret_val.append(entry)
|
||||
res.append(entry)
|
||||
|
||||
return ret_val
|
||||
return res
|
||||
|
@ -17,6 +17,7 @@ list in ``settings.yml``:
|
||||
|
||||
from json import loads
|
||||
from urllib.parse import urlencode
|
||||
from searx.result_types import EngineResults
|
||||
|
||||
engine_type = 'online'
|
||||
send_accept_language_header = True
|
||||
@ -70,21 +71,28 @@ def request(query, params):
|
||||
return params
|
||||
|
||||
|
||||
def response(resp):
|
||||
def response(resp) -> EngineResults:
|
||||
"""Parse out the result items from the response. In this example we parse the
|
||||
response from `api.artic.edu <https://artic.edu>`__ and filter out all
|
||||
images.
|
||||
|
||||
"""
|
||||
results = []
|
||||
res = EngineResults()
|
||||
json_data = loads(resp.text)
|
||||
|
||||
res.add(
|
||||
res.types.Answer(
|
||||
answer="this is a dummy answer ..",
|
||||
url="https://example.org",
|
||||
)
|
||||
)
|
||||
|
||||
for result in json_data['data']:
|
||||
|
||||
if not result['image_id']:
|
||||
continue
|
||||
|
||||
results.append(
|
||||
res.append(
|
||||
{
|
||||
'url': 'https://artic.edu/artworks/%(id)s' % result,
|
||||
'title': result['title'] + " (%(date_display)s) // %(artist_display)s" % result,
|
||||
@ -95,4 +103,4 @@ def response(resp):
|
||||
}
|
||||
)
|
||||
|
||||
return results
|
||||
return res
|
||||
|
@ -3,9 +3,12 @@
|
||||
Dictzone
|
||||
"""
|
||||
|
||||
from urllib.parse import urljoin
|
||||
import urllib.parse
|
||||
from lxml import html
|
||||
from searx.utils import eval_xpath
|
||||
|
||||
from searx.utils import eval_xpath, extract_text
|
||||
from searx.result_types import EngineResults
|
||||
from searx.network import get as http_get # https://github.com/searxng/searxng/issues/762
|
||||
|
||||
# about
|
||||
about = {
|
||||
@ -19,42 +22,84 @@ about = {
|
||||
|
||||
engine_type = 'online_dictionary'
|
||||
categories = ['general', 'translate']
|
||||
url = 'https://dictzone.com/{from_lang}-{to_lang}-dictionary/{query}'
|
||||
base_url = "https://dictzone.com"
|
||||
weight = 100
|
||||
|
||||
results_xpath = './/table[@id="r"]/tr'
|
||||
https_support = True
|
||||
|
||||
|
||||
def request(query, params): # pylint: disable=unused-argument
|
||||
params['url'] = url.format(from_lang=params['from_lang'][2], to_lang=params['to_lang'][2], query=params['query'])
|
||||
|
||||
from_lang = params["from_lang"][2] # "english"
|
||||
to_lang = params["to_lang"][2] # "german"
|
||||
query = params["query"]
|
||||
|
||||
params["url"] = f"{base_url}/{from_lang}-{to_lang}-dictionary/{urllib.parse.quote_plus(query)}"
|
||||
return params
|
||||
|
||||
|
||||
def response(resp):
|
||||
results = []
|
||||
def _clean_up_node(node):
|
||||
for x in ["./i", "./span", "./button"]:
|
||||
for n in node.xpath(x):
|
||||
n.getparent().remove(n)
|
||||
|
||||
|
||||
def response(resp) -> EngineResults:
|
||||
results = EngineResults()
|
||||
|
||||
item_list = []
|
||||
|
||||
if not resp.ok:
|
||||
return results
|
||||
|
||||
dom = html.fromstring(resp.text)
|
||||
|
||||
for k, result in enumerate(eval_xpath(dom, results_xpath)[1:]):
|
||||
try:
|
||||
from_result, to_results_raw = eval_xpath(result, './td')
|
||||
except: # pylint: disable=bare-except
|
||||
for result in eval_xpath(dom, ".//table[@id='r']//tr"):
|
||||
|
||||
# each row is an Translations.Item
|
||||
|
||||
td_list = result.xpath("./td")
|
||||
if len(td_list) != 2:
|
||||
# ignore header columns "tr/th"
|
||||
continue
|
||||
|
||||
to_results = []
|
||||
for to_result in eval_xpath(to_results_raw, './p/a'):
|
||||
t = to_result.text_content()
|
||||
if t.strip():
|
||||
to_results.append(to_result.text_content())
|
||||
col_from, col_to = td_list
|
||||
_clean_up_node(col_from)
|
||||
|
||||
results.append(
|
||||
{
|
||||
'url': urljoin(str(resp.url), '?%d' % k),
|
||||
'title': from_result.text_content(),
|
||||
'content': '; '.join(to_results),
|
||||
}
|
||||
)
|
||||
text = f"{extract_text(col_from)}"
|
||||
|
||||
synonyms = []
|
||||
p_list = col_to.xpath(".//p")
|
||||
|
||||
for i, p_item in enumerate(p_list):
|
||||
|
||||
smpl: str = extract_text(p_list[i].xpath("./i[@class='smpl']")) # type: ignore
|
||||
_clean_up_node(p_item)
|
||||
p_text: str = extract_text(p_item) # type: ignore
|
||||
|
||||
if smpl:
|
||||
p_text += " // " + smpl
|
||||
|
||||
if i == 0:
|
||||
text += f" : {p_text}"
|
||||
continue
|
||||
|
||||
synonyms.append(p_text)
|
||||
|
||||
item = results.types.Translations.Item(text=text, synonyms=synonyms)
|
||||
item_list.append(item)
|
||||
|
||||
# the "autotranslate" of dictzone is loaded by the JS from URL:
|
||||
# https://dictzone.com/trans/hello%20world/en_de
|
||||
|
||||
from_lang = resp.search_params["from_lang"][1] # "en"
|
||||
to_lang = resp.search_params["to_lang"][1] # "de"
|
||||
query = resp.search_params["query"]
|
||||
|
||||
# works only sometimes?
|
||||
autotranslate = http_get(f"{base_url}/trans/{query}/{from_lang}_{to_lang}", timeout=1.0)
|
||||
if autotranslate.ok and autotranslate.text:
|
||||
item_list.insert(0, results.types.Translations.Item(text=autotranslate.text))
|
||||
|
||||
if item_list:
|
||||
results.add(results.types.Translations(translations=item_list, url=resp.search_params["url"]))
|
||||
return results
|
||||
|
@ -27,6 +27,7 @@ from searx.network import get # see https://github.com/searxng/searxng/issues/7
|
||||
from searx import redisdb
|
||||
from searx.enginelib.traits import EngineTraits
|
||||
from searx.exceptions import SearxEngineCaptchaException
|
||||
from searx.result_types import EngineResults
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import logging
|
||||
@ -354,12 +355,12 @@ def is_ddg_captcha(dom):
|
||||
return bool(eval_xpath(dom, "//form[@id='challenge-form']"))
|
||||
|
||||
|
||||
def response(resp):
|
||||
def response(resp) -> EngineResults:
|
||||
results = EngineResults()
|
||||
|
||||
if resp.status_code == 303:
|
||||
return []
|
||||
return results
|
||||
|
||||
results = []
|
||||
doc = lxml.html.fromstring(resp.text)
|
||||
|
||||
if is_ddg_captcha(doc):
|
||||
@ -397,12 +398,14 @@ def response(resp):
|
||||
and "URL Decoded:" not in zero_click
|
||||
):
|
||||
current_query = resp.search_params["data"].get("q")
|
||||
|
||||
results.append(
|
||||
{
|
||||
'answer': zero_click,
|
||||
'url': "https://duckduckgo.com/?" + urlencode({"q": current_query}),
|
||||
}
|
||||
results.add(
|
||||
results.types.Answer(
|
||||
answer=zero_click,
|
||||
url="https://duckduckgo.com/?"
|
||||
+ urlencode(
|
||||
{"q": current_query},
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return results
|
||||
|
@ -21,6 +21,7 @@ from lxml import html
|
||||
from searx.data import WIKIDATA_UNITS
|
||||
from searx.utils import extract_text, html_to_text, get_string_replaces_function
|
||||
from searx.external_urls import get_external_url, get_earth_coordinates_url, area_to_osm_zoom
|
||||
from searx.result_types import EngineResults
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import logging
|
||||
@ -75,9 +76,9 @@ def request(query, params):
|
||||
return params
|
||||
|
||||
|
||||
def response(resp):
|
||||
def response(resp) -> EngineResults:
|
||||
# pylint: disable=too-many-locals, too-many-branches, too-many-statements
|
||||
results = []
|
||||
results = EngineResults()
|
||||
|
||||
search_res = resp.json()
|
||||
|
||||
@ -99,9 +100,15 @@ def response(resp):
|
||||
# add answer if there is one
|
||||
answer = search_res.get('Answer', '')
|
||||
if answer:
|
||||
logger.debug('AnswerType="%s" Answer="%s"', search_res.get('AnswerType'), answer)
|
||||
if search_res.get('AnswerType') not in ['calc', 'ip']:
|
||||
results.append({'answer': html_to_text(answer), 'url': search_res.get('AbstractURL', '')})
|
||||
answer_type = search_res.get('AnswerType')
|
||||
logger.debug('AnswerType="%s" Answer="%s"', answer_type, answer)
|
||||
if isinstance(answer, str) and answer_type not in ['calc', 'ip']:
|
||||
results.add(
|
||||
results.types.Answer(
|
||||
answer=html_to_text(answer),
|
||||
url=search_res.get('AbstractURL', ''),
|
||||
)
|
||||
)
|
||||
|
||||
# add infobox
|
||||
if 'Definition' in search_res:
|
||||
|
@ -25,6 +25,7 @@ from searx.locales import language_tag, region_tag, get_official_locales
|
||||
from searx.network import get # see https://github.com/searxng/searxng/issues/762
|
||||
from searx.exceptions import SearxEngineCaptchaException
|
||||
from searx.enginelib.traits import EngineTraits
|
||||
from searx.result_types import EngineResults
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import logging
|
||||
@ -315,12 +316,12 @@ def _parse_data_images(dom):
|
||||
return data_image_map
|
||||
|
||||
|
||||
def response(resp):
|
||||
def response(resp) -> EngineResults:
|
||||
"""Get response from google's search request"""
|
||||
# pylint: disable=too-many-branches, too-many-statements
|
||||
detect_google_sorry(resp)
|
||||
|
||||
results = []
|
||||
results = EngineResults()
|
||||
|
||||
# convert the text to dom
|
||||
dom = html.fromstring(resp.text)
|
||||
@ -331,11 +332,11 @@ def response(resp):
|
||||
for item in answer_list:
|
||||
for bubble in eval_xpath(item, './/div[@class="nnFGuf"]'):
|
||||
bubble.drop_tree()
|
||||
results.append(
|
||||
{
|
||||
'answer': extract_text(item),
|
||||
'url': (eval_xpath(item, '../..//a/@href') + [None])[0],
|
||||
}
|
||||
results.add(
|
||||
results.types.Answer(
|
||||
answer=extract_text(item),
|
||||
url=(eval_xpath(item, '../..//a/@href') + [None])[0],
|
||||
)
|
||||
)
|
||||
|
||||
# parse results
|
||||
|
76
searx/engines/ipernity.py
Normal file
76
searx/engines/ipernity.py
Normal file
@ -0,0 +1,76 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Ipernity (images)"""
|
||||
|
||||
from datetime import datetime
|
||||
from json import loads, JSONDecodeError
|
||||
|
||||
from urllib.parse import quote_plus
|
||||
from lxml import html
|
||||
|
||||
from searx.utils import extr, extract_text, eval_xpath, eval_xpath_list
|
||||
|
||||
about = {
|
||||
'website': 'https://www.ipernity.com',
|
||||
'official_api_documentation': 'https://www.ipernity.com/help/api',
|
||||
'use_official_api': False,
|
||||
'require_api_key': False,
|
||||
'results': 'HTML',
|
||||
}
|
||||
|
||||
paging = True
|
||||
categories = ['images']
|
||||
|
||||
|
||||
base_url = 'https://www.ipernity.com'
|
||||
page_size = 10
|
||||
|
||||
|
||||
def request(query, params):
|
||||
params['url'] = f"{base_url}/search/photo/@/page:{params['pageno']}:{page_size}?q={quote_plus(query)}"
|
||||
return params
|
||||
|
||||
|
||||
def response(resp):
|
||||
results = []
|
||||
|
||||
doc = html.fromstring(resp.text)
|
||||
|
||||
images = eval_xpath_list(doc, '//a[starts-with(@href, "/doc")]//img')
|
||||
|
||||
result_index = 0
|
||||
for result in eval_xpath_list(doc, '//script[@type="text/javascript"]'):
|
||||
info_js = extr(extract_text(result), '] = ', '};') + '}'
|
||||
|
||||
if not info_js:
|
||||
continue
|
||||
|
||||
try:
|
||||
info_item = loads(info_js)
|
||||
|
||||
if not info_item.get('mediakey'):
|
||||
continue
|
||||
|
||||
thumbnail_src = extract_text(eval_xpath(images[result_index], './@src'))
|
||||
img_src = thumbnail_src.replace('240.jpg', '640.jpg')
|
||||
|
||||
resolution = None
|
||||
if info_item.get("width") and info_item.get("height"):
|
||||
resolution = f'{info_item["width"]}x{info_item["height"]}'
|
||||
|
||||
item = {
|
||||
'template': 'images.html',
|
||||
'url': f"{base_url}/doc/{info_item['user_id']}/{info_item['doc_id']}",
|
||||
'title': info_item.get('title'),
|
||||
'content': info_item.get('content', ''),
|
||||
'resolution': resolution,
|
||||
'publishedDate': datetime.fromtimestamp(int(info_item['posted_at'])),
|
||||
'thumbnail_src': thumbnail_src,
|
||||
'img_src': img_src,
|
||||
}
|
||||
results.append(item)
|
||||
|
||||
result_index += 1
|
||||
except JSONDecodeError:
|
||||
continue
|
||||
|
||||
return results
|
@ -2,7 +2,8 @@
|
||||
"""LibreTranslate (Free and Open Source Machine Translation API)"""
|
||||
|
||||
import random
|
||||
from json import dumps
|
||||
import json
|
||||
from searx.result_types import EngineResults
|
||||
|
||||
about = {
|
||||
"website": 'https://libretranslate.com',
|
||||
@ -16,19 +17,27 @@ about = {
|
||||
engine_type = 'online_dictionary'
|
||||
categories = ['general', 'translate']
|
||||
|
||||
base_url = "https://translate.terraprint.co"
|
||||
api_key = ''
|
||||
base_url = "https://libretranslate.com/translate"
|
||||
api_key = ""
|
||||
|
||||
|
||||
def request(_query, params):
|
||||
request_url = random.choice(base_url) if isinstance(base_url, list) else base_url
|
||||
|
||||
if request_url.startswith("https://libretranslate.com") and not api_key:
|
||||
return None
|
||||
params['url'] = f"{request_url}/translate"
|
||||
|
||||
args = {'source': params['from_lang'][1], 'target': params['to_lang'][1], 'q': params['query']}
|
||||
args = {
|
||||
'q': params['query'],
|
||||
'source': params['from_lang'][1],
|
||||
'target': params['to_lang'][1],
|
||||
'alternatives': 3,
|
||||
}
|
||||
if api_key:
|
||||
args['api_key'] = api_key
|
||||
params['data'] = dumps(args)
|
||||
|
||||
params['data'] = json.dumps(args)
|
||||
params['method'] = 'POST'
|
||||
params['headers'] = {'Content-Type': 'application/json'}
|
||||
params['req_url'] = request_url
|
||||
@ -36,18 +45,15 @@ def request(_query, params):
|
||||
return params
|
||||
|
||||
|
||||
def response(resp):
|
||||
results = []
|
||||
def response(resp) -> EngineResults:
|
||||
results = EngineResults()
|
||||
|
||||
json_resp = resp.json()
|
||||
text = json_resp.get('translatedText')
|
||||
if not text:
|
||||
return results
|
||||
|
||||
from_lang = resp.search_params["from_lang"][1]
|
||||
to_lang = resp.search_params["to_lang"][1]
|
||||
query = resp.search_params["query"]
|
||||
req_url = resp.search_params["req_url"]
|
||||
|
||||
if text:
|
||||
results.append({"answer": text, "url": f"{req_url}/?source={from_lang}&target={to_lang}&q={query}"})
|
||||
item = results.types.Translations.Item(text=text, examples=json_resp.get('alternatives', []))
|
||||
results.add(results.types.Translations(translations=[item]))
|
||||
|
||||
return results
|
||||
|
@ -1,7 +1,7 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Lingva (alternative Google Translate frontend)"""
|
||||
|
||||
from json import loads
|
||||
from searx.result_types import EngineResults
|
||||
|
||||
about = {
|
||||
"website": 'https://lingva.ml',
|
||||
@ -16,20 +16,17 @@ engine_type = 'online_dictionary'
|
||||
categories = ['general', 'translate']
|
||||
|
||||
url = "https://lingva.thedaviddelta.com"
|
||||
search_url = "{url}/api/v1/{from_lang}/{to_lang}/{query}"
|
||||
|
||||
|
||||
def request(_query, params):
|
||||
params['url'] = search_url.format(
|
||||
url=url, from_lang=params['from_lang'][1], to_lang=params['to_lang'][1], query=params['query']
|
||||
)
|
||||
params['url'] = f"{url}/api/v1/{params['from_lang'][1]}/{params['to_lang'][1]}/{params['query']}"
|
||||
return params
|
||||
|
||||
|
||||
def response(resp):
|
||||
results = []
|
||||
def response(resp) -> EngineResults:
|
||||
results = EngineResults()
|
||||
|
||||
result = loads(resp.text)
|
||||
result = resp.json()
|
||||
info = result["info"]
|
||||
from_to_prefix = "%s-%s " % (resp.search_params['from_lang'][1], resp.search_params['to_lang'][1])
|
||||
|
||||
@ -38,28 +35,40 @@ def response(resp):
|
||||
|
||||
if 'definitions' in info: # pylint: disable=too-many-nested-blocks
|
||||
for definition in info['definitions']:
|
||||
if 'list' in definition:
|
||||
for item in definition['list']:
|
||||
if 'synonyms' in item:
|
||||
for synonym in item['synonyms']:
|
||||
results.append({"suggestion": from_to_prefix + synonym})
|
||||
for item in definition.get('list', []):
|
||||
for synonym in item.get('synonyms', []):
|
||||
results.append({"suggestion": from_to_prefix + synonym})
|
||||
|
||||
infobox = ""
|
||||
data = []
|
||||
|
||||
for definition in info['definitions']:
|
||||
for translation in definition['list']:
|
||||
data.append(
|
||||
results.types.Translations.Item(
|
||||
text=result['translation'],
|
||||
definitions=[translation['definition']] if translation['definition'] else [],
|
||||
examples=[translation['example']] if translation['example'] else [],
|
||||
synonyms=translation['synonyms'],
|
||||
)
|
||||
)
|
||||
|
||||
for translation in info["extraTranslations"]:
|
||||
for word in translation["list"]:
|
||||
infobox += f"<dl><dt>{word['word']}</dt>"
|
||||
data.append(
|
||||
results.types.Translations.Item(
|
||||
text=word['word'],
|
||||
definitions=word['meanings'],
|
||||
)
|
||||
)
|
||||
|
||||
for meaning in word["meanings"]:
|
||||
infobox += f"<dd>{meaning}</dd>"
|
||||
if not data and result['translation']:
|
||||
data.append(results.types.Translations.Item(text=result['translation']))
|
||||
|
||||
infobox += "</dl>"
|
||||
|
||||
results.append(
|
||||
{
|
||||
'infobox': result["translation"],
|
||||
'content': infobox,
|
||||
}
|
||||
params = resp.search_params
|
||||
results.add(
|
||||
results.types.Translations(
|
||||
translations=data,
|
||||
url=f"{url}/{params['from_lang'][1]}/{params['to_lang'][1]}/{params['query']}",
|
||||
)
|
||||
)
|
||||
|
||||
return results
|
||||
|
@ -3,8 +3,9 @@
|
||||
|
||||
import random
|
||||
import re
|
||||
from urllib.parse import urlencode
|
||||
from flask_babel import gettext
|
||||
import urllib.parse
|
||||
|
||||
from searx.result_types import EngineResults
|
||||
|
||||
about = {
|
||||
"website": 'https://codeberg.org/aryak/mozhi',
|
||||
@ -28,37 +29,33 @@ def request(_query, params):
|
||||
request_url = random.choice(base_url) if isinstance(base_url, list) else base_url
|
||||
|
||||
args = {'from': params['from_lang'][1], 'to': params['to_lang'][1], 'text': params['query'], 'engine': mozhi_engine}
|
||||
params['url'] = f"{request_url}/api/translate?{urlencode(args)}"
|
||||
params['url'] = f"{request_url}/api/translate?{urllib.parse.urlencode(args)}"
|
||||
return params
|
||||
|
||||
|
||||
def response(resp):
|
||||
def response(resp) -> EngineResults:
|
||||
res = EngineResults()
|
||||
translation = resp.json()
|
||||
|
||||
infobox = ""
|
||||
item = res.types.Translations.Item(text=translation['translated-text'])
|
||||
|
||||
if translation['target_transliteration'] and not re.match(
|
||||
re_transliteration_unsupported, translation['target_transliteration']
|
||||
):
|
||||
infobox = f"<b>{translation['target_transliteration']}</b>"
|
||||
item.transliteration = translation['target_transliteration']
|
||||
|
||||
if translation['word_choices']:
|
||||
for word in translation['word_choices']:
|
||||
infobox += f"<dl><dt>{word['word']}: {word['definition']}</dt>"
|
||||
if word.get('definition'):
|
||||
item.definitions.append(word['definition'])
|
||||
|
||||
if word['examples_target']:
|
||||
for example in word['examples_target']:
|
||||
infobox += f"<dd>{re.sub(r'<|>', '', example)}</dd>"
|
||||
infobox += f"<dd>{re.sub(r'<|>', '', example)}</dd>"
|
||||
for example in word.get('examples_target', []):
|
||||
item.examples.append(re.sub(r"<|>", "", example).lstrip('- '))
|
||||
|
||||
infobox += "</dl>"
|
||||
item.synonyms = translation.get('source_synonyms', [])
|
||||
|
||||
if translation['source_synonyms']:
|
||||
infobox += f"<dl><dt>{gettext('Synonyms')}: {', '.join(translation['source_synonyms'])}</dt></dl>"
|
||||
|
||||
result = {
|
||||
'infobox': translation['translated-text'],
|
||||
'content': infobox,
|
||||
}
|
||||
|
||||
return [result]
|
||||
url = urllib.parse.urlparse(resp.search_params["url"])
|
||||
# remove the api path
|
||||
url = url._replace(path="", fragment="").geturl()
|
||||
res.add(res.types.Translations(translations=[item], url=url))
|
||||
return res
|
||||
|
@ -4,16 +4,16 @@
|
||||
"""
|
||||
|
||||
import re
|
||||
from json import loads
|
||||
from urllib.parse import urlencode
|
||||
import urllib.parse
|
||||
|
||||
from functools import partial
|
||||
|
||||
from flask_babel import gettext
|
||||
|
||||
from searx.data import OSM_KEYS_TAGS, CURRENCIES
|
||||
from searx.utils import searx_useragent
|
||||
from searx.external_urls import get_external_url
|
||||
from searx.engines.wikidata import send_wikidata_query, sparql_string_escape, get_thumbnail
|
||||
from searx.result_types import EngineResults
|
||||
|
||||
# about
|
||||
about = {
|
||||
@ -37,8 +37,7 @@ search_string = 'search?{query}&polygon_geojson=1&format=jsonv2&addressdetails=1
|
||||
result_id_url = 'https://openstreetmap.org/{osm_type}/{osm_id}'
|
||||
result_lat_lon_url = 'https://www.openstreetmap.org/?mlat={lat}&mlon={lon}&zoom={zoom}&layers=M'
|
||||
|
||||
route_url = 'https://graphhopper.com/maps/?point={}&point={}&locale=en-US&vehicle=car&weighting=fastest&turn_costs=true&use_miles=false&layer=Omniscale' # pylint: disable=line-too-long
|
||||
route_re = re.compile('(?:from )?(.+) to (.+)')
|
||||
route_url = 'https://graphhopper.com/maps'
|
||||
|
||||
wikidata_image_sparql = """
|
||||
select ?item ?itemLabel ?image ?sign ?symbol ?website ?wikipediaName
|
||||
@ -138,27 +137,27 @@ KEY_RANKS = {k: i for i, k in enumerate(KEY_ORDER)}
|
||||
|
||||
|
||||
def request(query, params):
|
||||
"""do search-request"""
|
||||
params['url'] = base_url + search_string.format(query=urlencode({'q': query}))
|
||||
params['route'] = route_re.match(query)
|
||||
params['headers']['User-Agent'] = searx_useragent()
|
||||
if 'Accept-Language' not in params['headers']:
|
||||
params['headers']['Accept-Language'] = 'en'
|
||||
params['url'] = base_url + search_string.format(query=urllib.parse.urlencode({'q': query}))
|
||||
return params
|
||||
|
||||
|
||||
def response(resp):
|
||||
"""get response from search-request"""
|
||||
results = []
|
||||
nominatim_json = loads(resp.text)
|
||||
def response(resp) -> EngineResults:
|
||||
results = EngineResults()
|
||||
|
||||
nominatim_json = resp.json()
|
||||
user_language = resp.search_params['language']
|
||||
|
||||
if resp.search_params['route']:
|
||||
results.append(
|
||||
{
|
||||
'answer': gettext('Get directions'),
|
||||
'url': route_url.format(*resp.search_params['route'].groups()),
|
||||
}
|
||||
l = re.findall(r"from\s+(.*)\s+to\s+(.+)", resp.search_params["query"])
|
||||
if not l:
|
||||
l = re.findall(r"\s*(.*)\s+to\s+(.+)", resp.search_params["query"])
|
||||
if l:
|
||||
point1, point2 = [urllib.parse.quote_plus(p) for p in l[0]]
|
||||
|
||||
results.add(
|
||||
results.types.Answer(
|
||||
answer=gettext('Show route in map ..'),
|
||||
url=f"{route_url}/?point={point1}&point={point2}",
|
||||
)
|
||||
)
|
||||
|
||||
# simplify the code below: make sure extratags is a dictionary
|
||||
|
149
searx/engines/public_domain_image_archive.py
Normal file
149
searx/engines/public_domain_image_archive.py
Normal file
@ -0,0 +1,149 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Public domain image archive"""
|
||||
|
||||
from urllib.parse import urlencode, urlparse, urlunparse, parse_qsl
|
||||
from json import dumps
|
||||
|
||||
from searx.network import get
|
||||
from searx.utils import extr
|
||||
from searx.exceptions import SearxEngineAccessDeniedException, SearxEngineException
|
||||
|
||||
THUMBNAIL_SUFFIX = "?fit=max&h=360&w=360"
|
||||
"""
|
||||
Example thumbnail urls (from requests & html):
|
||||
- https://the-public-domain-review.imgix.net
|
||||
/shop/nov-2023-prints-00043.jpg
|
||||
?fit=max&h=360&w=360
|
||||
- https://the-public-domain-review.imgix.net
|
||||
/collections/the-history-of-four-footed-beasts-and-serpents-1658/
|
||||
8616383182_5740fa7851_o.jpg
|
||||
?fit=max&h=360&w=360
|
||||
|
||||
Example full image urls (from html)
|
||||
- https://the-public-domain-review.imgix.net/shop/
|
||||
nov-2023-prints-00043.jpg
|
||||
?fit=clip&w=970&h=800&auto=format,compress
|
||||
- https://the-public-domain-review.imgix.net/collections/
|
||||
the-history-of-four-footed-beasts-and-serpents-1658/8616383182_5740fa7851_o.jpg
|
||||
?fit=clip&w=310&h=800&auto=format,compress
|
||||
|
||||
The thumbnail url from the request will be cleaned for the full image link
|
||||
The cleaned thumbnail url will have THUMBNAIL_SUFFIX added to them, based on the original thumbnail parameters
|
||||
"""
|
||||
|
||||
# about
|
||||
about = {
|
||||
"website": 'https://pdimagearchive.org',
|
||||
"use_official_api": False,
|
||||
"require_api_key": False,
|
||||
"results": 'JSON',
|
||||
}
|
||||
|
||||
base_url = 'https://oqi2j6v4iz-dsn.algolia.net'
|
||||
pdia_config_url = 'https://pdimagearchive.org/_astro/config.BiNvrvzG.js'
|
||||
categories = ['images']
|
||||
page_size = 20
|
||||
paging = True
|
||||
|
||||
|
||||
__CACHED_API_KEY = None
|
||||
|
||||
|
||||
def _clean_url(url):
|
||||
parsed = urlparse(url)
|
||||
query = [(k, v) for (k, v) in parse_qsl(parsed.query) if k not in ['ixid', 's']]
|
||||
|
||||
return urlunparse((parsed.scheme, parsed.netloc, parsed.path, parsed.params, urlencode(query), parsed.fragment))
|
||||
|
||||
|
||||
def _get_algolia_api_key():
|
||||
global __CACHED_API_KEY # pylint:disable=global-statement
|
||||
|
||||
if __CACHED_API_KEY:
|
||||
return __CACHED_API_KEY
|
||||
|
||||
resp = get(pdia_config_url)
|
||||
if resp.status_code != 200:
|
||||
raise LookupError("Failed to obtain Algolia API key for PDImageArchive")
|
||||
|
||||
api_key = extr(resp.text, 'r="', '"', default=None)
|
||||
|
||||
if api_key is None:
|
||||
raise LookupError("Couldn't obtain Algolia API key for PDImageArchive")
|
||||
|
||||
__CACHED_API_KEY = api_key
|
||||
return api_key
|
||||
|
||||
|
||||
def _clear_cached_api_key():
|
||||
global __CACHED_API_KEY # pylint:disable=global-statement
|
||||
|
||||
__CACHED_API_KEY = None
|
||||
|
||||
|
||||
def request(query, params):
|
||||
api_key = _get_algolia_api_key()
|
||||
|
||||
args = {
|
||||
'x-algolia-api-key': api_key,
|
||||
'x-algolia-application-id': 'OQI2J6V4IZ',
|
||||
}
|
||||
params['url'] = f"{base_url}/1/indexes/*/queries?{urlencode(args)}"
|
||||
params["method"] = "POST"
|
||||
|
||||
request_params = {
|
||||
"page": params["pageno"] - 1,
|
||||
"query": query,
|
||||
"highlightPostTag": "__ais-highlight__",
|
||||
"highlightPreTag": "__ais-highlight__",
|
||||
}
|
||||
data = {
|
||||
"requests": [
|
||||
{"indexName": "prod_all-images", "params": urlencode(request_params)},
|
||||
]
|
||||
}
|
||||
params["data"] = dumps(data)
|
||||
|
||||
# http errors are handled manually to be able to reset the api key
|
||||
params['raise_for_httperror'] = False
|
||||
return params
|
||||
|
||||
|
||||
def response(resp):
|
||||
results = []
|
||||
json_data = resp.json()
|
||||
|
||||
if resp.status_code == 403:
|
||||
_clear_cached_api_key()
|
||||
raise SearxEngineAccessDeniedException()
|
||||
|
||||
if resp.status_code != 200:
|
||||
raise SearxEngineException()
|
||||
|
||||
if 'results' not in json_data:
|
||||
return []
|
||||
|
||||
for result in json_data['results'][0]['hits']:
|
||||
content = []
|
||||
|
||||
if "themes" in result:
|
||||
content.append("Themes: " + result['themes'])
|
||||
|
||||
if "encompassingWork" in result:
|
||||
content.append("Encompassing work: " + result['encompassingWork'])
|
||||
content = "\n".join(content)
|
||||
|
||||
base_image_url = result['thumbnail'].split("?")[0]
|
||||
|
||||
results.append(
|
||||
{
|
||||
'template': 'images.html',
|
||||
'url': _clean_url(f"{about['website']}/images/{result['objectID']}"),
|
||||
'img_src': _clean_url(base_image_url),
|
||||
'thumbnail_src': _clean_url(base_image_url + THUMBNAIL_SUFFIX),
|
||||
'title': f"{result['title'].strip()} by {result['artist']} {result.get('displayYear', '')}",
|
||||
'content': content,
|
||||
}
|
||||
)
|
||||
|
||||
return results
|
@ -19,6 +19,8 @@ from urllib.parse import urlencode
|
||||
from datetime import datetime
|
||||
from flask_babel import gettext
|
||||
|
||||
from searx.result_types import EngineResults
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import logging
|
||||
|
||||
@ -154,8 +156,9 @@ def parse_tineye_match(match_json):
|
||||
}
|
||||
|
||||
|
||||
def response(resp):
|
||||
def response(resp) -> EngineResults:
|
||||
"""Parse HTTP response from TinEye."""
|
||||
results = EngineResults()
|
||||
|
||||
# handle the 422 client side errors, and the possible 400 status code error
|
||||
if resp.status_code in (400, 422):
|
||||
@ -182,14 +185,13 @@ def response(resp):
|
||||
message = ','.join(description)
|
||||
|
||||
# see https://github.com/searxng/searxng/pull/1456#issuecomment-1193105023
|
||||
# results.append({'answer': message})
|
||||
logger.error(message)
|
||||
return []
|
||||
# results.add(results.types.Answer(answer=message))
|
||||
logger.info(message)
|
||||
return results
|
||||
|
||||
# Raise for all other responses
|
||||
resp.raise_for_status()
|
||||
|
||||
results = []
|
||||
json_data = resp.json()
|
||||
|
||||
for match_json in json_data['matches']:
|
||||
|
@ -3,6 +3,10 @@
|
||||
|
||||
"""
|
||||
|
||||
import urllib.parse
|
||||
|
||||
from searx.result_types import EngineResults
|
||||
|
||||
# about
|
||||
about = {
|
||||
"website": 'https://mymemory.translated.net/',
|
||||
@ -15,8 +19,8 @@ about = {
|
||||
|
||||
engine_type = 'online_dictionary'
|
||||
categories = ['general', 'translate']
|
||||
url = 'https://api.mymemory.translated.net/get?q={query}&langpair={from_lang}|{to_lang}{key}'
|
||||
web_url = 'https://mymemory.translated.net/en/{from_lang}/{to_lang}/{query}'
|
||||
api_url = "https://api.mymemory.translated.net"
|
||||
web_url = "https://mymemory.translated.net"
|
||||
weight = 100
|
||||
https_support = True
|
||||
|
||||
@ -24,29 +28,32 @@ api_key = ''
|
||||
|
||||
|
||||
def request(query, params): # pylint: disable=unused-argument
|
||||
|
||||
args = {"q": params["query"], "langpair": f"{params['from_lang'][1]}|{params['to_lang'][1]}"}
|
||||
if api_key:
|
||||
key_form = '&key=' + api_key
|
||||
else:
|
||||
key_form = ''
|
||||
params['url'] = url.format(
|
||||
from_lang=params['from_lang'][1], to_lang=params['to_lang'][1], query=params['query'], key=key_form
|
||||
)
|
||||
args["key"] = api_key
|
||||
|
||||
params['url'] = f"{api_url}/get?{urllib.parse.urlencode(args)}"
|
||||
return params
|
||||
|
||||
|
||||
def response(resp):
|
||||
results = []
|
||||
results.append(
|
||||
{
|
||||
'url': web_url.format(
|
||||
from_lang=resp.search_params['from_lang'][2],
|
||||
to_lang=resp.search_params['to_lang'][2],
|
||||
query=resp.search_params['query'],
|
||||
),
|
||||
'title': '[{0}-{1}] {2}'.format(
|
||||
resp.search_params['from_lang'][1], resp.search_params['to_lang'][1], resp.search_params['query']
|
||||
),
|
||||
'content': resp.json()['responseData']['translatedText'],
|
||||
}
|
||||
)
|
||||
def response(resp) -> EngineResults:
|
||||
results = EngineResults()
|
||||
data = resp.json()
|
||||
|
||||
args = {
|
||||
"q": resp.search_params["query"],
|
||||
"lang": resp.search_params.get("searxng_locale", "en"), # ui language
|
||||
"sl": resp.search_params['from_lang'][1],
|
||||
"tl": resp.search_params['to_lang'][1],
|
||||
}
|
||||
|
||||
link = f"{web_url}/search.php?{urllib.parse.urlencode(args)}"
|
||||
text = data['responseData']['translatedText']
|
||||
|
||||
examples = [f"{m['segment']} : {m['translation']}" for m in data['matches'] if m['translation'] != text]
|
||||
|
||||
item = results.types.Translations.Item(text=text, examples=examples)
|
||||
results.add(results.types.Translations(translations=[item], url=link))
|
||||
|
||||
return results
|
||||
|
@ -60,6 +60,9 @@ WIKIDATA_PROPERTIES = {
|
||||
'P2002': 'Twitter',
|
||||
'P2013': 'Facebook',
|
||||
'P2003': 'Instagram',
|
||||
'P4033': 'Mastodon',
|
||||
'P11947': 'Lemmy',
|
||||
'P12622': 'PeerTube',
|
||||
}
|
||||
|
||||
# SERVICE wikibase:mwapi : https://www.mediawiki.org/wiki/Wikidata_Query_Service/User_Manual/MWAPI
|
||||
@ -363,8 +366,8 @@ def get_attributes(language):
|
||||
def add_label(name):
|
||||
attributes.append(WDLabelAttribute(name))
|
||||
|
||||
def add_url(name, url_id=None, **kwargs):
|
||||
attributes.append(WDURLAttribute(name, url_id, kwargs))
|
||||
def add_url(name, url_id=None, url_path_prefix=None, **kwargs):
|
||||
attributes.append(WDURLAttribute(name, url_id, url_path_prefix, kwargs))
|
||||
|
||||
def add_image(name, url_id=None, priority=1):
|
||||
attributes.append(WDImageAttribute(name, url_id, priority))
|
||||
@ -476,6 +479,11 @@ def get_attributes(language):
|
||||
add_url('P2013', url_id='facebook_profile')
|
||||
add_url('P2003', url_id='instagram_profile')
|
||||
|
||||
# Fediverse
|
||||
add_url('P4033', url_path_prefix='/@') # Mastodon user
|
||||
add_url('P11947', url_path_prefix='/c/') # Lemmy community
|
||||
add_url('P12622', url_path_prefix='/c/') # PeerTube channel
|
||||
|
||||
# Map
|
||||
attributes.append(WDGeoAttribute('P625'))
|
||||
|
||||
@ -592,22 +600,50 @@ class WDURLAttribute(WDAttribute):
|
||||
|
||||
HTTP_WIKIMEDIA_IMAGE = 'http://commons.wikimedia.org/wiki/Special:FilePath/'
|
||||
|
||||
__slots__ = 'url_id', 'kwargs'
|
||||
__slots__ = 'url_id', 'url_path_prefix', 'kwargs'
|
||||
|
||||
def __init__(self, name, url_id=None, url_path_prefix=None, kwargs=None):
|
||||
"""
|
||||
:param url_id: ID matching one key in ``external_urls.json`` for
|
||||
converting IDs to full URLs.
|
||||
|
||||
:param url_path_prefix: Path prefix if the values are of format
|
||||
``account@domain``. If provided, value are rewritten to
|
||||
``https://<domain><url_path_prefix><account>``. For example::
|
||||
|
||||
WDURLAttribute('P4033', url_path_prefix='/@')
|
||||
|
||||
Adds Property `P4033 <https://www.wikidata.org/wiki/Property:P4033>`_
|
||||
to the wikidata query. This field might return for example
|
||||
``libreoffice@fosstodon.org`` and the URL built from this is then:
|
||||
|
||||
- account: ``libreoffice``
|
||||
- domain: ``fosstodon.org``
|
||||
- result url: https://fosstodon.org/@libreoffice
|
||||
"""
|
||||
|
||||
def __init__(self, name, url_id=None, kwargs=None):
|
||||
super().__init__(name)
|
||||
self.url_id = url_id
|
||||
self.url_path_prefix = url_path_prefix
|
||||
self.kwargs = kwargs
|
||||
|
||||
def get_str(self, result, language):
|
||||
value = result.get(self.name + 's')
|
||||
if self.url_id and value is not None and value != '':
|
||||
value = value.split(',')[0]
|
||||
if not value:
|
||||
return None
|
||||
|
||||
value = value.split(',')[0]
|
||||
if self.url_id:
|
||||
url_id = self.url_id
|
||||
if value.startswith(WDURLAttribute.HTTP_WIKIMEDIA_IMAGE):
|
||||
value = value[len(WDURLAttribute.HTTP_WIKIMEDIA_IMAGE) :]
|
||||
url_id = 'wikimedia_image'
|
||||
return get_external_url(url_id, value)
|
||||
|
||||
if self.url_path_prefix:
|
||||
[account, domain] = value.split('@')
|
||||
return f"https://{domain}{self.url_path_prefix}{account}"
|
||||
|
||||
return value
|
||||
|
||||
|
||||
|
@ -74,6 +74,7 @@ from urllib.parse import urlencode
|
||||
from lxml import html
|
||||
from searx.utils import extract_text, extract_url, eval_xpath, eval_xpath_list
|
||||
from searx.network import raise_for_httperror
|
||||
from searx.result_types import EngineResults
|
||||
|
||||
search_url = None
|
||||
"""
|
||||
@ -261,15 +262,15 @@ def request(query, params):
|
||||
return params
|
||||
|
||||
|
||||
def response(resp): # pylint: disable=too-many-branches
|
||||
'''Scrap *results* from the response (see :ref:`engine results`).'''
|
||||
def response(resp) -> EngineResults: # pylint: disable=too-many-branches
|
||||
"""Scrap *results* from the response (see :ref:`result types`)."""
|
||||
results = EngineResults()
|
||||
|
||||
if no_result_for_http_status and resp.status_code in no_result_for_http_status:
|
||||
return []
|
||||
return results
|
||||
|
||||
raise_for_httperror(resp)
|
||||
|
||||
results = []
|
||||
|
||||
if not resp.text:
|
||||
return results
|
||||
|
||||
|
82
searx/extended_types.py
Normal file
82
searx/extended_types.py
Normal file
@ -0,0 +1,82 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""This module implements the type extensions applied by SearXNG.
|
||||
|
||||
- :py:obj:`flask.request` is replaced by :py:obj:`sxng_request`
|
||||
- :py:obj:`flask.Request` is replaced by :py:obj:`SXNG_Request`
|
||||
- :py:obj:`httpx.response` is replaced by :py:obj:`SXNG_Response`
|
||||
|
||||
----
|
||||
|
||||
.. py:attribute:: sxng_request
|
||||
:type: SXNG_Request
|
||||
|
||||
A replacement for :py:obj:`flask.request` with type cast :py:obj:`SXNG_Request`.
|
||||
|
||||
.. autoclass:: SXNG_Request
|
||||
:members:
|
||||
|
||||
.. autoclass:: SXNG_Response
|
||||
:members:
|
||||
|
||||
"""
|
||||
# pylint: disable=invalid-name
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ["SXNG_Request", "sxng_request", "SXNG_Response"]
|
||||
|
||||
import typing
|
||||
import flask
|
||||
import httpx
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
import searx.preferences
|
||||
import searx.results
|
||||
|
||||
|
||||
class SXNG_Request(flask.Request):
|
||||
"""SearXNG extends the class :py:obj:`flask.Request` with properties from
|
||||
*this* class definition, see type cast :py:obj:`sxng_request`.
|
||||
"""
|
||||
|
||||
user_plugins: list[str]
|
||||
"""list of searx.plugins.Plugin.id (the id of the plugins)"""
|
||||
|
||||
preferences: "searx.preferences.Preferences"
|
||||
"""The prefernces of the request."""
|
||||
|
||||
errors: list[str]
|
||||
"""A list of errors (translated text) added by :py:obj:`searx.webapp` in
|
||||
case of errors."""
|
||||
# request.form is of type werkzeug.datastructures.ImmutableMultiDict
|
||||
# form: dict[str, str]
|
||||
|
||||
start_time: float
|
||||
"""Start time of the request, :py:obj:`timeit.default_timer` added by
|
||||
:py:obj:`searx.webapp` to calculate the total time of the request."""
|
||||
|
||||
render_time: float
|
||||
"""Duration of the rendering, calculated and added by
|
||||
:py:obj:`searx.webapp`."""
|
||||
|
||||
timings: list["searx.results.Timing"]
|
||||
"""A list of :py:obj:`searx.results.Timing` of the engines, calculatid in
|
||||
and hold by :py:obj:`searx.results.ResultContainer.timings`."""
|
||||
|
||||
|
||||
#: A replacement for :py:obj:`flask.request` with type cast :py:`SXNG_Request`.
|
||||
sxng_request = typing.cast(SXNG_Request, flask.request)
|
||||
|
||||
|
||||
class SXNG_Response(httpx.Response):
|
||||
"""SearXNG extends the class :py:obj:`httpx.Response` with properties from
|
||||
*this* class (type cast of :py:obj:`httpx.Response`).
|
||||
|
||||
.. code:: python
|
||||
|
||||
response = httpx.get("https://example.org")
|
||||
response = typing.cast(SXNG_Response, response)
|
||||
if response.ok:
|
||||
...
|
||||
"""
|
||||
|
||||
ok: bool
|
@ -18,6 +18,7 @@ from searx import get_setting
|
||||
|
||||
from searx.webutils import new_hmac, is_hmac_of
|
||||
from searx.exceptions import SearxEngineResponseException
|
||||
from searx.extended_types import sxng_request
|
||||
|
||||
from .resolvers import DEFAULT_RESOLVER_MAP
|
||||
from . import cache
|
||||
@ -124,7 +125,7 @@ def favicon_proxy():
|
||||
server>` setting.
|
||||
|
||||
"""
|
||||
authority = flask.request.args.get('authority')
|
||||
authority = sxng_request.args.get('authority')
|
||||
|
||||
# malformed request or RFC 3986 authority
|
||||
if not authority or "/" in authority:
|
||||
@ -134,11 +135,11 @@ def favicon_proxy():
|
||||
if not is_hmac_of(
|
||||
CFG.secret_key,
|
||||
authority.encode(),
|
||||
flask.request.args.get('h', ''),
|
||||
sxng_request.args.get('h', ''),
|
||||
):
|
||||
return '', 400
|
||||
|
||||
resolver = flask.request.preferences.get_value('favicon_resolver') # type: ignore
|
||||
resolver = sxng_request.preferences.get_value('favicon_resolver') # type: ignore
|
||||
# if resolver is empty or not valid, just return HTTP 400.
|
||||
if not resolver or resolver not in CFG.resolver_map.keys():
|
||||
return "", 400
|
||||
@ -151,7 +152,7 @@ def favicon_proxy():
|
||||
return resp
|
||||
|
||||
# return default favicon from static path
|
||||
theme = flask.request.preferences.get_value("theme") # type: ignore
|
||||
theme = sxng_request.preferences.get_value("theme") # type: ignore
|
||||
fav, mimetype = CFG.favicon(theme=theme)
|
||||
return flask.send_from_directory(fav.parent, fav.name, mimetype=mimetype)
|
||||
|
||||
@ -215,7 +216,7 @@ def favicon_url(authority: str) -> str:
|
||||
|
||||
"""
|
||||
|
||||
resolver = flask.request.preferences.get_value('favicon_resolver') # type: ignore
|
||||
resolver = sxng_request.preferences.get_value('favicon_resolver') # type: ignore
|
||||
# if resolver is empty or not valid, just return nothing.
|
||||
if not resolver or resolver not in CFG.resolver_map.keys():
|
||||
return ""
|
||||
@ -224,7 +225,7 @@ def favicon_url(authority: str) -> str:
|
||||
|
||||
if data_mime == (None, None):
|
||||
# we have already checked, the resolver does not have a favicon
|
||||
theme = flask.request.preferences.get_value("theme") # type: ignore
|
||||
theme = sxng_request.preferences.get_value("theme") # type: ignore
|
||||
return CFG.favicon_data_url(theme=theme)
|
||||
|
||||
if data_mime is not None:
|
||||
|
@ -6,13 +6,14 @@ Usage in a Flask app route:
|
||||
.. code:: python
|
||||
|
||||
from searx import infopage
|
||||
from searx.extended_types import sxng_request
|
||||
|
||||
_INFO_PAGES = infopage.InfoPageSet(infopage.MistletoePage)
|
||||
|
||||
@app.route('/info/<pagename>', methods=['GET'])
|
||||
def info(pagename):
|
||||
|
||||
locale = request.preferences.get_value('locale')
|
||||
locale = sxng_request.preferences.get_value('locale')
|
||||
page = _INFO_PAGES.get_page(pagename, locale)
|
||||
|
||||
"""
|
||||
|
@ -105,6 +105,7 @@ from searx import (
|
||||
redisdb,
|
||||
)
|
||||
from searx import botdetection
|
||||
from searx.extended_types import SXNG_Request, sxng_request
|
||||
from searx.botdetection import (
|
||||
config,
|
||||
http_accept,
|
||||
@ -144,7 +145,7 @@ def get_cfg() -> config.Config:
|
||||
return CFG
|
||||
|
||||
|
||||
def filter_request(request: flask.Request) -> werkzeug.Response | None:
|
||||
def filter_request(request: SXNG_Request) -> werkzeug.Response | None:
|
||||
# pylint: disable=too-many-return-statements
|
||||
|
||||
cfg = get_cfg()
|
||||
@ -201,13 +202,13 @@ def filter_request(request: flask.Request) -> werkzeug.Response | None:
|
||||
val = func.filter_request(network, request, cfg)
|
||||
if val is not None:
|
||||
return val
|
||||
logger.debug(f"OK {network}: %s", dump_request(flask.request))
|
||||
logger.debug(f"OK {network}: %s", dump_request(sxng_request))
|
||||
return None
|
||||
|
||||
|
||||
def pre_request():
|
||||
"""See :py:obj:`flask.Flask.before_request`"""
|
||||
return filter_request(flask.request)
|
||||
return filter_request(sxng_request)
|
||||
|
||||
|
||||
def is_installed():
|
||||
|
@ -35,13 +35,14 @@ from babel.support import Translations
|
||||
import babel.languages
|
||||
import babel.core
|
||||
import flask_babel
|
||||
import flask
|
||||
from flask.ctx import has_request_context
|
||||
|
||||
from searx import (
|
||||
data,
|
||||
logger,
|
||||
searx_dir,
|
||||
)
|
||||
from searx.extended_types import sxng_request
|
||||
|
||||
logger = logger.getChild('locales')
|
||||
|
||||
@ -85,13 +86,13 @@ Kong."""
|
||||
def localeselector():
|
||||
locale = 'en'
|
||||
if has_request_context():
|
||||
value = flask.request.preferences.get_value('locale')
|
||||
value = sxng_request.preferences.get_value('locale')
|
||||
if value:
|
||||
locale = value
|
||||
|
||||
# first, set the language that is not supported by babel
|
||||
if locale in ADDITIONAL_TRANSLATIONS:
|
||||
flask.request.form['use-translation'] = locale
|
||||
sxng_request.form['use-translation'] = locale
|
||||
|
||||
# second, map locale to a value python-babel supports
|
||||
locale = LOCALE_BEST_MATCH.get(locale, locale)
|
||||
@ -109,7 +110,7 @@ def localeselector():
|
||||
def get_translations():
|
||||
"""Monkey patch of :py:obj:`flask_babel.get_translations`"""
|
||||
if has_request_context():
|
||||
use_translation = flask.request.form.get('use-translation')
|
||||
use_translation = sxng_request.form.get('use-translation')
|
||||
if use_translation in ADDITIONAL_TRANSLATIONS:
|
||||
babel_ext = flask_babel.current_app.extensions['babel']
|
||||
return Translations.load(babel_ext.translation_directories[0], use_translation)
|
||||
|
@ -13,6 +13,7 @@ from contextlib import contextmanager
|
||||
import httpx
|
||||
import anyio
|
||||
|
||||
from searx.extended_types import SXNG_Response
|
||||
from .network import get_network, initialize, check_network_configuration # pylint:disable=cyclic-import
|
||||
from .client import get_loop
|
||||
from .raise_for_httperror import raise_for_httperror
|
||||
@ -85,7 +86,7 @@ def _get_timeout(start_time, kwargs):
|
||||
return timeout
|
||||
|
||||
|
||||
def request(method, url, **kwargs):
|
||||
def request(method, url, **kwargs) -> SXNG_Response:
|
||||
"""same as requests/requests/api.py request(...)"""
|
||||
with _record_http_time() as start_time:
|
||||
network = get_context_network()
|
||||
@ -159,34 +160,34 @@ class Request(NamedTuple):
|
||||
return Request('DELETE', url, kwargs)
|
||||
|
||||
|
||||
def get(url, **kwargs):
|
||||
def get(url, **kwargs) -> SXNG_Response:
|
||||
kwargs.setdefault('allow_redirects', True)
|
||||
return request('get', url, **kwargs)
|
||||
|
||||
|
||||
def options(url, **kwargs):
|
||||
def options(url, **kwargs) -> SXNG_Response:
|
||||
kwargs.setdefault('allow_redirects', True)
|
||||
return request('options', url, **kwargs)
|
||||
|
||||
|
||||
def head(url, **kwargs):
|
||||
def head(url, **kwargs) -> SXNG_Response:
|
||||
kwargs.setdefault('allow_redirects', False)
|
||||
return request('head', url, **kwargs)
|
||||
|
||||
|
||||
def post(url, data=None, **kwargs):
|
||||
def post(url, data=None, **kwargs) -> SXNG_Response:
|
||||
return request('post', url, data=data, **kwargs)
|
||||
|
||||
|
||||
def put(url, data=None, **kwargs):
|
||||
def put(url, data=None, **kwargs) -> SXNG_Response:
|
||||
return request('put', url, data=data, **kwargs)
|
||||
|
||||
|
||||
def patch(url, data=None, **kwargs):
|
||||
def patch(url, data=None, **kwargs) -> SXNG_Response:
|
||||
return request('patch', url, data=data, **kwargs)
|
||||
|
||||
|
||||
def delete(url, **kwargs):
|
||||
def delete(url, **kwargs) -> SXNG_Response:
|
||||
return request('delete', url, **kwargs)
|
||||
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# pylint: disable=global-statement
|
||||
# pylint: disable=missing-module-docstring, missing-class-docstring
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
import atexit
|
||||
import asyncio
|
||||
import ipaddress
|
||||
@ -11,6 +13,7 @@ from typing import Dict
|
||||
import httpx
|
||||
|
||||
from searx import logger, searx_debug
|
||||
from searx.extended_types import SXNG_Response
|
||||
from .client import new_client, get_loop, AsyncHTTPTransportNoHttp
|
||||
from .raise_for_httperror import raise_for_httperror
|
||||
|
||||
@ -233,8 +236,9 @@ class Network:
|
||||
del kwargs['raise_for_httperror']
|
||||
return do_raise_for_httperror
|
||||
|
||||
def patch_response(self, response, do_raise_for_httperror):
|
||||
def patch_response(self, response, do_raise_for_httperror) -> SXNG_Response:
|
||||
if isinstance(response, httpx.Response):
|
||||
response = typing.cast(SXNG_Response, response)
|
||||
# requests compatibility (response is not streamed)
|
||||
# see also https://www.python-httpx.org/compatibility/#checking-for-4xx5xx-responses
|
||||
response.ok = not response.is_error
|
||||
@ -258,7 +262,7 @@ class Network:
|
||||
return False
|
||||
return True
|
||||
|
||||
async def call_client(self, stream, method, url, **kwargs):
|
||||
async def call_client(self, stream, method, url, **kwargs) -> SXNG_Response:
|
||||
retries = self.retries
|
||||
was_disconnected = False
|
||||
do_raise_for_httperror = Network.extract_do_raise_for_httperror(kwargs)
|
||||
|
@ -1,232 +1,68 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# pylint: disable=missing-module-docstring, missing-class-docstring
|
||||
""".. sidebar:: Further reading ..
|
||||
|
||||
import sys
|
||||
from hashlib import sha256
|
||||
from importlib import import_module
|
||||
from os import listdir, makedirs, remove, stat, utime
|
||||
from os.path import abspath, basename, dirname, exists, join
|
||||
from shutil import copyfile
|
||||
from pkgutil import iter_modules
|
||||
from logging import getLogger
|
||||
from typing import List, Tuple
|
||||
- :ref:`plugins admin`
|
||||
- :ref:`SearXNG settings <settings plugins>`
|
||||
- :ref:`builtin plugins`
|
||||
|
||||
from searx import logger, settings
|
||||
Plugins can extend or replace functionality of various components of SearXNG.
|
||||
Here is an example of a very simple plugin that adds a "Hello" into the answer
|
||||
area:
|
||||
|
||||
.. code:: python
|
||||
|
||||
class Plugin: # pylint: disable=too-few-public-methods
|
||||
"""This class is currently never initialized and only used for type hinting."""
|
||||
from flask_babel import gettext as _
|
||||
from searx.plugins import Plugin
|
||||
from searx.result_types import Answer
|
||||
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
default_on: bool
|
||||
js_dependencies: Tuple[str]
|
||||
css_dependencies: Tuple[str]
|
||||
preference_section: str
|
||||
class MyPlugin(Plugin):
|
||||
|
||||
id = "self_info"
|
||||
default_on = True
|
||||
|
||||
logger = logger.getChild("plugins")
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
info = PluginInfo(id=self.id, name=_("Hello"), description=_("demo plugin"))
|
||||
|
||||
required_attrs = (
|
||||
# fmt: off
|
||||
("name", str),
|
||||
("description", str),
|
||||
("default_on", bool)
|
||||
# fmt: on
|
||||
)
|
||||
def post_search(self, request, search):
|
||||
return [ Answer(answer="Hello") ]
|
||||
|
||||
optional_attrs = (
|
||||
# fmt: off
|
||||
("js_dependencies", tuple),
|
||||
("css_dependencies", tuple),
|
||||
("preference_section", str),
|
||||
# fmt: on
|
||||
)
|
||||
Entry points (hooks) define when a plugin runs. Right now only three hooks are
|
||||
implemented. So feel free to implement a hook if it fits the behaviour of your
|
||||
plugin / a plugin doesn't need to implement all the hooks.
|
||||
|
||||
- pre search: :py:obj:`Plugin.pre_search`
|
||||
- post search: :py:obj:`Plugin.post_search`
|
||||
- on each result item: :py:obj:`Plugin.on_result`
|
||||
|
||||
def sha_sum(filename):
|
||||
with open(filename, "rb") as f:
|
||||
file_content_bytes = f.read()
|
||||
return sha256(file_content_bytes).hexdigest()
|
||||
For a coding example have a look at :ref:`self_info plugin`.
|
||||
|
||||
----
|
||||
|
||||
def sync_resource(base_path, resource_path, name, target_dir, plugin_dir):
|
||||
dep_path = join(base_path, resource_path)
|
||||
file_name = basename(dep_path)
|
||||
resource_path = join(target_dir, file_name)
|
||||
if not exists(resource_path) or sha_sum(dep_path) != sha_sum(resource_path):
|
||||
try:
|
||||
copyfile(dep_path, resource_path)
|
||||
# copy atime_ns and mtime_ns, so the weak ETags (generated by
|
||||
# the HTTP server) do not change
|
||||
dep_stat = stat(dep_path)
|
||||
utime(resource_path, ns=(dep_stat.st_atime_ns, dep_stat.st_mtime_ns))
|
||||
except IOError:
|
||||
logger.critical("failed to copy plugin resource {0} for plugin {1}".format(file_name, name))
|
||||
sys.exit(3)
|
||||
.. autoclass:: Plugin
|
||||
:members:
|
||||
|
||||
# returning with the web path of the resource
|
||||
return join("plugins/external_plugins", plugin_dir, file_name)
|
||||
.. autoclass:: PluginInfo
|
||||
:members:
|
||||
|
||||
.. autoclass:: PluginStorage
|
||||
:members:
|
||||
|
||||
def prepare_package_resources(plugin, plugin_module_name):
|
||||
plugin_base_path = dirname(abspath(plugin.__file__))
|
||||
.. autoclass:: searx.plugins._core.ModulePlugin
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
plugin_dir = plugin_module_name
|
||||
target_dir = join(settings["ui"]["static_path"], "plugins/external_plugins", plugin_dir)
|
||||
try:
|
||||
makedirs(target_dir, exist_ok=True)
|
||||
except IOError:
|
||||
logger.critical("failed to create resource directory {0} for plugin {1}".format(target_dir, plugin_module_name))
|
||||
sys.exit(3)
|
||||
"""
|
||||
|
||||
resources = []
|
||||
from __future__ import annotations
|
||||
|
||||
if hasattr(plugin, "js_dependencies"):
|
||||
resources.extend(map(basename, plugin.js_dependencies))
|
||||
plugin.js_dependencies = [
|
||||
sync_resource(plugin_base_path, x, plugin_module_name, target_dir, plugin_dir)
|
||||
for x in plugin.js_dependencies
|
||||
]
|
||||
__all__ = ["PluginInfo", "Plugin", "PluginStorage"]
|
||||
|
||||
if hasattr(plugin, "css_dependencies"):
|
||||
resources.extend(map(basename, plugin.css_dependencies))
|
||||
plugin.css_dependencies = [
|
||||
sync_resource(plugin_base_path, x, plugin_module_name, target_dir, plugin_dir)
|
||||
for x in plugin.css_dependencies
|
||||
]
|
||||
from ._core import PluginInfo, Plugin, PluginStorage
|
||||
|
||||
for f in listdir(target_dir):
|
||||
if basename(f) not in resources:
|
||||
resource_path = join(target_dir, basename(f))
|
||||
try:
|
||||
remove(resource_path)
|
||||
except IOError:
|
||||
logger.critical(
|
||||
"failed to remove unused resource file {0} for plugin {1}".format(resource_path, plugin_module_name)
|
||||
)
|
||||
sys.exit(3)
|
||||
|
||||
|
||||
def load_plugin(plugin_module_name, external):
|
||||
# pylint: disable=too-many-branches
|
||||
try:
|
||||
plugin = import_module(plugin_module_name)
|
||||
except (
|
||||
SyntaxError,
|
||||
KeyboardInterrupt,
|
||||
SystemExit,
|
||||
SystemError,
|
||||
ImportError,
|
||||
RuntimeError,
|
||||
) as e:
|
||||
logger.critical("%s: fatal exception", plugin_module_name, exc_info=e)
|
||||
sys.exit(3)
|
||||
except BaseException:
|
||||
logger.exception("%s: exception while loading, the plugin is disabled", plugin_module_name)
|
||||
return None
|
||||
|
||||
# difference with searx: use module name instead of the user name
|
||||
plugin.id = plugin_module_name
|
||||
|
||||
#
|
||||
plugin.logger = getLogger(plugin_module_name)
|
||||
|
||||
for plugin_attr, plugin_attr_type in required_attrs:
|
||||
if not hasattr(plugin, plugin_attr):
|
||||
logger.critical('%s: missing attribute "%s", cannot load plugin', plugin, plugin_attr)
|
||||
sys.exit(3)
|
||||
attr = getattr(plugin, plugin_attr)
|
||||
if not isinstance(attr, plugin_attr_type):
|
||||
type_attr = str(type(attr))
|
||||
logger.critical(
|
||||
'{1}: attribute "{0}" is of type {2}, must be of type {3}, cannot load plugin'.format(
|
||||
plugin, plugin_attr, type_attr, plugin_attr_type
|
||||
)
|
||||
)
|
||||
sys.exit(3)
|
||||
|
||||
for plugin_attr, plugin_attr_type in optional_attrs:
|
||||
if not hasattr(plugin, plugin_attr) or not isinstance(getattr(plugin, plugin_attr), plugin_attr_type):
|
||||
setattr(plugin, plugin_attr, plugin_attr_type())
|
||||
|
||||
if not hasattr(plugin, "preference_section"):
|
||||
plugin.preference_section = "general"
|
||||
|
||||
# query plugin
|
||||
if plugin.preference_section == "query":
|
||||
for plugin_attr in ("query_keywords", "query_examples"):
|
||||
if not hasattr(plugin, plugin_attr):
|
||||
logger.critical('missing attribute "{0}", cannot load plugin: {1}'.format(plugin_attr, plugin))
|
||||
sys.exit(3)
|
||||
|
||||
if settings.get("enabled_plugins"):
|
||||
# searx compatibility: plugin.name in settings['enabled_plugins']
|
||||
plugin.default_on = plugin.name in settings["enabled_plugins"] or plugin.id in settings["enabled_plugins"]
|
||||
|
||||
# copy resources if this is an external plugin
|
||||
if external:
|
||||
prepare_package_resources(plugin, plugin_module_name)
|
||||
|
||||
logger.debug("%s: loaded", plugin_module_name)
|
||||
|
||||
return plugin
|
||||
|
||||
|
||||
def load_and_initialize_plugin(plugin_module_name, external, init_args):
|
||||
plugin = load_plugin(plugin_module_name, external)
|
||||
if plugin and hasattr(plugin, 'init'):
|
||||
try:
|
||||
return plugin if plugin.init(*init_args) else None
|
||||
except Exception: # pylint: disable=broad-except
|
||||
plugin.logger.exception("Exception while calling init, the plugin is disabled")
|
||||
return None
|
||||
return plugin
|
||||
|
||||
|
||||
class PluginStore:
|
||||
def __init__(self):
|
||||
self.plugins: List[Plugin] = []
|
||||
|
||||
def __iter__(self):
|
||||
yield from self.plugins
|
||||
|
||||
def register(self, plugin):
|
||||
self.plugins.append(plugin)
|
||||
|
||||
def call(self, ordered_plugin_list, plugin_type, *args, **kwargs):
|
||||
ret = True
|
||||
for plugin in ordered_plugin_list:
|
||||
if hasattr(plugin, plugin_type):
|
||||
try:
|
||||
ret = getattr(plugin, plugin_type)(*args, **kwargs)
|
||||
if not ret:
|
||||
break
|
||||
except Exception: # pylint: disable=broad-except
|
||||
plugin.logger.exception("Exception while calling %s", plugin_type)
|
||||
return ret
|
||||
|
||||
|
||||
plugins = PluginStore()
|
||||
|
||||
|
||||
def plugin_module_names():
|
||||
yield_plugins = set()
|
||||
|
||||
# embedded plugins
|
||||
for module in iter_modules(path=[dirname(__file__)]):
|
||||
yield (__name__ + "." + module.name, False)
|
||||
yield_plugins.add(module.name)
|
||||
# external plugins
|
||||
for module_name in settings['plugins']:
|
||||
if module_name not in yield_plugins:
|
||||
yield (module_name, True)
|
||||
yield_plugins.add(module_name)
|
||||
STORAGE: PluginStorage = PluginStorage()
|
||||
|
||||
|
||||
def initialize(app):
|
||||
for module_name, external in plugin_module_names():
|
||||
plugin = load_and_initialize_plugin(module_name, external, (app, settings))
|
||||
if plugin:
|
||||
plugins.register(plugin)
|
||||
STORAGE.load_builtins()
|
||||
STORAGE.init(app)
|
||||
|
394
searx/plugins/_core.py
Normal file
394
searx/plugins/_core.py
Normal file
@ -0,0 +1,394 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# pylint: disable=too-few-public-methods,missing-module-docstring
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ["PluginInfo", "Plugin", "PluginStorage"]
|
||||
|
||||
import abc
|
||||
import importlib
|
||||
import logging
|
||||
import pathlib
|
||||
import types
|
||||
import typing
|
||||
import warnings
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
import flask
|
||||
|
||||
import searx
|
||||
from searx.utils import load_module
|
||||
from searx.extended_types import SXNG_Request
|
||||
from searx.result_types import Result
|
||||
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from searx.search import SearchWithPlugins
|
||||
|
||||
|
||||
_default = pathlib.Path(__file__).parent
|
||||
log: logging.Logger = logging.getLogger("searx.plugins")
|
||||
|
||||
|
||||
@dataclass
|
||||
class PluginInfo:
|
||||
"""Object that holds informations about a *plugin*, these infos are shown to
|
||||
the user in the Preferences menu.
|
||||
|
||||
To be able to translate the information into other languages, the text must
|
||||
be written in English and translated with :py:obj:`flask_babel.gettext`.
|
||||
"""
|
||||
|
||||
id: str
|
||||
"""The ID-selector in HTML/CSS `#<id>`."""
|
||||
|
||||
name: str
|
||||
"""Name of the *plugin*."""
|
||||
|
||||
description: str
|
||||
"""Short description of the *answerer*."""
|
||||
|
||||
preference_section: typing.Literal["general", "ui", "privacy", "query"] | None = "general"
|
||||
"""Section (tab/group) in the preferences where this plugin is shown to the
|
||||
user.
|
||||
|
||||
The value ``query`` is reserved for plugins that are activated via a
|
||||
*keyword* as part of a search query, see:
|
||||
|
||||
- :py:obj:`PluginInfo.examples`
|
||||
- :py:obj:`Plugin.keywords`
|
||||
|
||||
Those plugins are shown in the preferences in tab *Special Queries*.
|
||||
"""
|
||||
|
||||
examples: list[str] = field(default_factory=list)
|
||||
"""List of short examples of the usage / of query terms."""
|
||||
|
||||
keywords: list[str] = field(default_factory=list)
|
||||
"""See :py:obj:`Plugin.keywords`"""
|
||||
|
||||
|
||||
class Plugin(abc.ABC):
|
||||
"""Abstract base class of all Plugins."""
|
||||
|
||||
id: typing.ClassVar[str]
|
||||
"""The ID (suffix) in the HTML form."""
|
||||
|
||||
default_on: typing.ClassVar[bool]
|
||||
"""Plugin is enabled/disabled by default."""
|
||||
|
||||
keywords: list[str] = []
|
||||
"""Keywords in the search query that activate the plugin. The *keyword* is
|
||||
the first word in a search query. If a plugin should be executed regardless
|
||||
of the search query, the list of keywords should be empty (which is also the
|
||||
default in the base class for Plugins)."""
|
||||
|
||||
log: logging.Logger
|
||||
"""A logger object, is automatically initialized when calling the
|
||||
constructor (if not already set in the subclass)."""
|
||||
|
||||
info: PluginInfo
|
||||
"""Informations about the *plugin*, see :py:obj:`PluginInfo`."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
for attr in ["id", "default_on"]:
|
||||
if getattr(self, attr, None) is None:
|
||||
raise NotImplementedError(f"plugin {self} is missing attribute {attr}")
|
||||
|
||||
if not self.id:
|
||||
self.id = f"{self.__class__.__module__}.{self.__class__.__name__}"
|
||||
if not getattr(self, "log", None):
|
||||
self.log = log.getChild(self.id)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
"""The hash value is used in :py:obj:`set`, for example, when an object
|
||||
is added to the set. The hash value is also used in other contexts,
|
||||
e.g. when checking for equality to identify identical plugins from
|
||||
different sources (name collisions)."""
|
||||
|
||||
return id(self)
|
||||
|
||||
def __eq__(self, other):
|
||||
"""py:obj:`Plugin` objects are equal if the hash values of the two
|
||||
objects are equal."""
|
||||
|
||||
return hash(self) == hash(other)
|
||||
|
||||
def init(self, app: flask.Flask) -> bool: # pylint: disable=unused-argument
|
||||
"""Initialization of the plugin, the return value decides whether this
|
||||
plugin is active or not. Initialization only takes place once, at the
|
||||
time the WEB application is set up. The base methode always returns
|
||||
``True``, the methode can be overwritten in the inheritances,
|
||||
|
||||
- ``True`` plugin is active
|
||||
- ``False`` plugin is inactive
|
||||
"""
|
||||
return True
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def pre_search(self, request: SXNG_Request, search: "SearchWithPlugins") -> bool:
|
||||
"""Runs BEFORE the search request and returns a boolean:
|
||||
|
||||
- ``True`` to continue the search
|
||||
- ``False`` to stop the search
|
||||
"""
|
||||
return True
|
||||
|
||||
def on_result(self, request: SXNG_Request, search: "SearchWithPlugins", result: Result) -> bool:
|
||||
"""Runs for each result of each engine and returns a boolean:
|
||||
|
||||
- ``True`` to keep the result
|
||||
- ``False`` to remove the result from the result list
|
||||
|
||||
The ``result`` can be modified to the needs.
|
||||
|
||||
.. hint::
|
||||
|
||||
If :py:obj:`Result.url` is modified, :py:obj:`Result.parsed_url` must
|
||||
be changed accordingly:
|
||||
|
||||
.. code:: python
|
||||
|
||||
result["parsed_url"] = urlparse(result["url"])
|
||||
"""
|
||||
return True
|
||||
|
||||
def post_search(self, request: SXNG_Request, search: "SearchWithPlugins") -> None | typing.Sequence[Result]:
|
||||
"""Runs AFTER the search request. Can return a list of :py:obj:`Result`
|
||||
objects to be added to the final result list."""
|
||||
return
|
||||
|
||||
|
||||
class ModulePlugin(Plugin):
|
||||
"""A wrapper class for legacy *plugins*.
|
||||
|
||||
.. note::
|
||||
|
||||
For internal use only!
|
||||
|
||||
In a module plugin, the follwing names are mapped:
|
||||
|
||||
- `module.query_keywords` --> :py:obj:`Plugin.keywords`
|
||||
- `module.plugin_id` --> :py:obj:`Plugin.id`
|
||||
- `module.logger` --> :py:obj:`Plugin.log`
|
||||
"""
|
||||
|
||||
_required_attrs = (("name", str), ("description", str), ("default_on", bool))
|
||||
|
||||
def __init__(self, mod: types.ModuleType):
|
||||
"""In case of missing attributes in the module or wrong types are given,
|
||||
a :py:obj:`TypeError` exception is raised."""
|
||||
|
||||
self.module = mod
|
||||
self.id = getattr(self.module, "plugin_id", self.module.__name__)
|
||||
self.log = logging.getLogger(self.module.__name__)
|
||||
self.keywords = getattr(self.module, "query_keywords", [])
|
||||
|
||||
for attr, attr_type in self._required_attrs:
|
||||
if not hasattr(self.module, attr):
|
||||
msg = f"missing attribute {attr}, cannot load plugin"
|
||||
self.log.critical(msg)
|
||||
raise TypeError(msg)
|
||||
if not isinstance(getattr(self.module, attr), attr_type):
|
||||
msg = f"attribute {attr} is not of type {attr_type}"
|
||||
self.log.critical(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
self.default_on = mod.default_on
|
||||
self.info = PluginInfo(
|
||||
id=self.id,
|
||||
name=self.module.name,
|
||||
description=self.module.description,
|
||||
preference_section=getattr(self.module, "preference_section", None),
|
||||
examples=getattr(self.module, "query_examples", []),
|
||||
keywords=self.keywords,
|
||||
)
|
||||
|
||||
# monkeypatch module
|
||||
self.module.logger = self.log # type: ignore
|
||||
|
||||
super().__init__()
|
||||
|
||||
def init(self, app: flask.Flask) -> bool:
|
||||
if not hasattr(self.module, "init"):
|
||||
return True
|
||||
return self.module.init(app)
|
||||
|
||||
def pre_search(self, request: SXNG_Request, search: "SearchWithPlugins") -> bool:
|
||||
if not hasattr(self.module, "pre_search"):
|
||||
return True
|
||||
return self.module.pre_search(request, search)
|
||||
|
||||
def on_result(self, request: SXNG_Request, search: "SearchWithPlugins", result: Result) -> bool:
|
||||
if not hasattr(self.module, "on_result"):
|
||||
return True
|
||||
return self.module.on_result(request, search, result)
|
||||
|
||||
def post_search(self, request: SXNG_Request, search: "SearchWithPlugins") -> None | list[Result]:
|
||||
if not hasattr(self.module, "post_search"):
|
||||
return None
|
||||
return self.module.post_search(request, search)
|
||||
|
||||
|
||||
class PluginStorage:
|
||||
"""A storage for managing the *plugins* of SearXNG."""
|
||||
|
||||
plugin_list: set[Plugin]
|
||||
"""The list of :py:obj:`Plugins` in this storage."""
|
||||
|
||||
legacy_plugins = [
|
||||
"ahmia_filter",
|
||||
"calculator",
|
||||
"hostnames",
|
||||
"oa_doi_rewrite",
|
||||
"tor_check",
|
||||
"tracker_url_remover",
|
||||
"unit_converter",
|
||||
]
|
||||
"""Internal plugins implemented in the legacy style (as module / deprecated!)."""
|
||||
|
||||
def __init__(self):
|
||||
self.plugin_list = set()
|
||||
|
||||
def __iter__(self):
|
||||
|
||||
yield from self.plugin_list
|
||||
|
||||
def __len__(self):
|
||||
return len(self.plugin_list)
|
||||
|
||||
@property
|
||||
def info(self) -> list[PluginInfo]:
|
||||
return [p.info for p in self.plugin_list]
|
||||
|
||||
def load_builtins(self):
|
||||
"""Load plugin modules from:
|
||||
|
||||
- the python packages in :origin:`searx/plugins` and
|
||||
- the external plugins from :ref:`settings plugins`.
|
||||
"""
|
||||
|
||||
for f in _default.iterdir():
|
||||
|
||||
if f.name.startswith("_"):
|
||||
continue
|
||||
|
||||
if f.stem not in self.legacy_plugins:
|
||||
self.register_by_fqn(f"searx.plugins.{f.stem}.SXNGPlugin")
|
||||
continue
|
||||
|
||||
# for backward compatibility
|
||||
mod = load_module(f.name, str(f.parent))
|
||||
self.register(ModulePlugin(mod))
|
||||
|
||||
for fqn in searx.get_setting("plugins"): # type: ignore
|
||||
self.register_by_fqn(fqn)
|
||||
|
||||
def register(self, plugin: Plugin):
|
||||
"""Register a :py:obj:`Plugin`. In case of name collision (if two
|
||||
plugins have same ID) a :py:obj:`KeyError` exception is raised.
|
||||
"""
|
||||
|
||||
if plugin in self.plugin_list:
|
||||
msg = f"name collision '{plugin.id}'"
|
||||
plugin.log.critical(msg)
|
||||
raise KeyError(msg)
|
||||
|
||||
self.plugin_list.add(plugin)
|
||||
plugin.log.debug("plugin has been loaded")
|
||||
|
||||
def register_by_fqn(self, fqn: str):
|
||||
"""Register a :py:obj:`Plugin` via its fully qualified class name (FQN).
|
||||
The FQNs of external plugins could be read from a configuration, for
|
||||
example, and registered using this method
|
||||
"""
|
||||
|
||||
mod_name, _, obj_name = fqn.rpartition('.')
|
||||
if not mod_name:
|
||||
# for backward compatibility
|
||||
code_obj = importlib.import_module(fqn)
|
||||
else:
|
||||
mod = importlib.import_module(mod_name)
|
||||
code_obj = getattr(mod, obj_name, None)
|
||||
|
||||
if code_obj is None:
|
||||
msg = f"plugin {fqn} is not implemented"
|
||||
log.critical(msg)
|
||||
raise ValueError(msg)
|
||||
|
||||
if isinstance(code_obj, types.ModuleType):
|
||||
# for backward compatibility
|
||||
warnings.warn(
|
||||
f"plugin {fqn} is implemented in a legacy module / migrate to searx.plugins.Plugin", DeprecationWarning
|
||||
)
|
||||
self.register(ModulePlugin(code_obj))
|
||||
return
|
||||
|
||||
self.register(code_obj())
|
||||
|
||||
def init(self, app: flask.Flask) -> None:
|
||||
"""Calls the method :py:obj:`Plugin.init` of each plugin in this
|
||||
storage. Depending on its return value, the plugin is removed from
|
||||
*this* storage or not."""
|
||||
|
||||
for plg in self.plugin_list.copy():
|
||||
if not plg.init(app):
|
||||
self.plugin_list.remove(plg)
|
||||
|
||||
def pre_search(self, request: SXNG_Request, search: "SearchWithPlugins") -> bool:
|
||||
|
||||
ret = True
|
||||
for plugin in [p for p in self.plugin_list if p.id in search.user_plugins]:
|
||||
try:
|
||||
ret = bool(plugin.pre_search(request=request, search=search))
|
||||
except Exception: # pylint: disable=broad-except
|
||||
plugin.log.exception("Exception while calling pre_search")
|
||||
continue
|
||||
if not ret:
|
||||
# skip this search on the first False from a plugin
|
||||
break
|
||||
return ret
|
||||
|
||||
def on_result(self, request: SXNG_Request, search: "SearchWithPlugins", result: Result) -> bool:
|
||||
|
||||
ret = True
|
||||
for plugin in [p for p in self.plugin_list if p.id in search.user_plugins]:
|
||||
try:
|
||||
ret = bool(plugin.on_result(request=request, search=search, result=result))
|
||||
except Exception: # pylint: disable=broad-except
|
||||
plugin.log.exception("Exception while calling on_result")
|
||||
continue
|
||||
if not ret:
|
||||
# ignore this result item on the first False from a plugin
|
||||
break
|
||||
|
||||
return ret
|
||||
|
||||
def post_search(self, request: SXNG_Request, search: "SearchWithPlugins") -> None:
|
||||
"""Extend :py:obj:`search.result_container
|
||||
<searx.results.ResultContainer`> with result items from plugins listed
|
||||
in :py:obj:`search.user_plugins <SearchWithPlugins.user_plugins>`.
|
||||
"""
|
||||
|
||||
keyword = None
|
||||
for keyword in search.search_query.query.split():
|
||||
if keyword:
|
||||
break
|
||||
|
||||
for plugin in [p for p in self.plugin_list if p.id in search.user_plugins]:
|
||||
|
||||
if plugin.keywords:
|
||||
# plugin with keywords: skip plugin if no keyword match
|
||||
if keyword and keyword not in plugin.keywords:
|
||||
continue
|
||||
try:
|
||||
results = plugin.post_search(request=request, search=search) or []
|
||||
except Exception: # pylint: disable=broad-except
|
||||
plugin.log.exception("Exception while calling post_search")
|
||||
continue
|
||||
|
||||
# In case of *plugins* prefix ``plugin:`` is set, see searx.result_types.Result
|
||||
search.result_container.extend(f"plugin: {plugin.id}", results)
|
@ -1,27 +1,33 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# pylint: disable=missing-module-docstring
|
||||
|
||||
from __future__ import annotations
|
||||
from hashlib import md5
|
||||
|
||||
import flask
|
||||
|
||||
from searx.data import ahmia_blacklist_loader
|
||||
from searx import get_setting
|
||||
|
||||
|
||||
name = "Ahmia blacklist"
|
||||
description = "Filter out onion results that appear in Ahmia's blacklist. (See https://ahmia.fi/blacklist)"
|
||||
default_on = True
|
||||
preference_section = 'onions'
|
||||
|
||||
ahmia_blacklist = None
|
||||
ahmia_blacklist: list = []
|
||||
|
||||
|
||||
def on_result(_request, _search, result):
|
||||
if not result.get('is_onion') or not result.get('parsed_url'):
|
||||
def on_result(_request, _search, result) -> bool:
|
||||
if not getattr(result, 'is_onion', None) or not getattr(result, 'parsed_url', None):
|
||||
return True
|
||||
result_hash = md5(result['parsed_url'].hostname.encode()).hexdigest()
|
||||
return result_hash not in ahmia_blacklist
|
||||
|
||||
|
||||
def init(_app, settings):
|
||||
def init(app=flask.Flask) -> bool: # pylint: disable=unused-argument
|
||||
global ahmia_blacklist # pylint: disable=global-statement
|
||||
if not settings['outgoing']['using_tor_proxy']:
|
||||
if not get_setting("outgoing.using_tor_proxy"):
|
||||
# disable the plugin
|
||||
return False
|
||||
ahmia_blacklist = ahmia_blacklist_loader()
|
||||
|
@ -1,28 +1,27 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Calculate mathematical expressions using ack#eval
|
||||
"""Calculate mathematical expressions using :py:obj`ast.parse` (mode="eval").
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import Callable
|
||||
|
||||
import ast
|
||||
import re
|
||||
import operator
|
||||
from multiprocessing import Process, Queue
|
||||
from typing import Callable
|
||||
import multiprocessing
|
||||
|
||||
import flask
|
||||
import babel
|
||||
import babel.numbers
|
||||
from flask_babel import gettext
|
||||
|
||||
from searx.plugins import logger
|
||||
from searx.result_types import EngineResults
|
||||
|
||||
name = "Basic Calculator"
|
||||
description = gettext("Calculate mathematical expressions via the search bar")
|
||||
default_on = True
|
||||
|
||||
preference_section = 'general'
|
||||
plugin_id = 'calculator'
|
||||
|
||||
logger = logger.getChild(plugin_id)
|
||||
|
||||
operators: dict[type, Callable] = {
|
||||
ast.Add: operator.add,
|
||||
ast.Sub: operator.sub,
|
||||
@ -33,11 +32,17 @@ operators: dict[type, Callable] = {
|
||||
ast.USub: operator.neg,
|
||||
}
|
||||
|
||||
# with multiprocessing.get_context("fork") we are ready for Py3.14 (by emulating
|
||||
# the old behavior "fork") but it will not solve the core problem of fork, nor
|
||||
# will it remove the deprecation warnings in py3.12 & py3.13. Issue is
|
||||
# ddiscussed here: https://github.com/searxng/searxng/issues/4159
|
||||
mp_fork = multiprocessing.get_context("fork")
|
||||
|
||||
|
||||
def _eval_expr(expr):
|
||||
"""
|
||||
>>> _eval_expr('2^6')
|
||||
4
|
||||
64
|
||||
>>> _eval_expr('2**6')
|
||||
64
|
||||
>>> _eval_expr('1 + 2*3**(4^5) / (6 + -7)')
|
||||
@ -63,46 +68,49 @@ def _eval(node):
|
||||
raise TypeError(node)
|
||||
|
||||
|
||||
def handler(q: multiprocessing.Queue, func, args, **kwargs): # pylint:disable=invalid-name
|
||||
try:
|
||||
q.put(func(*args, **kwargs))
|
||||
except:
|
||||
q.put(None)
|
||||
raise
|
||||
|
||||
|
||||
def timeout_func(timeout, func, *args, **kwargs):
|
||||
|
||||
def handler(q: Queue, func, args, **kwargs): # pylint:disable=invalid-name
|
||||
try:
|
||||
q.put(func(*args, **kwargs))
|
||||
except:
|
||||
q.put(None)
|
||||
raise
|
||||
|
||||
que = Queue()
|
||||
p = Process(target=handler, args=(que, func, args), kwargs=kwargs)
|
||||
que = mp_fork.Queue()
|
||||
p = mp_fork.Process(target=handler, args=(que, func, args), kwargs=kwargs)
|
||||
p.start()
|
||||
p.join(timeout=timeout)
|
||||
ret_val = None
|
||||
# pylint: disable=used-before-assignment,undefined-variable
|
||||
if not p.is_alive():
|
||||
ret_val = que.get()
|
||||
else:
|
||||
logger.debug("terminate function after timeout is exceeded")
|
||||
logger.debug("terminate function after timeout is exceeded") # type: ignore
|
||||
p.terminate()
|
||||
p.join()
|
||||
p.close()
|
||||
return ret_val
|
||||
|
||||
|
||||
def post_search(_request, search):
|
||||
def post_search(request, search) -> EngineResults:
|
||||
results = EngineResults()
|
||||
|
||||
# only show the result of the expression on the first page
|
||||
if search.search_query.pageno > 1:
|
||||
return True
|
||||
return results
|
||||
|
||||
query = search.search_query.query
|
||||
# in order to avoid DoS attacks with long expressions, ignore long expressions
|
||||
if len(query) > 100:
|
||||
return True
|
||||
return results
|
||||
|
||||
# replace commonly used math operators with their proper Python operator
|
||||
query = query.replace("x", "*").replace(":", "/")
|
||||
|
||||
# use UI language
|
||||
ui_locale = babel.Locale.parse(flask.request.preferences.get_value('locale'), sep='-')
|
||||
ui_locale = babel.Locale.parse(request.preferences.get_value('locale'), sep='-')
|
||||
|
||||
# parse the number system in a localized way
|
||||
def _decimal(match: re.Match) -> str:
|
||||
@ -116,15 +124,17 @@ def post_search(_request, search):
|
||||
|
||||
# only numbers and math operators are accepted
|
||||
if any(str.isalpha(c) for c in query):
|
||||
return True
|
||||
return results
|
||||
|
||||
# in python, powers are calculated via **
|
||||
query_py_formatted = query.replace("^", "**")
|
||||
|
||||
# Prevent the runtime from being longer than 50 ms
|
||||
result = timeout_func(0.05, _eval_expr, query_py_formatted)
|
||||
if result is None or result == "":
|
||||
return True
|
||||
result = babel.numbers.format_decimal(result, locale=ui_locale)
|
||||
search.result_container.answers['calculate'] = {'answer': f"{search.search_query.query} = {result}"}
|
||||
return True
|
||||
res = timeout_func(0.05, _eval_expr, query_py_formatted)
|
||||
if res is None or res == "":
|
||||
return results
|
||||
|
||||
res = babel.numbers.format_decimal(res, locale=ui_locale)
|
||||
results.add(results.types.Answer(answer=f"{search.search_query.query} = {res}"))
|
||||
|
||||
return results
|
||||
|
@ -1,43 +1,66 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# pylint: disable=missing-module-docstring
|
||||
# pylint: disable=missing-module-docstring, missing-class-docstring
|
||||
from __future__ import annotations
|
||||
import typing
|
||||
|
||||
import hashlib
|
||||
import re
|
||||
import hashlib
|
||||
|
||||
from flask_babel import gettext
|
||||
|
||||
name = "Hash plugin"
|
||||
description = gettext("Converts strings to different hash digests.")
|
||||
default_on = True
|
||||
preference_section = 'query'
|
||||
query_keywords = ['md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512']
|
||||
query_examples = 'sha512 The quick brown fox jumps over the lazy dog'
|
||||
from searx.plugins import Plugin, PluginInfo
|
||||
from searx.result_types import EngineResults
|
||||
|
||||
parser_re = re.compile('(md5|sha1|sha224|sha256|sha384|sha512) (.*)', re.I)
|
||||
if typing.TYPE_CHECKING:
|
||||
from searx.search import SearchWithPlugins
|
||||
from searx.extended_types import SXNG_Request
|
||||
|
||||
|
||||
def post_search(_request, search):
|
||||
# process only on first page
|
||||
if search.search_query.pageno > 1:
|
||||
return True
|
||||
m = parser_re.match(search.search_query.query)
|
||||
if not m:
|
||||
# wrong query
|
||||
return True
|
||||
class SXNGPlugin(Plugin):
|
||||
"""Plugin converts strings to different hash digests. The results are
|
||||
displayed in area for the "answers".
|
||||
"""
|
||||
|
||||
function, string = m.groups()
|
||||
if not string.strip():
|
||||
# end if the string is empty
|
||||
return True
|
||||
id = "hash_plugin"
|
||||
default_on = True
|
||||
keywords = ["md5", "sha1", "sha224", "sha256", "sha384", "sha512"]
|
||||
|
||||
# select hash function
|
||||
f = hashlib.new(function.lower())
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
# make digest from the given string
|
||||
f.update(string.encode('utf-8').strip())
|
||||
answer = function + " " + gettext('hash digest') + ": " + f.hexdigest()
|
||||
self.parser_re = re.compile(f"({'|'.join(self.keywords)}) (.*)", re.I)
|
||||
self.info = PluginInfo(
|
||||
id=self.id,
|
||||
name=gettext("Hash plugin"),
|
||||
description=gettext("Converts strings to different hash digests."),
|
||||
examples=["sha512 The quick brown fox jumps over the lazy dog"],
|
||||
preference_section="query",
|
||||
)
|
||||
|
||||
# print result
|
||||
search.result_container.answers.clear()
|
||||
search.result_container.answers['hash'] = {'answer': answer}
|
||||
return True
|
||||
def post_search(self, request: "SXNG_Request", search: "SearchWithPlugins") -> EngineResults:
|
||||
"""Returns a result list only for the first page."""
|
||||
results = EngineResults()
|
||||
|
||||
if search.search_query.pageno > 1:
|
||||
return results
|
||||
|
||||
m = self.parser_re.match(search.search_query.query)
|
||||
if not m:
|
||||
# wrong query
|
||||
return results
|
||||
|
||||
function, string = m.groups()
|
||||
if not string.strip():
|
||||
# end if the string is empty
|
||||
return results
|
||||
|
||||
# select hash function
|
||||
f = hashlib.new(function.lower())
|
||||
|
||||
# make digest from the given string
|
||||
f.update(string.encode("utf-8").strip())
|
||||
answer = function + " " + gettext("hash digest") + ": " + f.hexdigest()
|
||||
|
||||
results.add(results.types.Answer(answer=answer))
|
||||
|
||||
return results
|
||||
|
@ -91,15 +91,17 @@ something like this:
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from urllib.parse import urlunparse, urlparse
|
||||
|
||||
from flask_babel import gettext
|
||||
|
||||
from searx import settings
|
||||
from searx.plugins import logger
|
||||
from searx.settings_loader import get_yaml_cfg
|
||||
|
||||
|
||||
name = gettext('Hostnames plugin')
|
||||
description = gettext('Rewrite hostnames, remove results or prioritize them based on the hostname')
|
||||
default_on = False
|
||||
@ -107,16 +109,15 @@ preference_section = 'general'
|
||||
|
||||
plugin_id = 'hostnames'
|
||||
|
||||
logger = logger.getChild(plugin_id)
|
||||
parsed = 'parsed_url'
|
||||
_url_fields = ['iframe_src', 'audio_src']
|
||||
|
||||
|
||||
def _load_regular_expressions(settings_key):
|
||||
def _load_regular_expressions(settings_key) -> dict | set | None:
|
||||
setting_value = settings.get(plugin_id, {}).get(settings_key)
|
||||
|
||||
if not setting_value:
|
||||
return {}
|
||||
return None
|
||||
|
||||
# load external file with configuration
|
||||
if isinstance(setting_value, str):
|
||||
@ -128,20 +129,20 @@ def _load_regular_expressions(settings_key):
|
||||
if isinstance(setting_value, dict):
|
||||
return {re.compile(p): r for (p, r) in setting_value.items()}
|
||||
|
||||
return {}
|
||||
return None
|
||||
|
||||
|
||||
replacements = _load_regular_expressions('replace')
|
||||
removables = _load_regular_expressions('remove')
|
||||
high_priority = _load_regular_expressions('high_priority')
|
||||
low_priority = _load_regular_expressions('low_priority')
|
||||
replacements: dict = _load_regular_expressions('replace') or {} # type: ignore
|
||||
removables: set = _load_regular_expressions('remove') or set() # type: ignore
|
||||
high_priority: set = _load_regular_expressions('high_priority') or set() # type: ignore
|
||||
low_priority: set = _load_regular_expressions('low_priority') or set() # type: ignore
|
||||
|
||||
|
||||
def _matches_parsed_url(result, pattern):
|
||||
return parsed in result and pattern.search(result[parsed].netloc)
|
||||
return result[parsed] and (parsed in result and pattern.search(result[parsed].netloc))
|
||||
|
||||
|
||||
def on_result(_request, _search, result):
|
||||
def on_result(_request, _search, result) -> bool:
|
||||
for pattern, replacement in replacements.items():
|
||||
if _matches_parsed_url(result, pattern):
|
||||
# logger.debug(result['url'])
|
||||
@ -150,7 +151,7 @@ def on_result(_request, _search, result):
|
||||
# logger.debug(result['url'])
|
||||
|
||||
for url_field in _url_fields:
|
||||
if not result.get(url_field):
|
||||
if not getattr(result, url_field, None):
|
||||
continue
|
||||
|
||||
url_src = urlparse(result[url_field])
|
||||
@ -163,7 +164,7 @@ def on_result(_request, _search, result):
|
||||
return False
|
||||
|
||||
for url_field in _url_fields:
|
||||
if not result.get(url_field):
|
||||
if not getattr(result, url_field, None):
|
||||
continue
|
||||
|
||||
url_src = urlparse(result[url_field])
|
||||
|
@ -1,18 +1,21 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# pylint: disable=missing-module-docstring
|
||||
|
||||
from __future__ import annotations
|
||||
import re
|
||||
from urllib.parse import urlparse, parse_qsl
|
||||
|
||||
from flask_babel import gettext
|
||||
|
||||
from searx import settings
|
||||
|
||||
|
||||
regex = re.compile(r'10\.\d{4,9}/[^\s]+')
|
||||
|
||||
name = gettext('Open Access DOI rewrite')
|
||||
description = gettext('Avoid paywalls by redirecting to open-access versions of publications when available')
|
||||
default_on = False
|
||||
preference_section = 'general'
|
||||
preference_section = 'general/doi_resolver'
|
||||
|
||||
|
||||
def extract_doi(url):
|
||||
@ -34,8 +37,9 @@ def get_doi_resolver(preferences):
|
||||
return doi_resolvers[selected_resolver]
|
||||
|
||||
|
||||
def on_result(request, _search, result):
|
||||
if 'parsed_url' not in result:
|
||||
def on_result(request, _search, result) -> bool:
|
||||
|
||||
if not result.parsed_url:
|
||||
return True
|
||||
|
||||
doi = extract_doi(result['parsed_url'])
|
||||
|
@ -1,32 +1,57 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# pylint: disable=missing-module-docstring,invalid-name
|
||||
# pylint: disable=missing-module-docstring, missing-class-docstring
|
||||
from __future__ import annotations
|
||||
import typing
|
||||
|
||||
import re
|
||||
from flask_babel import gettext
|
||||
|
||||
from searx.botdetection._helpers import get_real_ip
|
||||
from searx.result_types import EngineResults
|
||||
|
||||
name = gettext('Self Information')
|
||||
description = gettext('Displays your IP if the query is "ip" and your user agent if the query contains "user agent".')
|
||||
default_on = True
|
||||
preference_section = 'query'
|
||||
query_keywords = ['user-agent']
|
||||
query_examples = ''
|
||||
from . import Plugin, PluginInfo
|
||||
|
||||
# "ip" or "my ip" regex
|
||||
ip_regex = re.compile('^ip$|my ip', re.IGNORECASE)
|
||||
|
||||
# Self User Agent regex
|
||||
ua_regex = re.compile('.*user[ -]agent.*', re.IGNORECASE)
|
||||
if typing.TYPE_CHECKING:
|
||||
from searx.search import SearchWithPlugins
|
||||
from searx.extended_types import SXNG_Request
|
||||
|
||||
|
||||
def post_search(request, search):
|
||||
if search.search_query.pageno > 1:
|
||||
return True
|
||||
if ip_regex.search(search.search_query.query):
|
||||
ip = get_real_ip(request)
|
||||
search.result_container.answers['ip'] = {'answer': gettext('Your IP is: ') + ip}
|
||||
elif ua_regex.match(search.search_query.query):
|
||||
ua = request.user_agent
|
||||
search.result_container.answers['user-agent'] = {'answer': gettext('Your user-agent is: ') + ua.string}
|
||||
return True
|
||||
class SXNGPlugin(Plugin):
|
||||
"""Simple plugin that displays information about user's request, including
|
||||
the IP or HTTP User-Agent. The information is displayed in area for the
|
||||
"answers".
|
||||
"""
|
||||
|
||||
id = "self_info"
|
||||
default_on = True
|
||||
keywords = ["ip", "user-agent"]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.ip_regex = re.compile(r"^ip", re.IGNORECASE)
|
||||
self.ua_regex = re.compile(r"^user-agent", re.IGNORECASE)
|
||||
|
||||
self.info = PluginInfo(
|
||||
id=self.id,
|
||||
name=gettext("Self Information"),
|
||||
description=gettext(
|
||||
"""Displays your IP if the query is "ip" and your user agent if the query is "user-agent"."""
|
||||
),
|
||||
preference_section="query",
|
||||
)
|
||||
|
||||
def post_search(self, request: "SXNG_Request", search: "SearchWithPlugins") -> EngineResults:
|
||||
"""Returns a result list only for the first page."""
|
||||
results = EngineResults()
|
||||
|
||||
if search.search_query.pageno > 1:
|
||||
return results
|
||||
|
||||
if self.ip_regex.search(search.search_query.query):
|
||||
results.add(results.types.Answer(answer=gettext("Your IP is: ") + get_real_ip(request)))
|
||||
|
||||
if self.ua_regex.match(search.search_query.query):
|
||||
results.add(results.types.Answer(answer=gettext("Your user-agent is: ") + str(request.user_agent)))
|
||||
|
||||
return results
|
||||
|
@ -1,8 +1,8 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""A plugin to check if the ip address of the request is a Tor exit-node if the
|
||||
user searches for ``tor-check``. It fetches the tor exit node list from
|
||||
https://check.torproject.org/exit-addresses and parses all the IPs into a list,
|
||||
then checks if the user's IP address is in it.
|
||||
:py:obj:`url_exit_list` and parses all the IPs into a list, then checks if the
|
||||
user's IP address is in it.
|
||||
|
||||
Enable in ``settings.yml``:
|
||||
|
||||
@ -14,10 +14,15 @@ Enable in ``settings.yml``:
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from flask_babel import gettext
|
||||
from httpx import HTTPError
|
||||
|
||||
from searx.network import get
|
||||
from searx.result_types import Answer
|
||||
|
||||
|
||||
default_on = False
|
||||
|
||||
@ -42,27 +47,28 @@ query_examples = ''
|
||||
# Regex for exit node addresses in the list.
|
||||
reg = re.compile(r"(?<=ExitAddress )\S+")
|
||||
|
||||
url_exit_list = "https://check.torproject.org/exit-addresses"
|
||||
"""URL to load Tor exit list from."""
|
||||
|
||||
def post_search(request, search):
|
||||
|
||||
def post_search(request, search) -> list[Answer]:
|
||||
results = []
|
||||
|
||||
if search.search_query.pageno > 1:
|
||||
return True
|
||||
return results
|
||||
|
||||
if search.search_query.query.lower() == "tor-check":
|
||||
|
||||
# Request the list of tor exit nodes.
|
||||
try:
|
||||
resp = get("https://check.torproject.org/exit-addresses")
|
||||
node_list = re.findall(reg, resp.text)
|
||||
resp = get(url_exit_list)
|
||||
node_list = re.findall(reg, resp.text) # type: ignore
|
||||
|
||||
except HTTPError:
|
||||
# No answer, return error
|
||||
search.result_container.answers["tor"] = {
|
||||
"answer": gettext(
|
||||
"Could not download the list of Tor exit-nodes from: https://check.torproject.org/exit-addresses"
|
||||
)
|
||||
}
|
||||
return True
|
||||
msg = gettext("Could not download the list of Tor exit-nodes from")
|
||||
Answer(results=results, answer=f"{msg} {url_exit_list}")
|
||||
return results
|
||||
|
||||
x_forwarded_for = request.headers.getlist("X-Forwarded-For")
|
||||
|
||||
@ -72,20 +78,11 @@ def post_search(request, search):
|
||||
ip_address = request.remote_addr
|
||||
|
||||
if ip_address in node_list:
|
||||
search.result_container.answers["tor"] = {
|
||||
"answer": gettext(
|
||||
"You are using Tor and it looks like you have this external IP address: {ip_address}".format(
|
||||
ip_address=ip_address
|
||||
)
|
||||
)
|
||||
}
|
||||
else:
|
||||
search.result_container.answers["tor"] = {
|
||||
"answer": gettext(
|
||||
"You are not using Tor and you have this external IP address: {ip_address}".format(
|
||||
ip_address=ip_address
|
||||
)
|
||||
)
|
||||
}
|
||||
msg = gettext("You are using Tor and it looks like you have the external IP address")
|
||||
Answer(results=results, answer=f"{msg} {ip_address}")
|
||||
|
||||
return True
|
||||
else:
|
||||
msg = gettext("You are not using Tor and you have the external IP address")
|
||||
Answer(results=results, answer=f"{msg} {ip_address}")
|
||||
|
||||
return results
|
||||
|
@ -1,6 +1,8 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# pylint: disable=missing-module-docstring
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from urllib.parse import urlunparse, parse_qsl, urlencode
|
||||
|
||||
@ -19,24 +21,24 @@ default_on = True
|
||||
preference_section = 'privacy'
|
||||
|
||||
|
||||
def on_result(_request, _search, result):
|
||||
if 'parsed_url' not in result:
|
||||
def on_result(_request, _search, result) -> bool:
|
||||
|
||||
parsed_url = getattr(result, "parsed_url", None)
|
||||
if not parsed_url:
|
||||
return True
|
||||
|
||||
query = result['parsed_url'].query
|
||||
|
||||
if query == "":
|
||||
if parsed_url.query == "":
|
||||
return True
|
||||
parsed_query = parse_qsl(query)
|
||||
|
||||
parsed_query = parse_qsl(parsed_url.query)
|
||||
changes = 0
|
||||
for i, (param_name, _) in enumerate(list(parsed_query)):
|
||||
for reg in regexes:
|
||||
if reg.match(param_name):
|
||||
parsed_query.pop(i - changes)
|
||||
changes += 1
|
||||
result['parsed_url'] = result['parsed_url']._replace(query=urlencode(parsed_query))
|
||||
result['url'] = urlunparse(result['parsed_url'])
|
||||
result.parsed_url = result.parsed_url._replace(query=urlencode(parsed_query))
|
||||
result.url = urlunparse(result.parsed_url)
|
||||
break
|
||||
|
||||
return True
|
||||
|
@ -18,11 +18,14 @@ Enable in ``settings.yml``:
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import re
|
||||
import babel.numbers
|
||||
|
||||
from flask_babel import gettext, get_locale
|
||||
|
||||
from searx import data
|
||||
from searx.result_types import Answer
|
||||
|
||||
|
||||
name = "Unit converter plugin"
|
||||
@ -171,16 +174,16 @@ def symbol_to_si():
|
||||
return SYMBOL_TO_SI
|
||||
|
||||
|
||||
def _parse_text_and_convert(search, from_query, to_query):
|
||||
def _parse_text_and_convert(from_query, to_query) -> str | None:
|
||||
|
||||
# pylint: disable=too-many-branches, too-many-locals
|
||||
|
||||
if not (from_query and to_query):
|
||||
return
|
||||
return None
|
||||
|
||||
measured = re.match(RE_MEASURE, from_query, re.VERBOSE)
|
||||
if not (measured and measured.group('number'), measured.group('unit')):
|
||||
return
|
||||
return None
|
||||
|
||||
# Symbols are not unique, if there are several hits for the from-unit, then
|
||||
# the correct one must be determined by comparing it with the to-unit
|
||||
@ -198,7 +201,7 @@ def _parse_text_and_convert(search, from_query, to_query):
|
||||
target_list.append((si_name, from_si, orig_symbol))
|
||||
|
||||
if not (source_list and target_list):
|
||||
return
|
||||
return None
|
||||
|
||||
source_to_si = target_from_si = target_symbol = None
|
||||
|
||||
@ -212,7 +215,7 @@ def _parse_text_and_convert(search, from_query, to_query):
|
||||
target_symbol = target[2]
|
||||
|
||||
if not (source_to_si and target_from_si):
|
||||
return
|
||||
return None
|
||||
|
||||
_locale = get_locale() or 'en_US'
|
||||
|
||||
@ -239,25 +242,28 @@ def _parse_text_and_convert(search, from_query, to_query):
|
||||
else:
|
||||
result = babel.numbers.format_decimal(value, locale=_locale, format='#,##0.##########;-#')
|
||||
|
||||
search.result_container.answers['conversion'] = {'answer': f'{result} {target_symbol}'}
|
||||
return f'{result} {target_symbol}'
|
||||
|
||||
|
||||
def post_search(_request, search):
|
||||
def post_search(_request, search) -> list[Answer]:
|
||||
results = []
|
||||
|
||||
# only convert between units on the first page
|
||||
if search.search_query.pageno > 1:
|
||||
return True
|
||||
return results
|
||||
|
||||
query = search.search_query.query
|
||||
query_parts = query.split(" ")
|
||||
|
||||
if len(query_parts) < 3:
|
||||
return True
|
||||
return results
|
||||
|
||||
for query_part in query_parts:
|
||||
for keyword in CONVERT_KEYWORDS:
|
||||
if query_part == keyword:
|
||||
from_query, to_query = query.split(keyword, 1)
|
||||
_parse_text_and_convert(search, from_query.strip(), to_query.strip())
|
||||
return True
|
||||
target_val = _parse_text_and_convert(from_query.strip(), to_query.strip())
|
||||
if target_val:
|
||||
Answer(results=results, answer=target_val)
|
||||
|
||||
return True
|
||||
return results
|
||||
|
109
searx/plugins/url_rewrite.py
Normal file
109
searx/plugins/url_rewrite.py
Normal file
@ -0,0 +1,109 @@
|
||||
"""
|
||||
The **URL Rewrite plugin** allows modification of search result URLs based on regular expression patterns.
|
||||
URLs can be rewritten to different destinations, blocked entirely, or have their priority adjusted in the
|
||||
results list.
|
||||
|
||||
The plugin can be enabled by adding it to the `enabled_plugins` **list** in the `settings.yml`:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
enabled_plugins:
|
||||
- 'URL Rewrite'
|
||||
...
|
||||
|
||||
Configuration options in `settings.yml`:
|
||||
|
||||
- ``url_rewrite.rules``: A **list** of rules that define how URLs should be handled. Each rule
|
||||
can contain the following keys:
|
||||
|
||||
- ``pattern``: Regular expression pattern to match against result URLs
|
||||
- ``repl``: Replacement string for matched URLs, or `false` to remove matching results
|
||||
- ``priority``: Optional priority adjustment ('high' or 'low') for matching results
|
||||
|
||||
Example configuration:
|
||||
|
||||
.. code:: yaml
|
||||
|
||||
url_rewrite:
|
||||
rules:
|
||||
- pattern: '^(?P<url>https://(.*\.)?cnn\.com/.*)$'
|
||||
repl: 'https://reader.example.com/?url=\g<url>'
|
||||
- pattern: '^https?://(?:www\.)?facebook\.com/.*'
|
||||
repl: false
|
||||
- pattern: '^(?P<url>https://news\.example\.com/.*)$'
|
||||
repl: 'https://proxy.example.com/?url=\g<url>'
|
||||
priority: high
|
||||
|
||||
In this example:
|
||||
- CNN articles are redirected through a reader service
|
||||
- Facebook URLs are removed from results
|
||||
- News site URLs are proxied and given higher priority
|
||||
|
||||
The ``pattern`` field supports Python regular expressions with named capture groups.
|
||||
The ``repl`` field can reference captured groups using ``\g<name>`` syntax.
|
||||
Setting ``repl`` to ``false`` will remove matching results entirely.
|
||||
The optional ``priority`` field can be set to 'high' or 'low' to adjust result ranking.
|
||||
"""
|
||||
import re
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from flask_babel import gettext
|
||||
|
||||
from searx import settings
|
||||
from searx.settings_loader import get_yaml_cfg
|
||||
from searx.plugins import logger
|
||||
|
||||
|
||||
name = gettext("URL Rewrite")
|
||||
description = gettext("Rewrite URLs of search results")
|
||||
default_on = True
|
||||
preference_section = 'general'
|
||||
plugin_id = 'url_rewrite'
|
||||
|
||||
|
||||
logger = logger.getChild(plugin_id)
|
||||
config = settings.get(plugin_id, {})
|
||||
rules = config.get("rules", [])
|
||||
|
||||
|
||||
def on_result(request, search, result):
|
||||
if not rules:
|
||||
logger.debug("No url rewrite rules found in settings")
|
||||
return True
|
||||
|
||||
if 'url' not in result:
|
||||
logger.debug("No url found in result")
|
||||
return True
|
||||
|
||||
for rewrite in rules:
|
||||
pattern = rewrite.get('pattern')
|
||||
repl = rewrite.get('repl')
|
||||
priority = rewrite.get('priority')
|
||||
replace_url = rewrite.get('replace_url', True)
|
||||
|
||||
if not pattern:
|
||||
continue
|
||||
|
||||
if repl is None:
|
||||
logger.debug(f'No repl found for pattern {pattern}, skipping')
|
||||
continue
|
||||
|
||||
if re.search(pattern, result['url']):
|
||||
if repl is False:
|
||||
logger.info(f'Dropping {result["url"]} - matched {pattern}')
|
||||
return False
|
||||
|
||||
new_url = re.sub(pattern, repl, result['url'])
|
||||
result['url'] = new_url
|
||||
|
||||
if replace_url:
|
||||
result['parsed_url'] = urlparse(new_url)
|
||||
|
||||
if priority:
|
||||
result['priority'] = priority
|
||||
logger.info(f'Set priority to {priority} for {result["url"]}')
|
||||
|
||||
logger.info(f'Rewrote {result["url"]} using pattern {pattern}')
|
||||
break
|
||||
|
||||
return True
|
@ -1,6 +1,7 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Searx preferences implementation.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
# pylint: disable=useless-object-inheritance
|
||||
|
||||
@ -13,12 +14,14 @@ from collections import OrderedDict
|
||||
import flask
|
||||
import babel
|
||||
|
||||
import searx.plugins
|
||||
|
||||
from searx import settings, autocomplete, favicons
|
||||
from searx.enginelib import Engine
|
||||
from searx.plugins import Plugin
|
||||
from searx.engines import DEFAULT_CATEGORY
|
||||
from searx.extended_types import SXNG_Request
|
||||
from searx.locales import LOCALE_NAMES
|
||||
from searx.webutils import VALID_LANGUAGE_CODE
|
||||
from searx.engines import DEFAULT_CATEGORY
|
||||
|
||||
|
||||
COOKIE_MAX_AGE = 60 * 60 * 24 * 365 * 5 # 5 years
|
||||
@ -312,7 +315,7 @@ class EnginesSetting(BooleanChoices):
|
||||
class PluginsSetting(BooleanChoices):
|
||||
"""Plugin settings"""
|
||||
|
||||
def __init__(self, default_value, plugins: Iterable[Plugin]):
|
||||
def __init__(self, default_value, plugins: Iterable[searx.plugins.Plugin]):
|
||||
super().__init__(default_value, {plugin.id: plugin.default_on for plugin in plugins})
|
||||
|
||||
def transform_form_items(self, items):
|
||||
@ -340,7 +343,7 @@ class ClientPref:
|
||||
return tag
|
||||
|
||||
@classmethod
|
||||
def from_http_request(cls, http_request: flask.Request):
|
||||
def from_http_request(cls, http_request: SXNG_Request):
|
||||
"""Build ClientPref object from HTTP request.
|
||||
|
||||
- `Accept-Language used for locale setting
|
||||
@ -375,11 +378,11 @@ class Preferences:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
themes: List[str],
|
||||
categories: List[str],
|
||||
engines: Dict[str, Engine],
|
||||
plugins: Iterable[Plugin],
|
||||
client: Optional[ClientPref] = None,
|
||||
themes: list[str],
|
||||
categories: list[str],
|
||||
engines: dict[str, Engine],
|
||||
plugins: searx.plugins.PluginStorage,
|
||||
client: ClientPref | None = None,
|
||||
):
|
||||
|
||||
super().__init__()
|
||||
|
58
searx/result_types/__init__.py
Normal file
58
searx/result_types/__init__.py
Normal file
@ -0,0 +1,58 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Typification of the result items generated by the *engines*, *answerers* and
|
||||
*plugins*.
|
||||
|
||||
.. note::
|
||||
|
||||
We are at the beginning of typing the results. Further typing will follow,
|
||||
but this is a very large task that we will only be able to implement
|
||||
gradually. For more, please read :ref:`result types`.
|
||||
|
||||
"""
|
||||
# pylint: disable=too-few-public-methods
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ["Result", "EngineResults", "AnswerSet", "Answer", "Translations"]
|
||||
|
||||
import abc
|
||||
|
||||
from searx import enginelib
|
||||
|
||||
from ._base import Result, LegacyResult
|
||||
from .answer import AnswerSet, Answer, Translations
|
||||
|
||||
|
||||
class ResultList(list, abc.ABC):
|
||||
"""Base class of all result lists (abstract)."""
|
||||
|
||||
class types: # pylint: disable=invalid-name
|
||||
"""The collection of result types (which have already been implemented)."""
|
||||
|
||||
Answer = Answer
|
||||
Translations = Translations
|
||||
|
||||
def __init__(self):
|
||||
# pylint: disable=useless-parent-delegation
|
||||
super().__init__()
|
||||
|
||||
def add(self, result: Result):
|
||||
"""Add a :py:`Result` item to the result list."""
|
||||
self.append(result)
|
||||
|
||||
|
||||
class EngineResults(ResultList):
|
||||
"""Result list that should be used by engine developers. For convenience,
|
||||
engine developers don't need to import types / see :py:obj:`ResultList.types`.
|
||||
|
||||
.. code:: python
|
||||
|
||||
from searx.result_types import EngineResults
|
||||
...
|
||||
def response(resp) -> EngineResults:
|
||||
res = EngineResults()
|
||||
...
|
||||
res.add( res.types.Answer(answer="lorem ipsum ..", url="https://example.org") )
|
||||
...
|
||||
return res
|
||||
"""
|
224
searx/result_types/_base.py
Normal file
224
searx/result_types/_base.py
Normal file
@ -0,0 +1,224 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# pylint: disable=too-few-public-methods, missing-module-docstring
|
||||
"""Basic types for the typification of results.
|
||||
|
||||
- :py:obj:`Result` base class
|
||||
- :py:obj:`LegacyResult` for internal use only
|
||||
|
||||
----
|
||||
|
||||
.. autoclass:: Result
|
||||
:members:
|
||||
|
||||
.. autoclass:: LegacyResult
|
||||
:members:
|
||||
"""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ["Result"]
|
||||
|
||||
import re
|
||||
import urllib.parse
|
||||
import warnings
|
||||
|
||||
import msgspec
|
||||
|
||||
|
||||
class Result(msgspec.Struct, kw_only=True):
|
||||
"""Base class of all result types :ref:`result types`."""
|
||||
|
||||
url: str | None = None
|
||||
"""A link related to this *result*"""
|
||||
|
||||
template: str = "default.html"
|
||||
"""Name of the template used to render the result.
|
||||
|
||||
By default :origin:`result_templates/default.html
|
||||
<searx/templates/simple/result_templates/default.html>` is used.
|
||||
"""
|
||||
|
||||
engine: str | None = ""
|
||||
"""Name of the engine *this* result comes from. In case of *plugins* a
|
||||
prefix ``plugin:`` is set, in case of *answerer* prefix ``answerer:`` is
|
||||
set.
|
||||
|
||||
The field is optional and is initialized from the context if necessary.
|
||||
"""
|
||||
|
||||
parsed_url: urllib.parse.ParseResult | None = None
|
||||
""":py:obj:`urllib.parse.ParseResult` of :py:obj:`Result.url`.
|
||||
|
||||
The field is optional and is initialized from the context if necessary.
|
||||
"""
|
||||
|
||||
def normalize_result_fields(self):
|
||||
"""Normalize a result ..
|
||||
|
||||
- if field ``url`` is set and field ``parse_url`` is unset, init
|
||||
``parse_url`` from field ``url``. This method can be extended in the
|
||||
inheritance.
|
||||
|
||||
"""
|
||||
|
||||
if not self.parsed_url and self.url:
|
||||
self.parsed_url = urllib.parse.urlparse(self.url)
|
||||
|
||||
# if the result has no scheme, use http as default
|
||||
if not self.parsed_url.scheme:
|
||||
self.parsed_url = self.parsed_url._replace(scheme="http")
|
||||
self.url = self.parsed_url.geturl()
|
||||
|
||||
def __post_init__(self):
|
||||
pass
|
||||
|
||||
def __hash__(self) -> int:
|
||||
"""Generates a hash value that uniquely identifies the content of *this*
|
||||
result. The method can be adapted in the inheritance to compare results
|
||||
from different sources.
|
||||
|
||||
If two result objects are not identical but have the same content, their
|
||||
hash values should also be identical.
|
||||
|
||||
The hash value is used in contexts, e.g. when checking for equality to
|
||||
identify identical results from different sources (engines).
|
||||
"""
|
||||
|
||||
return id(self)
|
||||
|
||||
def __eq__(self, other):
|
||||
"""py:obj:`Result` objects are equal if the hash values of the two
|
||||
objects are equal. If needed, its recommended to overwrite
|
||||
"py:obj:`Result.__hash__`."""
|
||||
|
||||
return hash(self) == hash(other)
|
||||
|
||||
# for legacy code where a result is treated as a Python dict
|
||||
|
||||
def __setitem__(self, field_name, value):
|
||||
|
||||
return setattr(self, field_name, value)
|
||||
|
||||
def __getitem__(self, field_name):
|
||||
|
||||
if field_name not in self.__struct_fields__:
|
||||
raise KeyError(f"{field_name}")
|
||||
return getattr(self, field_name)
|
||||
|
||||
def __iter__(self):
|
||||
|
||||
return iter(self.__struct_fields__)
|
||||
|
||||
def as_dict(self):
|
||||
return {f: getattr(self, f) for f in self.__struct_fields__}
|
||||
|
||||
|
||||
class MainResult(Result): # pylint: disable=missing-class-docstring
|
||||
|
||||
# open_group and close_group should not manged in the Result class (we should rop it from here!)
|
||||
open_group: bool = False
|
||||
close_group: bool = False
|
||||
|
||||
title: str = ""
|
||||
"""Link title of the result item."""
|
||||
|
||||
content: str = ""
|
||||
"""Extract or description of the result item"""
|
||||
|
||||
img_src: str = ""
|
||||
"""URL of a image that is displayed in the result item."""
|
||||
|
||||
thumbnail: str = ""
|
||||
"""URL of a thumbnail that is displayed in the result item."""
|
||||
|
||||
|
||||
class LegacyResult(dict):
|
||||
"""A wrapper around a legacy result item. The SearXNG core uses this class
|
||||
for untyped dictionaries / to be downward compatible.
|
||||
|
||||
This class is needed until we have implemented an :py:obj:`Result` class for
|
||||
each result type and the old usages in the codebase have been fully
|
||||
migrated.
|
||||
|
||||
There is only one place where this class is used, in the
|
||||
:py:obj:`searx.results.ResultContainer`.
|
||||
|
||||
.. attention::
|
||||
|
||||
Do not use this class in your own implementations!
|
||||
"""
|
||||
|
||||
UNSET = object()
|
||||
WHITESPACE_REGEX = re.compile('( |\t|\n)+', re.M | re.U)
|
||||
|
||||
def as_dict(self):
|
||||
return self
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Init fields with defaults / compare with defaults of the fields in class Result
|
||||
self.engine = self.get("engine", "")
|
||||
self.template = self.get("template", "default.html")
|
||||
self.url = self.get("url", None)
|
||||
self.parsed_url = self.get("parsed_url", None)
|
||||
|
||||
self.content = self.get("content", "")
|
||||
self.title = self.get("title", "")
|
||||
|
||||
# Legacy types that have already been ported to a type ..
|
||||
|
||||
if "answer" in self:
|
||||
warnings.warn(
|
||||
f"engine {self.engine} is using deprecated `dict` for answers"
|
||||
f" / use a class from searx.result_types.answer",
|
||||
DeprecationWarning,
|
||||
)
|
||||
self.template = "answer/legacy.html"
|
||||
|
||||
def __hash__(self) -> int: # type: ignore
|
||||
|
||||
if "answer" in self:
|
||||
return hash(self["answer"])
|
||||
if not any(cls in self for cls in ["suggestion", "correction", "infobox", "number_of_results", "engine_data"]):
|
||||
# it is a commun url-result ..
|
||||
return hash(self.url)
|
||||
return id(self)
|
||||
|
||||
def __eq__(self, other):
|
||||
|
||||
return hash(self) == hash(other)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
|
||||
return f"LegacyResult: {super().__repr__()}"
|
||||
|
||||
def __getattr__(self, name: str, default=UNSET):
|
||||
|
||||
if default == self.UNSET and name not in self:
|
||||
raise AttributeError(f"LegacyResult object has no field named: {name}")
|
||||
return self[name]
|
||||
|
||||
def __setattr__(self, name: str, val):
|
||||
|
||||
self[name] = val
|
||||
|
||||
def normalize_result_fields(self):
|
||||
|
||||
self.title = self.WHITESPACE_REGEX.sub(" ", self.title)
|
||||
|
||||
if not self.parsed_url and self.url:
|
||||
self.parsed_url = urllib.parse.urlparse(self.url)
|
||||
|
||||
# if the result has no scheme, use http as default
|
||||
if not self.parsed_url.scheme:
|
||||
self.parsed_url = self.parsed_url._replace(scheme="http")
|
||||
self.url = self.parsed_url.geturl()
|
||||
|
||||
if self.content:
|
||||
self.content = self.WHITESPACE_REGEX.sub(" ", self.content)
|
||||
if self.content == self.title:
|
||||
# avoid duplicate content between the content and title fields
|
||||
self.content = ""
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user