This commit is contained in:
Virág Gábor 2024-11-06 11:01:28 +01:00
parent 606d2c1dd6
commit 80b26a39c6
131 changed files with 12724 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

3
.dockerignore Normal file
View file

@ -0,0 +1,3 @@
.git/
venv/
test/

9
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,9 @@
# These are supported funding model platforms
github: benbusby
ko_fi: benbusby
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

45
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View file

@ -0,0 +1,45 @@
---
name: Bug report
about: Create a bug report to help fix an issue with Whoogle
title: "[BUG] <brief bug description>"
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Deployment Method**
- [ ] Heroku (one-click deploy)
- [ ] Docker
- [ ] `run` executable
- [ ] pip/pipx
- [ ] Other: [describe setup]
**Version of Whoogle Search**
- [ ] Latest build from [source] (i.e. GitHub, Docker Hub, pip, etc)
- [ ] Version [version number]
- [ ] Not sure
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View file

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest a feature that would improve Whoogle
title: "[FEATURE] <description of feature>"
labels: enhancement
assignees: ''
---
<!--
DO NOT REQUEST UI/THEME/GUI/APPEARANCE IMPROVEMENTS HERE
THESE SHOULD GO IN ISSUE #60
REQUESTING A NEW FEATURE SHOULD BE STRICTLY RELATED TO NEW FUNCTIONALITY
-->
**Describe the feature you'd like to see added**
A short description of the feature, and what it would accomplish.
**Additional context**
Add any other context or screenshots about the feature request here.

38
.github/ISSUE_TEMPLATE/new-theme.md vendored Normal file
View file

@ -0,0 +1,38 @@
---
name: New theme
about: Create a new theme for Whoogle
title: "[THEME] <your theme name>"
labels: theme
assignees: benbusby
---
Use the following template to design your theme, replacing the blank spaces with the colors of your choice.
```css
:root {
/* LIGHT THEME COLORS */
--whoogle-logo: #______;
--whoogle-page-bg: #______;
--whoogle-element-bg: #______;
--whoogle-text: #______;
--whoogle-contrast-text: #______;
--whoogle-secondary-text: #______;
--whoogle-result-bg: #______;
--whoogle-result-title: #______;
--whoogle-result-url: #______;
--whoogle-result-visited: #______;
/* DARK THEME COLORS */
--whoogle-dark-logo: #______;
--whoogle-dark-page-bg: #______;
--whoogle-dark-element-bg: #______;
--whoogle-dark-text: #______;
--whoogle-dark-contrast-text: #______;
--whoogle-dark-secondary-text: #______;
--whoogle-dark-result-bg: #______;
--whoogle-dark-result-title: #______;
--whoogle-dark-result-url: #______;
--whoogle-dark-result-visited: #______;
}
```

10
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View file

@ -0,0 +1,10 @@
---
name: Question
about: Ask a (simple) question about Whoogle
title: "[QUESTION] <question here>"
labels: question
assignees: ''
---
Type out your question here. Please make sure that this is a topic that isn't already covered in the README.

59
.github/workflows/buildx.yml vendored Normal file
View file

@ -0,0 +1,59 @@
name: buildx
on:
workflow_run:
workflows: ["docker_main"]
branches: [main]
types:
- completed
push:
tags:
- '*'
jobs:
on-success:
runs-on: ubuntu-latest
steps:
- name: Wait for tests to succeed
if: ${{ github.event.workflow_run.conclusion != 'success' && startsWith(github.ref, 'refs/tags') != true }}
run: exit 1
- name: checkout code
uses: actions/checkout@v2
- name: install buildx
id: buildx
uses: crazy-max/ghaction-docker-buildx@v1
with:
version: latest
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to ghcr.io
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: build and push the image
if: startsWith(github.ref, 'refs/heads/main') && github.actor == 'benbusby'
run: |
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
docker buildx ls
docker buildx build --push \
--tag benbusby/whoogle-search:latest \
--platform linux/amd64,linux/arm64 .
docker buildx build --push \
--tag ghcr.io/benbusby/whoogle-search:latest \
--platform linux/amd64,linux/arm64 .
- name: build and push tag
if: startsWith(github.ref, 'refs/tags')
run: |
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
docker buildx ls
docker buildx build --push \
--tag benbusby/whoogle-search:${GITHUB_REF#refs/*/v}\
--platform linux/amd64,linux/arm/v7,linux/arm64 .
docker buildx build --push \
--tag ghcr.io/benbusby/whoogle-search:${GITHUB_REF#refs/*/v}\
--platform linux/amd64,linux/arm/v7,linux/arm64 .

28
.github/workflows/docker_main.yml vendored Normal file
View file

@ -0,0 +1,28 @@
name: docker_main
on:
workflow_run:
workflows: ["tests"]
branches: [main]
types:
- completed
# TODO: Needs refactoring to use reusable workflows and share w/ docker_tests
jobs:
on-success:
runs-on: ubuntu-latest
steps:
- name: checkout code
uses: actions/checkout@v2
- name: build and test (docker)
run: |
docker build --tag whoogle-search:test .
docker run --publish 5000:5000 --detach --name whoogle-search-nocompose whoogle-search:test
sleep 15
docker exec whoogle-search-nocompose curl -f http://localhost:5000/healthz || exit 1
- name: build and test (docker-compose)
run: |
docker rm -f whoogle-search-nocompose
WHOOGLE_IMAGE="whoogle-search:test" docker compose up --detach
sleep 15
docker exec whoogle-search curl -f http://localhost:5000/healthz || exit 1

26
.github/workflows/docker_tests.yml vendored Normal file
View file

@ -0,0 +1,26 @@
name: docker_tests
on:
push:
branches: main
pull_request:
branches: main
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: checkout code
uses: actions/checkout@v2
- name: build and test (docker)
run: |
docker build --tag whoogle-search:test .
docker run --publish 5000:5000 --detach --name whoogle-search-nocompose whoogle-search:test
sleep 15
docker exec whoogle-search-nocompose curl -f http://localhost:5000/healthz || exit 1
- name: build and test (docker compose)
run: |
docker rm -f whoogle-search-nocompose
WHOOGLE_IMAGE="whoogle-search:test" docker compose up --detach
sleep 15
docker exec whoogle-search curl -f http://localhost:5000/healthz || exit 1

67
.github/workflows/pypi.yml vendored Normal file
View file

@ -0,0 +1,67 @@
name: pypi
on:
push:
branches: main
tags: v*
jobs:
publish-test:
name: Build and publish to TestPyPI
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.9
uses: actions/setup-python@v5
with:
python-version: 3.9
- name: Install pypa/build
run: >-
python -m
pip install
build
setuptools
--user
- name: Set dev timestamp
run: echo "DEV_BUILD=$(date +%s)" >> $GITHUB_ENV
- name: Build binary wheel and source tarball
run: >-
python -m
build
--sdist
--wheel
--outdir dist/
.
- name: Publish distribution to TestPyPI
uses: pypa/gh-action-pypi-publish@master
with:
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
repository_url: https://test.pypi.org/legacy/
publish:
name: Build and publish to PyPI
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.9
uses: actions/setup-python@v5
with:
python-version: 3.9
- name: Install pypa/build
run: >-
python -m
pip install
build
--user
- name: Build binary wheel and source tarball
run: >-
python -m
build
--sdist
--wheel
--outdir dist/
.
- name: Publish distribution to PyPI
if: startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@master
with:
password: ${{ secrets.PYPI_API_TOKEN }}

19
.github/workflows/scan.yml vendored Normal file
View file

@ -0,0 +1,19 @@
name: scan
on:
schedule:
- cron: '0 0 * * *'
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build the container image
run: |
docker build --tag whoogle-search:test .
- name: Initiate grype scan
run: |
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b .
chmod +x ./grype
./grype whoogle-search:test --only-fixed

17
.github/workflows/tests.yml vendored Normal file
View file

@ -0,0 +1,17 @@
name: tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies
run: pip install --upgrade pip && pip install -r requirements.txt
- name: Run tests
run: ./run test

27
.gitignore vendored Normal file
View file

@ -0,0 +1,27 @@
venv/
.venv/
.idea/
__pycache__/
*.pyc
*.pem
*.conf
*.key
config.json
test/static
flask_session/
app/static/config
app/static/custom_config
app/static/bangs/*
!app/static/bangs/00-whoogle.json
# pip stuff
/build/
dist/
*.egg-info/
# env
whoogle.env
# vim
*~
*.swp

1
.replit Normal file
View file

@ -0,0 +1 @@
entrypoint = "misc/replit.py"

103
Dockerfile Normal file
View file

@ -0,0 +1,103 @@
FROM python:3.12.6-alpine3.20 AS builder
RUN apk --update add \
build-base \
libxml2-dev \
libxslt-dev \
openssl-dev \
libffi-dev
COPY requirements.txt .
RUN pip install --upgrade pip
RUN pip install --prefix /install --no-warn-script-location --no-cache-dir -r requirements.txt
FROM python:3.12.6-alpine3.20
RUN apk add --update --no-cache tor curl openrc libstdc++
# git go //for obfs4proxy
# libcurl4-openssl-dev
RUN apk -U upgrade
# uncomment to build obfs4proxy
# RUN git clone https://gitlab.com/yawning/obfs4.git
# WORKDIR /obfs4
# RUN go build -o obfs4proxy/obfs4proxy ./obfs4proxy
# RUN cp ./obfs4proxy/obfs4proxy /usr/bin/obfs4proxy
ARG DOCKER_USER=whoogle
ARG DOCKER_USERID=927
ARG config_dir=/config
RUN mkdir -p $config_dir
RUN chmod a+w $config_dir
VOLUME $config_dir
ARG url_prefix=''
ARG username=''
ARG password=''
ARG proxyuser=''
ARG proxypass=''
ARG proxytype=''
ARG proxyloc=''
ARG whoogle_dotenv=''
ARG use_https=''
ARG whoogle_port=5000
ARG twitter_alt='farside.link/nitter'
ARG youtube_alt='farside.link/invidious'
ARG reddit_alt='farside.link/libreddit'
ARG medium_alt='farside.link/scribe'
ARG translate_alt='farside.link/lingva'
ARG imgur_alt='farside.link/rimgo'
ARG wikipedia_alt='farside.link/wikiless'
ARG imdb_alt='farside.link/libremdb'
ARG quora_alt='farside.link/quetre'
ARG so_alt='farside.link/anonymousoverflow'
ENV CONFIG_VOLUME=$config_dir \
WHOOGLE_URL_PREFIX=$url_prefix \
WHOOGLE_USER=$username \
WHOOGLE_PASS=$password \
WHOOGLE_PROXY_USER=$proxyuser \
WHOOGLE_PROXY_PASS=$proxypass \
WHOOGLE_PROXY_TYPE=$proxytype \
WHOOGLE_PROXY_LOC=$proxyloc \
WHOOGLE_DOTENV=$whoogle_dotenv \
HTTPS_ONLY=$use_https \
EXPOSE_PORT=$whoogle_port \
WHOOGLE_ALT_TW=$twitter_alt \
WHOOGLE_ALT_YT=$youtube_alt \
WHOOGLE_ALT_RD=$reddit_alt \
WHOOGLE_ALT_MD=$medium_alt \
WHOOGLE_ALT_TL=$translate_alt \
WHOOGLE_ALT_IMG=$imgur_alt \
WHOOGLE_ALT_WIKI=$wikipedia_alt \
WHOOGLE_ALT_IMDB=$imdb_alt \
WHOOGLE_ALT_QUORA=$quora_alt \
WHOOGLE_ALT_SO=$so_alt
WORKDIR /whoogle
COPY --from=builder /install /usr/local
COPY misc/tor/torrc /etc/tor/torrc
COPY misc/tor/start-tor.sh misc/tor/start-tor.sh
COPY app/ app/
COPY run whoogle.env* ./
# Create user/group to run as
RUN adduser -D -g $DOCKER_USERID -u $DOCKER_USERID $DOCKER_USER
# Fix ownership / permissions
RUN chown -R ${DOCKER_USER}:${DOCKER_USER} /whoogle /var/lib/tor
# Allow writing symlinks to build dir
RUN chown $DOCKER_USERID:$DOCKER_USERID app/static/build
USER $DOCKER_USER:$DOCKER_USER
EXPOSE $EXPOSE_PORT
HEALTHCHECK --interval=30s --timeout=5s \
CMD curl -f http://localhost:${EXPOSE_PORT}/healthz || exit 1
CMD misc/tor/start-tor.sh & ./run

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Ben Busby
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

6
MANIFEST.in Normal file
View file

@ -0,0 +1,6 @@
graft app/static
graft app/templates
graft app/misc
include requirements.txt
recursive-include test
global-exclude *.pyc

194
app.json Normal file
View file

@ -0,0 +1,194 @@
{
"name": "Whoogle Search",
"description": "A lightweight, privacy-oriented, containerized Google search proxy for desktop/mobile that removes Javascript, AMP links, tracking, and ads/sponsored content",
"repository": "https://github.com/benbusby/whoogle-search",
"logo": "https://raw.githubusercontent.com/benbusby/whoogle-search/master/app/static/img/favicon/ms-icon-150x150.png",
"keywords": [
"search",
"metasearch",
"flask",
"docker",
"heroku",
"adblock",
"degoogle",
"privacy"
],
"stack": "container",
"env": {
"WHOOGLE_URL_PREFIX": {
"description": "The URL prefix to use for the whoogle instance (i.e. \"/whoogle\")",
"value": "",
"required": false
},
"WHOOGLE_USER": {
"description": "The username for basic auth. WHOOGLE_PASS must also be set if used. Leave empty to disable.",
"value": "",
"required": false
},
"WHOOGLE_PASS": {
"description": "The password for basic auth. WHOOGLE_USER must also be set if used. Leave empty to disable.",
"value": "",
"required": false
},
"WHOOGLE_PROXY_USER": {
"description": "The username of the proxy server. Leave empty to disable.",
"value": "",
"required": false
},
"WHOOGLE_PROXY_PASS": {
"description": "The password of the proxy server. Leave empty to disable.",
"value": "",
"required": false
},
"WHOOGLE_PROXY_TYPE": {
"description": "The type of the proxy server. For example \"socks5\". Leave empty to disable.",
"value": "",
"required": false
},
"WHOOGLE_PROXY_LOC": {
"description": "The location of the proxy server (host or ip). Leave empty to disable.",
"value": "",
"required": false
},
"WHOOGLE_ALT_TW": {
"description": "The site to use as a replacement for twitter.com when site alternatives are enabled in the config.",
"value": "farside.link/nitter",
"required": false
},
"WHOOGLE_ALT_YT": {
"description": "The site to use as a replacement for youtube.com when site alternatives are enabled in the config.",
"value": "farside.link/invidious",
"required": false
},
"WHOOGLE_ALT_RD": {
"description": "The site to use as a replacement for reddit.com when site alternatives are enabled in the config.",
"value": "farside.link/libreddit",
"required": false
},
"WHOOGLE_ALT_MD": {
"description": "The site to use as a replacement for medium.com when site alternatives are enabled in the config.",
"value": "farside.link/scribe",
"required": false
},
"WHOOGLE_ALT_TL": {
"description": "The Google Translate alternative to use for all searches following the 'translate ___' structure.",
"value": "farside.link/lingva",
"required": false
},
"WHOOGLE_ALT_IMG": {
"description": "The site to use as a replacement for imgur.com when site alternatives are enabled in the config.",
"value": "farside.link/rimgo",
"required": false
},
"WHOOGLE_ALT_WIKI": {
"description": "The site to use as a replacement for wikipedia.com when site alternatives are enabled in the config.",
"value": "farside.link/wikiless",
"required": false
},
"WHOOGLE_ALT_IMDB": {
"description": "The site to use as a replacement for imdb.com when site alternatives are enabled in the config.",
"value": "farside.link/libremdb",
"required": false
},
"WHOOGLE_ALT_QUORA": {
"description": "The site to use as a replacement for quora.com when site alternatives are enabled in the config.",
"value": "farside.link/quetre",
"required": false
},
"WHOOGLE_ALT_SO": {
"description": "The site to use as a replacement for stackoverflow.com when site alternatives are enabled in the config.",
"value": "farside.link/anonymousoverflow",
"required": false
},
"WHOOGLE_MINIMAL": {
"description": "Remove everything except basic result cards from all search queries (set to 1 or leave blank)",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_COUNTRY": {
"description": "[CONFIG] The country to use for restricting search results (use values from https://raw.githubusercontent.com/benbusby/whoogle-search/develop/app/static/settings/countries.json)",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_TIME_PERIOD" : {
"description": "[CONFIG] The time period to use for restricting search results",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_LANGUAGE": {
"description": "[CONFIG] The language to use for the interface (use values from https://raw.githubusercontent.com/benbusby/whoogle-search/develop/app/static/settings/languages.json)",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_SEARCH_LANGUAGE": {
"description": "[CONFIG] The language to use for search results (use values from https://raw.githubusercontent.com/benbusby/whoogle-search/develop/app/static/settings/languages.json)",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_DISABLE": {
"description": "[CONFIG] Disable ability for client to change config (set to 1 or leave blank)",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_BLOCK": {
"description": "[CONFIG] Block websites from search results (comma-separated list)",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_THEME": {
"description": "[CONFIG] Set theme to 'dark', 'light', or 'system'",
"value": "system",
"required": false
},
"WHOOGLE_CONFIG_SAFE": {
"description": "[CONFIG] Use safe mode for searches (set to 1 or leave blank)",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_ALTS": {
"description": "[CONFIG] Use social media alternatives (set to 1 or leave blank)",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_NEAR": {
"description": "[CONFIG] Restrict results to only those near a particular city",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_TOR": {
"description": "[CONFIG] Use Tor, if available (set to 1 or leave blank)",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_NEW_TAB": {
"description": "[CONFIG] Always open results in new tab (set to 1 or leave blank)",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_VIEW_IMAGE": {
"description": "[CONFIG] Enable View Image option (set to 1 or leave blank)",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_GET_ONLY": {
"description": "[CONFIG] Search using GET requests only (set to 1 or leave blank)",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_STYLE": {
"description": "[CONFIG] Custom CSS styling (paste in CSS or leave blank)",
"value": ":root { /* LIGHT THEME COLORS */ --whoogle-background: #d8dee9; --whoogle-accent: #2e3440; --whoogle-text: #3B4252; --whoogle-contrast-text: #eceff4; --whoogle-secondary-text: #70757a; --whoogle-result-bg: #fff; --whoogle-result-title: #4c566a; --whoogle-result-url: #81a1c1; --whoogle-result-visited: #a3be8c; /* DARK THEME COLORS */ --whoogle-dark-background: #222; --whoogle-dark-accent: #685e79; --whoogle-dark-text: #fff; --whoogle-dark-contrast-text: #000; --whoogle-dark-secondary-text: #bbb; --whoogle-dark-result-bg: #000; --whoogle-dark-result-title: #1967d2; --whoogle-dark-result-url: #4b11a8; --whoogle-dark-result-visited: #bbbbff; }",
"required": false
},
"WHOOGLE_CONFIG_PREFERENCES_ENCRYPTED": {
"description": "[CONFIG] Encrypt preferences token, requires WHOOGLE_CONFIG_PREFERENCES_KEY to be set",
"value": "",
"required": false
},
"WHOOGLE_CONFIG_PREFERENCES_KEY": {
"description": "[CONFIG] Key to encrypt preferences",
"value": "NEEDS_TO_BE_MODIFIED",
"required": false
}
}
}

BIN
app/.DS_Store vendored Normal file

Binary file not shown.

199
app/__init__.py Executable file
View file

@ -0,0 +1,199 @@
from app.filter import clean_query
from app.request import send_tor_signal
from app.utils.session import generate_key
from app.utils.bangs import gen_bangs_json, load_all_bangs
from app.utils.misc import gen_file_hash, read_config_bool
from base64 import b64encode
from bs4 import MarkupResemblesLocatorWarning
from datetime import datetime, timedelta
from dotenv import load_dotenv
from flask import Flask
import json
import logging.config
import os
from stem import Signal
import threading
import warnings
from werkzeug.middleware.proxy_fix import ProxyFix
from app.utils.misc import read_config_bool
from app.version import __version__
app = Flask(__name__, static_folder=os.path.dirname(
os.path.abspath(__file__)) + '/static')
app.wsgi_app = ProxyFix(app.wsgi_app)
# look for WHOOGLE_ENV, else look in parent directory
dot_env_path = os.getenv(
"WHOOGLE_DOTENV_PATH",
os.path.join(os.path.dirname(os.path.abspath(__file__)), "../whoogle.env"))
# Load .env file if enabled
if os.path.exists(dot_env_path):
load_dotenv(dot_env_path)
app.enc_key = generate_key()
if read_config_bool('HTTPS_ONLY'):
app.config['SESSION_COOKIE_NAME'] = '__Secure-session'
app.config['SESSION_COOKIE_SECURE'] = True
app.config['VERSION_NUMBER'] = __version__
app.config['APP_ROOT'] = os.getenv(
'APP_ROOT',
os.path.dirname(os.path.abspath(__file__)))
app.config['STATIC_FOLDER'] = os.getenv(
'STATIC_FOLDER',
os.path.join(app.config['APP_ROOT'], 'static'))
app.config['BUILD_FOLDER'] = os.path.join(
app.config['STATIC_FOLDER'], 'build')
app.config['CACHE_BUSTING_MAP'] = {}
app.config['LANGUAGES'] = json.load(open(
os.path.join(app.config['STATIC_FOLDER'], 'settings/languages.json'),
encoding='utf-8'))
app.config['COUNTRIES'] = json.load(open(
os.path.join(app.config['STATIC_FOLDER'], 'settings/countries.json'),
encoding='utf-8'))
app.config['TIME_PERIODS'] = json.load(open(
os.path.join(app.config['STATIC_FOLDER'], 'settings/time_periods.json'),
encoding='utf-8'))
app.config['TRANSLATIONS'] = json.load(open(
os.path.join(app.config['STATIC_FOLDER'], 'settings/translations.json'),
encoding='utf-8'))
app.config['THEMES'] = json.load(open(
os.path.join(app.config['STATIC_FOLDER'], 'settings/themes.json'),
encoding='utf-8'))
app.config['HEADER_TABS'] = json.load(open(
os.path.join(app.config['STATIC_FOLDER'], 'settings/header_tabs.json'),
encoding='utf-8'))
app.config['CONFIG_PATH'] = os.getenv(
'CONFIG_VOLUME',
os.path.join(app.config['STATIC_FOLDER'], 'config'))
app.config['DEFAULT_CONFIG'] = os.path.join(
app.config['CONFIG_PATH'],
'config.json')
app.config['CONFIG_DISABLE'] = read_config_bool('WHOOGLE_CONFIG_DISABLE')
app.config['SESSION_FILE_DIR'] = os.path.join(
app.config['CONFIG_PATH'],
'session')
app.config['MAX_SESSION_SIZE'] = 4000 # Sessions won't exceed 4KB
app.config['BANG_PATH'] = os.getenv(
'CONFIG_VOLUME',
os.path.join(app.config['STATIC_FOLDER'], 'bangs'))
app.config['BANG_FILE'] = os.path.join(
app.config['BANG_PATH'],
'bangs.json')
# Ensure all necessary directories exist
if not os.path.exists(app.config['CONFIG_PATH']):
os.makedirs(app.config['CONFIG_PATH'])
if not os.path.exists(app.config['SESSION_FILE_DIR']):
os.makedirs(app.config['SESSION_FILE_DIR'])
if not os.path.exists(app.config['BANG_PATH']):
os.makedirs(app.config['BANG_PATH'])
if not os.path.exists(app.config['BUILD_FOLDER']):
os.makedirs(app.config['BUILD_FOLDER'])
# Session values
app_key_path = os.path.join(app.config['CONFIG_PATH'], 'whoogle.key')
if os.path.exists(app_key_path):
try:
app.config['SECRET_KEY'] = open(app_key_path, 'r').read()
except PermissionError:
app.config['SECRET_KEY'] = str(b64encode(os.urandom(32)))
else:
app.config['SECRET_KEY'] = str(b64encode(os.urandom(32)))
with open(app_key_path, 'w') as key_file:
key_file.write(app.config['SECRET_KEY'])
key_file.close()
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=365)
# NOTE: SESSION_COOKIE_SAMESITE must be set to 'lax' to allow the user's
# previous session to persist when accessing the instance from an external
# link. Setting this value to 'strict' causes Whoogle to revalidate a new
# session, and fail, resulting in cookies being disabled.
app.config['SESSION_COOKIE_SAMESITE'] = 'Strict'
# Config fields that are used to check for updates
app.config['RELEASES_URL'] = 'https://github.com/' \
'benbusby/whoogle-search/releases'
app.config['LAST_UPDATE_CHECK'] = datetime.now() - timedelta(hours=24)
app.config['HAS_UPDATE'] = ''
# The alternative to Google Translate is treated a bit differently than other
# social media site alternatives, in that it is used for any translation
# related searches.
translate_url = os.getenv('WHOOGLE_ALT_TL', 'https://farside.link/lingva')
if not translate_url.startswith('http'):
translate_url = 'https://' + translate_url
app.config['TRANSLATE_URL'] = translate_url
app.config['CSP'] = 'default-src \'none\';' \
'frame-src ' + translate_url + ';' \
'manifest-src \'self\';' \
'img-src \'self\' data:;' \
'style-src \'self\' \'unsafe-inline\';' \
'script-src \'self\';' \
'media-src \'self\';' \
'connect-src \'self\';'
# Generate DDG bang filter
generating_bangs = False
if not os.path.exists(app.config['BANG_FILE']):
generating_bangs = True
json.dump({}, open(app.config['BANG_FILE'], 'w'))
bangs_thread = threading.Thread(
target=gen_bangs_json,
args=(app.config['BANG_FILE'],))
bangs_thread.start()
# Build new mapping of static files for cache busting
cache_busting_dirs = ['css', 'js']
for cb_dir in cache_busting_dirs:
full_cb_dir = os.path.join(app.config['STATIC_FOLDER'], cb_dir)
for cb_file in os.listdir(full_cb_dir):
# Create hash from current file state
full_cb_path = os.path.join(full_cb_dir, cb_file)
cb_file_link = gen_file_hash(full_cb_dir, cb_file)
build_path = os.path.join(app.config['BUILD_FOLDER'], cb_file_link)
try:
os.symlink(full_cb_path, build_path)
except FileExistsError:
# Symlink hasn't changed, ignore
pass
# Create mapping for relative path urls
map_path = build_path.replace(app.config['APP_ROOT'], '')
if map_path.startswith('/'):
map_path = map_path[1:]
app.config['CACHE_BUSTING_MAP'][cb_file] = map_path
# Templating functions
app.jinja_env.globals.update(clean_query=clean_query)
app.jinja_env.globals.update(
cb_url=lambda f: app.config['CACHE_BUSTING_MAP'][f.lower()])
# Attempt to acquire tor identity, to determine if Tor config is available
send_tor_signal(Signal.HEARTBEAT)
# Suppress spurious warnings from BeautifulSoup
warnings.simplefilter('ignore', MarkupResemblesLocatorWarning)
from app import routes # noqa
# The gen_bangs_json function takes care of loading bangs, so skip it here if
# it's already being loaded
if not generating_bangs:
load_all_bangs(app.config['BANG_FILE'])
# Disable logging from imported modules
logging.config.dictConfig({
'version': 1,
'disable_existing_loggers': True,
})

3
app/__main__.py Executable file
View file

@ -0,0 +1,3 @@
from .routes import run_app
run_app()

785
app/filter.py Executable file
View file

@ -0,0 +1,785 @@
import cssutils
from bs4 import BeautifulSoup
from bs4.element import ResultSet, Tag
from cryptography.fernet import Fernet
from flask import render_template
import html
import urllib.parse as urlparse
from urllib.parse import parse_qs
import re
from app.models.g_classes import GClasses
from app.request import VALID_PARAMS, MAPS_URL
from app.utils.misc import get_abs_url, read_config_bool
from app.utils.results import (
BLANK_B64, GOOG_IMG, GOOG_STATIC, G_M_LOGO_URL, LOGO_URL, SITE_ALTS,
has_ad_content, filter_link_args, append_anon_view, get_site_alt,
)
from app.models.endpoint import Endpoint
from app.models.config import Config
MAPS_ARGS = ['q', 'daddr']
minimal_mode_sections = ['Top stories', 'Images', 'People also ask']
unsupported_g_pages = [
'google.com/aclk'
'*.googleapis.com'
'*.gstatic.com'
'*.google-analytics.com'
'adservice.google.com'
'support.google.com',
'accounts.google.com',
'policies.google.com',
'google.com/preferences',
'google.com/intl',
'advanced_search',
'tbm=shop',
'ageverification.google.co.kr'
]
unsupported_g_divs = [
'google.com/preferences?hl=',
'ageverification.google.co.kr'
'google.com/aclk?sa='
]
def extract_q(q_str: str, href: str) -> str:
"""Extracts the 'q' element from a result link. This is typically
either the link to a result's website, or a string.
Args:
q_str: The result link to parse
href: The full url to check for standalone 'q' elements first,
rather than parsing the whole query string and then checking.
Returns:
str: The 'q' element of the link, or an empty string
"""
return parse_qs(q_str, keep_blank_values=True)['q'][0] if ('&q=' in href or '?q=' in href) else ''
def build_map_url(href: str) -> str:
"""Tries to extract known args that explain the location in the url. If a
location is found, returns the default url with it. Otherwise, returns the
url unchanged.
Args:
href: The full url to check.
Returns:
str: The parsed url, or the url unchanged.
"""
# parse the url
parsed_url = parse_qs(href)
# iterate through the known parameters and try build the url
for param in MAPS_ARGS:
if param in parsed_url:
return MAPS_URL + "?q=" + parsed_url[param][0]
# query could not be extracted returning unchanged url
return href
def clean_query(query: str) -> str:
"""Strips the blocked site list from the query, if one is being
used.
Args:
query: The query string
Returns:
str: The query string without any "-site:..." filters
"""
return query[:query.find('-site:')] if '-site:' in query else query
def clean_css(css: str, page_url: str) -> str:
"""Removes all remote URLs from a CSS string.
Args:
css: The CSS string
Returns:
str: The filtered CSS, with URLs proxied through Whoogle
"""
sheet = cssutils.parseString(css)
urls = cssutils.getUrls(sheet)
for url in urls:
abs_url = get_abs_url(url, page_url)
if abs_url.startswith('data:'):
continue
css = css.replace(
url,
f'{Endpoint.element}?type=image/png&url={abs_url}'
)
return css
class Filter:
# Limit used for determining if a result is a "regular" result or a list
# type result (such as "people also asked", "related searches", etc)
RESULT_CHILD_LIMIT = 7
def __init__(
self,
user_key: str,
config: Config,
root_url='',
page_url='',
query='',
mobile=False) -> None:
self.soup = None
self.config = config
self.mobile = mobile
self.user_key = user_key
self.page_url = page_url
self.query = query
self.main_divs = ResultSet('')
self._elements = 0
self._av = set()
self.root_url = root_url[:-1] if root_url.endswith('/') else root_url
def __getitem__(self, name):
return getattr(self, name)
@property
def elements(self):
return self._elements
def encrypt_path(self, path, is_element=False) -> str:
# Encrypts path to avoid plaintext results in logs
if is_element:
# Element paths are encrypted separately from text, to allow key
# regeneration once all items have been served to the user
enc_path = Fernet(self.user_key).encrypt(path.encode()).decode()
self._elements += 1
return enc_path
return Fernet(self.user_key).encrypt(path.encode()).decode()
def clean(self, soup) -> BeautifulSoup:
self.soup = soup
self.main_divs = self.soup.find('div', {'id': 'main'})
self.remove_ads()
self.remove_block_titles()
self.remove_block_url()
self.collapse_sections()
self.update_css()
self.update_styling()
self.remove_block_tabs()
# self.main_divs is only populated for the main page of search results
# (i.e. not images/news/etc).
if self.main_divs:
for div in self.main_divs:
self.sanitize_div(div)
for img in [_ for _ in self.soup.find_all('img') if 'src' in _.attrs]:
self.update_element_src(img, 'image/png')
for audio in [_ for _ in self.soup.find_all('audio') if 'src' in _.attrs]:
self.update_element_src(audio, 'audio/mpeg')
audio['controls'] = ''
for link in self.soup.find_all('a', href=True):
self.update_link(link)
self.add_favicon(link)
if self.config.alts:
self.site_alt_swap()
input_form = self.soup.find('form')
if input_form is not None:
input_form['method'] = 'GET' if self.config.get_only else 'POST'
# Use a relative URI for submissions
input_form['action'] = 'search'
# Ensure no extra scripts passed through
for script in self.soup('script'):
script.decompose()
# Update default footer and header
footer = self.soup.find('footer')
if footer:
# Remove divs that have multiple links beyond just page navigation
[_.decompose() for _ in footer.find_all('div', recursive=False)
if len(_.find_all('a', href=True)) > 3]
for link in footer.find_all('a', href=True):
link['href'] = f'{link["href"]}&preferences={self.config.preferences}'
header = self.soup.find('header')
if header:
header.decompose()
self.remove_site_blocks(self.soup)
return self.soup
def sanitize_div(self, div) -> None:
"""Removes escaped script and iframe tags from results
Returns:
None (The soup object is modified directly)
"""
if not div:
return
for d in div.find_all('div', recursive=True):
d_text = d.find(text=True, recursive=False)
# Ensure we're working with tags that contain text content
if not d_text or not d.string:
continue
d.string = html.unescape(d_text)
div_soup = BeautifulSoup(d.string, 'html.parser')
# Remove all valid script or iframe tags in the div
for script in div_soup.find_all('script'):
script.decompose()
for iframe in div_soup.find_all('iframe'):
iframe.decompose()
d.string = str(div_soup)
def add_favicon(self, link) -> None:
"""Adds icons for each returned result, using the result site's favicon
Returns:
None (The soup object is modified directly)
"""
# Skip empty, parentless, or internal links
show_favicons = read_config_bool('WHOOGLE_SHOW_FAVICONS', True)
is_valid_link = link and link.parent and link['href'].startswith('http')
if not show_favicons or not is_valid_link:
return
parent = link.parent
is_result_div = False
# Check each parent to make sure that the div doesn't already have a
# favicon attached, and that the div is a result div
while parent:
p_cls = parent.attrs.get('class') or []
if 'has-favicon' in p_cls or GClasses.scroller_class in p_cls:
return
elif GClasses.result_class_a not in p_cls:
parent = parent.parent
else:
is_result_div = True
break
if not is_result_div:
return
# Construct the html for inserting the icon into the parent div
parsed = urlparse.urlparse(link['href'])
favicon = self.encrypt_path(
f'{parsed.scheme}://{parsed.netloc}/favicon.ico',
is_element=True)
src = f'{self.root_url}/{Endpoint.element}?url={favicon}' + \
'&type=image/x-icon'
html = f'<img class="site-favicon" src="{src}">'
favicon = BeautifulSoup(html, 'html.parser')
link.parent.insert(0, favicon)
# Update all parents to indicate that a favicon has been attached
parent = link.parent
while parent:
p_cls = parent.get('class') or []
p_cls.append('has-favicon')
parent['class'] = p_cls
parent = parent.parent
if GClasses.result_class_a in p_cls:
break
def remove_site_blocks(self, soup) -> None:
if not self.config.block or not soup.body:
return
search_string = ' '.join(['-site:' +
_ for _ in self.config.block.split(',')])
selected = soup.body.findAll(text=re.compile(search_string))
for result in selected:
result.string.replace_with(result.string.replace(
search_string, ''))
def remove_ads(self) -> None:
"""Removes ads found in the list of search result divs
Returns:
None (The soup object is modified directly)
"""
if not self.main_divs:
return
for div in [_ for _ in self.main_divs.find_all('div', recursive=True)]:
div_ads = [_ for _ in div.find_all('span', recursive=True)
if has_ad_content(_.text)]
_ = div.decompose() if len(div_ads) else None
def remove_block_titles(self) -> None:
if not self.main_divs or not self.config.block_title:
return
block_title = re.compile(self.config.block_title)
for div in [_ for _ in self.main_divs.find_all('div', recursive=True)]:
block_divs = [_ for _ in div.find_all('h3', recursive=True)
if block_title.search(_.text) is not None]
_ = div.decompose() if len(block_divs) else None
def remove_block_url(self) -> None:
if not self.main_divs or not self.config.block_url:
return
block_url = re.compile(self.config.block_url)
for div in [_ for _ in self.main_divs.find_all('div', recursive=True)]:
block_divs = [_ for _ in div.find_all('a', recursive=True)
if block_url.search(_.attrs['href']) is not None]
_ = div.decompose() if len(block_divs) else None
def remove_block_tabs(self) -> None:
if self.main_divs:
for div in self.main_divs.find_all(
'div',
attrs={'class': f'{GClasses.main_tbm_tab}'}
):
_ = div.decompose()
else:
# when in images tab
for div in self.soup.find_all(
'div',
attrs={'class': f'{GClasses.images_tbm_tab}'}
):
_ = div.decompose()
def collapse_sections(self) -> None:
"""Collapses long result sections ("people also asked", "related
searches", etc) into "details" elements
These sections are typically the only sections in the results page that
have more than ~5 child divs within a primary result div.
Returns:
None (The soup object is modified directly)
"""
minimal_mode = read_config_bool('WHOOGLE_MINIMAL')
def pull_child_divs(result_div: BeautifulSoup):
try:
return result_div.findChildren(
'div', recursive=False
)[0].findChildren(
'div', recursive=False)
except IndexError:
return []
if not self.main_divs:
return
#töörölni kell People also ask,
search_terms = ["People also search for", "Related searches", "Kapcsolódó keresések", "Mások ezeket keresték még"]
details_list = []
# Loop through results and check for the number of child divs in each
for result in self.main_divs.find_all():
result_children = pull_child_divs(result)
if minimal_mode:
if any(f">{x}</span" in str(s) for s in result_children
for x in minimal_mode_sections):
result.decompose()
continue
for s in result_children:
if ('Twitter ' in str(s)):
result.decompose()
continue
if len(result_children) < self.RESULT_CHILD_LIMIT:
continue
else:
if len(result_children) < self.RESULT_CHILD_LIMIT:
continue
# Find and decompose the first element with an inner HTML text val.
# This typically extracts the title of the section (i.e. "Related
# Searches", "People also ask", etc)
# If there are more than one child tags with text
# parenthesize the rest except the first
label = 'Collapsed Results'
subtitle = None
for elem in result_children:
if elem.text:
content = list(elem.strings)
label = content[0]
if len(content) > 1:
subtitle = '<span> (' + \
''.join(content[1:]) + ')</span>'
elem.decompose()
break
# Determine the class based on the label content
if any(term in label for term in search_terms):
details_class = 'search-recommendations'
details_attrs = {'class': details_class, 'open': 'true'}
else:
details_class = 'other-results'
details_attrs = {'class': details_class}
# Create the new details element to wrap around the result's
# first parent
parent = None
idx = 0
while not parent and idx < len(result_children):
parent = result_children[idx].parent
idx += 1
details = BeautifulSoup(features='html.parser').new_tag('details', attrs=details_attrs)
summary = BeautifulSoup(features='html.parser').new_tag('summary', attrs={'class': "summary_div"})
summary.string = label
if subtitle:
soup = BeautifulSoup(subtitle, 'html.parser')
summary.append(soup)
details.append(summary)
if parent and not minimal_mode:
parent.wrap(details)
elif parent and minimal_mode:
# Remove parent element from document if "minimal mode" is
# enabled
parent.decompose()
for details in details_list:
self.main_divs.append(details)
def update_element_src(self, element: Tag, mime: str, attr='src') -> None:
"""Encrypts the original src of an element and rewrites the element src
to use the "/element?src=" pass-through.
Returns:
None (The soup element is modified directly)
"""
src = element[attr].split(' ')[0]
if src.startswith('//'):
src = 'https:' + src
elif src.startswith('data:'):
return
if src.startswith(LOGO_URL):
# Re-brand with Whoogle logo
element.replace_with(BeautifulSoup(
render_template('logo.html'),
features='html.parser'))
return
elif src.startswith(G_M_LOGO_URL):
# Re-brand with single-letter Whoogle logo
element['src'] = 'static/img/favicon/apple-icon.png'
element.parent['href'] = 'home'
return
elif src.startswith(GOOG_IMG) or GOOG_STATIC in src:
element['src'] = BLANK_B64
return
element[attr] = f'{self.root_url}/{Endpoint.element}?url=' + (
self.encrypt_path(
src,
is_element=True
) + '&type=' + urlparse.quote(mime)
)
def update_css(self) -> None:
"""Updates URLs used in inline styles to be proxied by Whoogle
using the /element endpoint.
Returns:
None (The soup element is modified directly)
"""
# Filter all <style> tags
for style in self.soup.find_all('style'):
style.string = clean_css(style.string, self.page_url)
# TODO: Convert remote stylesheets to style tags and proxy all
# remote requests
# for link in soup.find_all('link', attrs={'rel': 'stylesheet'}):
# print(link)
def update_styling(self) -> None:
# Update CSS classes for result divs
soup = GClasses.replace_css_classes(self.soup)
# Remove unnecessary button(s)
for button in self.soup.find_all('button'):
button.decompose()
# Remove svg logos
for svg in self.soup.find_all('svg'):
svg.decompose()
# Update logo
logo = self.soup.find('a', {'class': 'l'})
if logo and self.mobile:
logo['style'] = ('display:flex; justify-content:center; '
'align-items:center; color:#685e79; '
'font-size:18px; ')
# Fix search bar length on mobile
""" try:
search_bar = self.soup.find('header').find('form').find('div')
search_bar['style'] = 'width: 100%;'
except AttributeError:
pass
# Fix body max width on images tab
style = self.soup.find('style')
div = self.soup.find('div', attrs={
'class': f'{GClasses.images_tbm_tab}'})
if style and div and not self.mobile:
css = style.string
css_html_tag = (
'html{'
'font-family: Roboto, Helvetica Neue, Arial, sans-serif;'
'font-size: 14px;'
'line-height: 20px;'
'text-size-adjust: 100%;'
'word-wrap: break-word;'
'}'
)
css = f"{css_html_tag}{css}"
css = re.sub('body{(.*?)}',
'body{padding:0 8px;margin:0 auto;max-width:900px;}',
css)
style.string = css """
def update_link(self, link: Tag) -> None:
"""Update internal link paths with encrypted path, otherwise remove
unnecessary redirects and/or marketing params from the url
Args:
link: A bs4 Tag element to inspect and update
Returns:
None (the tag is updated directly)
"""
parsed_link = urlparse.urlparse(link['href'])
if '/url?q=' in link['href']:
link_netloc = extract_q(parsed_link.query, link['href'])
else:
link_netloc = parsed_link.netloc
# Remove any elements that direct to unsupported Google pages
if any(url in link_netloc for url in unsupported_g_pages):
# FIXME: The "Shopping" tab requires further filtering (see #136)
# Temporarily removing all links to that tab for now.
# Replaces the /url google unsupported link to the direct url
link['href'] = link_netloc
parent = link.parent
if any(divlink in link_netloc for divlink in unsupported_g_divs):
# Handle case where a search is performed in a different
# language than what is configured. This usually returns a
# div with the same classes as normal search results, but with
# a link to configure language preferences through Google.
# Since we want all language config done through Whoogle, we
# can safely decompose this element.
while parent:
p_cls = parent.attrs.get('class') or []
if f'{GClasses.result_class_a}' in p_cls:
parent.decompose()
break
parent = parent.parent
else:
# Remove cases where google links appear in the footer
while parent:
p_cls = parent.attrs.get('class') or []
if parent.name == 'footer' or f'{GClasses.footer}' in p_cls:
link.decompose()
parent = parent.parent
if link.decomposed:
return
# Replace href with only the intended destination (no "utm" type tags)
href = link['href'].replace('https://www.google.com', '')
result_link = urlparse.urlparse(href)
q = extract_q(result_link.query, href)
if q.startswith('/') and q not in self.query and 'spell=1' not in href:
# Internal google links (i.e. mail, maps, etc) should still
# be forwarded to Google
link['href'] = 'https://google.com' + q
elif q.startswith('https://accounts.google.com'):
# Remove Sign-in link
link.decompose()
return
elif '/search?q=' in href:
# "li:1" implies the query should be interpreted verbatim,
# which is accomplished by wrapping the query in double quotes
if 'li:1' in href:
q = '"' + q + '"'
new_search = 'search?q=' + self.encrypt_path(q)
query_params = parse_qs(urlparse.urlparse(href).query)
for param in VALID_PARAMS:
if param not in query_params:
continue
param_val = query_params[param][0]
new_search += '&' + param + '=' + param_val
link['href'] = new_search
elif 'url?q=' in href:
# Strip unneeded arguments
link['href'] = filter_link_args(q)
# Add alternate viewing options for results,
# if the result doesn't already have an AV link
netloc = urlparse.urlparse(link['href']).netloc
if self.config.anon_view and netloc not in self._av:
self._av.add(netloc)
append_anon_view(link, self.config)
else:
if href.startswith(MAPS_URL):
# Maps links don't work if a site filter is applied
link['href'] = build_map_url(link['href'])
elif (href.startswith('/?') or href.startswith('/search?') or
href.startswith('/imgres?')):
# make sure that tags can be clicked as relative URLs
link['href'] = href[1:]
elif href.startswith('/intl/'):
# do nothing, keep original URL for ToS
pass
elif href.startswith('/preferences'):
# there is no config specific URL, remove this
link.decompose()
return
else:
link['href'] = href
if self.config.new_tab and (
link["href"].startswith("http")
or link["href"].startswith("imgres?")
):
link["target"] = "_blank"
def site_alt_swap(self) -> None:
"""Replaces link locations and page elements if "alts" config
is enabled
"""
for site, alt in SITE_ALTS.items():
if site != "medium.com" and alt != "":
# Ignore medium.com replacements since these are handled
# specifically in the link description replacement, and medium
# results are never given their own "card" result where this
# replacement would make sense.
# Also ignore if the alt is empty, since this is used to indicate
# that the alt is not enabled.
for div in self.soup.find_all('div', text=re.compile(site)):
# Use the number of words in the div string to determine if the
# string is a result description (shouldn't replace domains used
# in desc text).
if len(div.string.split(' ')) == 1:
div.string = div.string.replace(site, alt)
for link in self.soup.find_all('a', href=True):
# Search and replace all link descriptions
# with alternative location
link['href'] = get_site_alt(link['href'])
link_desc = link.find_all(
text=re.compile('|'.join(SITE_ALTS.keys())))
if len(link_desc) == 0:
continue
# Replace link description
link_desc = link_desc[0]
if site not in link_desc or not alt:
continue
new_desc = BeautifulSoup(features='html.parser').new_tag('div')
link_str = str(link_desc)
# Medium links should be handled differently, since 'medium.com'
# is a common substring of domain names, but shouldn't be
# replaced (i.e. 'philomedium.com' should stay as it is).
if 'medium.com' in link_str:
if link_str.startswith('medium.com') or '.medium.com' in link_str:
link_str = SITE_ALTS['medium.com'] + link_str[
link_str.find('medium.com') + len('medium.com'):]
new_desc.string = link_str
else:
new_desc.string = link_str.replace(site, alt)
link_desc.replace_with(new_desc)
def view_image(self, soup) -> BeautifulSoup:
"""Replaces the soup with a new one that handles mobile results and
adds the link of the image full res to the results.
Args:
soup: A BeautifulSoup object containing the image mobile results.
Returns:
BeautifulSoup: The new BeautifulSoup object
"""
# get some tags that are unchanged between mobile and pc versions
cor_suggested = soup.find_all(class_="By0U9")
next_pages = soup.find(class_= "uZgmoc")
results = []
# find results div
results_div = soup.find('div', attrs={'class': "nQvrDb"})
# find all the results (if any)
results_all = []
if results_div:
results_all = results_div.find_all('div', attrs={'class': "lIMUZd"})
for item in results_all:
urls = item.find('a')['href'].split('&imgrefurl=')
# Skip urls that are not two-element lists
if len(urls) != 2:
continue
img_url = urlparse.unquote(urls[0].replace(
f'/{Endpoint.imgres}?imgurl=', ''))
try:
# Try to strip out only the necessary part of the web page link
web_page = urlparse.unquote(urls[1].split('&')[0])
except IndexError:
web_page = urlparse.unquote(urls[1])
img_tbn = urlparse.unquote(item.find('a').find('img')['src'])
results.append({
'domain': urlparse.urlparse(web_page).netloc,
'img_url': img_url,
'web_page': web_page,
'img_tbn': img_tbn,
})
# Create a new BeautifulSoup object with the template
soup = BeautifulSoup(render_template('imageresults.html',
length=len(results),
results=results,
view_label="Kép megtekintése"),
features='html.parser')
# replace correction suggested by google object if exists
if len(cor_suggested):
soup.find_all(class_= "By0U9"
)[0].replaceWith(cor_suggested[0])
# replace next page object at the bottom of the page
soup.find_all(class_= "uZgmoc")[0].replaceWith(next_pages)
return soup

BIN
app/models/.DS_Store vendored Executable file

Binary file not shown.

0
app/models/__init__.py Executable file
View file

267
app/models/config.py Executable file
View file

@ -0,0 +1,267 @@
from inspect import Attribute
from typing import Optional
from app.utils.misc import read_config_bool
from flask import current_app
import os
from base64 import urlsafe_b64encode, urlsafe_b64decode
from cryptography.fernet import Fernet
import hashlib
import brotli
import logging
import json
import cssutils
from cssutils.css.cssstylesheet import CSSStyleSheet
from cssutils.css.cssstylerule import CSSStyleRule
# removes warnings from cssutils
cssutils.log.setLevel(logging.CRITICAL)
def get_rule_for_selector(stylesheet: CSSStyleSheet,
selector: str) -> Optional[CSSStyleRule]:
"""Search for a rule that matches a given selector in a stylesheet.
Args:
stylesheet (CSSStyleSheet) -- the stylesheet to search
selector (str) -- the selector to search for
Returns:
Optional[CSSStyleRule] -- the rule that matches the selector or None
"""
for rule in stylesheet.cssRules:
if hasattr(rule, "selectorText") and selector == rule.selectorText:
return rule
return None
class Config:
def __init__(self, **kwargs):
app_config = current_app.config
self.url = os.getenv('WHOOGLE_CONFIG_URL', '')
self.lang_search = os.getenv('WHOOGLE_CONFIG_SEARCH_LANGUAGE', '')
self.lang_interface = os.getenv('WHOOGLE_CONFIG_LANGUAGE', default='lang_hu')
self.style_modified = os.getenv(
'WHOOGLE_CONFIG_STYLE', '')
self.block = os.getenv('WHOOGLE_CONFIG_BLOCK', '')
self.block_title = os.getenv('WHOOGLE_CONFIG_BLOCK_TITLE', '')
self.block_url = os.getenv('WHOOGLE_CONFIG_BLOCK_URL', '')
self.country = os.getenv('WHOOGLE_CONFIG_COUNTRY', '')
self.tbs = os.getenv('WHOOGLE_CONFIG_TIME_PERIOD', '')
self.theme = os.getenv('WHOOGLE_CONFIG_THEME', 'dark')
self.safe = read_config_bool('WHOOGLE_CONFIG_SAFE', default=True)
self.dark = read_config_bool('WHOOGLE_CONFIG_DARK') # deprecated
self.alts = read_config_bool('WHOOGLE_CONFIG_ALTS')
self.nojs = read_config_bool('WHOOGLE_CONFIG_NOJS')
self.tor = read_config_bool('WHOOGLE_CONFIG_TOR')
self.near = os.getenv('WHOOGLE_CONFIG_NEAR', '')
self.new_tab = read_config_bool('WHOOGLE_CONFIG_NEW_TAB')
self.view_image = read_config_bool('WHOOGLE_CONFIG_VIEW_IMAGE', default=True)
self.get_only = read_config_bool('WHOOGLE_CONFIG_GET_ONLY')
self.anon_view = read_config_bool('WHOOGLE_CONFIG_ANON_VIEW')
self.preferences_encrypted = read_config_bool('WHOOGLE_CONFIG_PREFERENCES_ENCRYPTED')
self.preferences_key = os.getenv('WHOOGLE_CONFIG_PREFERENCES_KEY', '')
self.accept_language = False
self.safe_keys = [
'lang_search',
'lang_interface',
'country',
'theme',
'alts',
'new_tab',
'view_image',
'block',
'safe',
'nojs',
'anon_view',
'preferences_encrypted',
'tbs'
]
# Skip setting custom config if there isn't one
if kwargs:
mutable_attrs = self.get_mutable_attrs()
for attr in mutable_attrs:
if attr in kwargs.keys():
setattr(self, attr, kwargs[attr])
elif attr not in kwargs.keys() and mutable_attrs[attr] == bool:
setattr(self, attr, False)
def __getitem__(self, name):
return getattr(self, name)
def __setitem__(self, name, value):
return setattr(self, name, value)
def __delitem__(self, name):
return delattr(self, name)
def __contains__(self, name):
return hasattr(self, name)
def get_mutable_attrs(self):
return {name: type(attr) for name, attr in self.__dict__.items()
if not name.startswith("__")
and (type(attr) is bool or type(attr) is str)}
def get_attrs(self):
return {name: attr for name, attr in self.__dict__.items()
if not name.startswith("__")
and (type(attr) is bool or type(attr) is str)}
@property
def style(self) -> str:
"""Returns the default style updated with specified modifications.
Returns:
str -- the new style
"""
style_sheet = cssutils.parseString(
open(os.path.join(current_app.config['STATIC_FOLDER'],
'css/variables.css')).read()
)
modified_sheet = cssutils.parseString(self.style_modified)
for rule in modified_sheet:
rule_default = get_rule_for_selector(style_sheet,
rule.selectorText)
# if modified rule is in default stylesheet, update it
if rule_default is not None:
# TODO: update this in a smarter way to handle :root better
# for now if we change a varialbe in :root all other default
# variables need to be also present
rule_default.style = rule.style
# else add the new rule to the default stylesheet
else:
style_sheet.add(rule)
return str(style_sheet.cssText, 'utf-8')
@property
def preferences(self) -> str:
# if encryption key is not set will uncheck preferences encryption
if self.preferences_encrypted:
self.preferences_encrypted = bool(self.preferences_key)
# add a tag for visibility if preferences token startswith 'e' it means
# the token is encrypted, 'u' means the token is unencrypted and can be
# used by other whoogle instances
encrypted_flag = "e" if self.preferences_encrypted else 'u'
preferences_digest = self._encode_preferences()
return f"{encrypted_flag}{preferences_digest}"
def is_safe_key(self, key) -> bool:
"""Establishes a group of config options that are safe to set
in the url.
Args:
key (str) -- the key to check against
Returns:
bool -- True/False depending on if the key is in the "safe"
array
"""
return key in self.safe_keys
def get_localization_lang(self):
"""Returns the correct language to use for localization, but falls
back to english if not set.
Returns:
str -- the localization language string
"""
if (self.lang_interface and
self.lang_interface in current_app.config['TRANSLATIONS']):
return self.lang_interface
return 'lang_en'
def from_params(self, params) -> 'Config':
"""Modify user config with search parameters. This is primarily
used for specifying configuration on a search-by-search basis on
public instances.
Args:
params -- the url arguments (can be any deemed safe by is_safe())
Returns:
Config -- a modified config object
"""
if 'preferences' in params:
params_new = self._decode_preferences(params['preferences'])
# if preferences leads to an empty dictionary it means preferences
# parameter was not decrypted successfully
if len(params_new):
params = params_new
for param_key in params.keys():
if not self.is_safe_key(param_key):
continue
param_val = params.get(param_key)
if param_val == 'off':
param_val = False
elif isinstance(param_val, str):
if param_val.isdigit():
param_val = int(param_val)
self[param_key] = param_val
return self
def to_params(self, keys: list = []) -> str:
"""Generates a set of safe params for using in Whoogle URLs
Args:
keys (list) -- optional list of keys of URL parameters
Returns:
str -- a set of URL parameters
"""
if not len(keys):
keys = self.safe_keys
param_str = ''
for safe_key in keys:
if not self[safe_key]:
continue
param_str = param_str + f'&{safe_key}={self[safe_key]}'
return param_str
def _get_fernet_key(self, password: str) -> bytes:
hash_object = hashlib.md5(password.encode())
key = urlsafe_b64encode(hash_object.hexdigest().encode())
return key
def _encode_preferences(self) -> str:
preferences_json = json.dumps(self.get_attrs()).encode()
compressed_preferences = brotli.compress(preferences_json)
if self.preferences_encrypted and self.preferences_key:
key = self._get_fernet_key(self.preferences_key)
encrypted_preferences = Fernet(key).encrypt(compressed_preferences)
compressed_preferences = brotli.compress(encrypted_preferences)
return urlsafe_b64encode(compressed_preferences).decode()
def _decode_preferences(self, preferences: str) -> dict:
mode = preferences[0]
preferences = preferences[1:]
try:
decoded_data = brotli.decompress(urlsafe_b64decode(preferences.encode() + b'=='))
if mode == 'e' and self.preferences_key:
# preferences are encrypted
key = self._get_fernet_key(self.preferences_key)
decrypted_data = Fernet(key).decrypt(decoded_data)
decoded_data = brotli.decompress(decrypted_data)
config = json.loads(decoded_data)
except Exception:
config = {}
return config

22
app/models/endpoint.py Executable file
View file

@ -0,0 +1,22 @@
from enum import Enum
class Endpoint(Enum):
autocomplete = 'autocomplete'
home = 'home'
healthz = 'healthz'
config = 'config'
opensearch = 'opensearch.xml'
search = 'search'
search_html = 'search.html'
url = 'url'
imgres = 'imgres'
element = 'element'
window = 'window'
def __str__(self):
return self.value
def in_path(self, path: str) -> bool:
return path.startswith(self.value) or \
path.startswith(f'/{self.value}')

48
app/models/g_classes.py Executable file
View file

@ -0,0 +1,48 @@
from bs4 import BeautifulSoup
class GClasses:
"""A class for tracking obfuscated class names used in Google results that
are directly referenced in Whoogle's filtering code.
Note: Using these should be a last resort. It is always preferred to filter
results using structural cues instead of referencing class names, as these
are liable to change at any moment.
"""
main_tbm_tab = 'KP7LCb'
images_tbm_tab = 'n692Zd'
footer = 'TuS8Ad'
result_class_a = 'ZINbbc'
result_class_b = 'luh4td'
scroller_class = 'idg8be'
line_tag = 'BsXmcf'
result_classes = {
result_class_a: ['Gx5Zad'],
result_class_b: ['fP1Qef']
}
@classmethod
def replace_css_classes(cls, soup: BeautifulSoup) -> BeautifulSoup:
"""Replace updated Google classes with the original class names that
Whoogle relies on for styling.
Args:
soup: The result page as a BeautifulSoup object
Returns:
BeautifulSoup: The new BeautifulSoup
"""
result_divs = soup.find_all('div', {
'class': [_ for c in cls.result_classes.values() for _ in c]
})
for div in result_divs:
new_class = ' '.join(div['class'])
for key, val in cls.result_classes.items():
new_class = ' '.join(new_class.replace(_, key) for _ in val)
div['class'] = new_class.split(' ')
return soup
def __str__(self):
return self.value

352
app/request.py Executable file
View file

@ -0,0 +1,352 @@
from app.models.config import Config
from app.utils.misc import read_config_bool
from datetime import datetime
from defusedxml import ElementTree as ET
import random
import requests
from requests import Response, ConnectionError
import urllib.parse as urlparse
import os
from stem import Signal, SocketError
from stem.connection import AuthenticationFailure
from stem.control import Controller
from stem.connection import authenticate_cookie, authenticate_password
MAPS_URL = 'https://maps.google.com/maps'
AUTOCOMPLETE_URL = ('https://suggestqueries.google.com/'
'complete/search?client=toolbar&')
MOBILE_UA = '{}/5.0 (Android 0; Mobile; rv:54.0) Gecko/54.0 {}/59.0'
DESKTOP_UA = '{}/5.0 (X11; {} x86_64; rv:75.0) Gecko/20100101 {}/75.0'
# Valid query params
VALID_PARAMS = ['tbs', 'tbm', 'start', 'near', 'source', 'nfpr']
class TorError(Exception):
"""Exception raised for errors in Tor requests.
Attributes:
message: a message describing the error that occurred
disable: optionally disables Tor in the user config (note:
this should only happen if the connection has been dropped
altogether).
"""
def __init__(self, message, disable=False) -> None:
self.message = message
self.disable = disable
super().__init__(message)
def send_tor_signal(signal: Signal) -> bool:
use_pass = read_config_bool('WHOOGLE_TOR_USE_PASS')
confloc = './misc/tor/control.conf'
# Check that the custom location of conf is real.
temp = os.getenv('WHOOGLE_TOR_CONF', '')
if os.path.isfile(temp):
confloc = temp
# Attempt to authenticate and send signal.
try:
with Controller.from_port(port=9051) as c:
if use_pass:
with open(confloc, "r") as conf:
# Scan for the last line of the file.
for line in conf:
pass
secret = line.strip('\n')
authenticate_password(c, password=secret)
else:
cookie_path = '/var/lib/tor/control_auth_cookie'
authenticate_cookie(c, cookie_path=cookie_path)
c.signal(signal)
os.environ['TOR_AVAILABLE'] = '1'
return True
except (SocketError, AuthenticationFailure,
ConnectionRefusedError, ConnectionError):
# TODO: Handle Tor authentication (password and cookie)
os.environ['TOR_AVAILABLE'] = '0'
return False
def gen_user_agent(is_mobile) -> str:
user_agent = os.environ.get('WHOOGLE_USER_AGENT', '')
user_agent_mobile = os.environ.get('WHOOGLE_USER_AGENT_MOBILE', '')
if user_agent and not is_mobile:
return user_agent
if user_agent_mobile and is_mobile:
return user_agent_mobile
firefox = random.choice(['Choir', 'Squier', 'Higher', 'Wire']) + 'fox'
linux = random.choice(['Win', 'Sin', 'Gin', 'Fin', 'Kin']) + 'ux'
if is_mobile:
return MOBILE_UA.format("Mozilla", firefox)
return DESKTOP_UA.format("Mozilla", linux, firefox)
def gen_query(query, args, config) -> str:
param_dict = {key: '' for key in VALID_PARAMS}
# Use :past(hour/day/week/month/year) if available
# example search "new restaurants :past month"
lang = ''
if ':past' in query and 'tbs' not in args:
time_range = str.strip(query.split(':past', 1)[-1])
param_dict['tbs'] = '&tbs=' + ('qdr:' + str.lower(time_range[0]))
elif 'tbs' in args or 'tbs' in config:
result_tbs = args.get('tbs') if 'tbs' in args else config['tbs']
param_dict['tbs'] = '&tbs=' + result_tbs
# Occasionally the 'tbs' param provided by google also contains a
# field for 'lr', but formatted strangely. This is a rough solution
# for this.
#
# Example:
# &tbs=qdr:h,lr:lang_1pl
# -- the lr param needs to be extracted and remove the leading '1'
result_params = [_ for _ in result_tbs.split(',') if 'lr:' in _]
if len(result_params) > 0:
result_param = result_params[0]
lang = result_param[result_param.find('lr:') + 3:len(result_param)]
# Ensure search query is parsable
query = urlparse.quote(query)
# Pass along type of results (news, images, books, etc)
if 'tbm' in args:
param_dict['tbm'] = '&tbm=' + args.get('tbm')
# Get results page start value (10 per page, ie page 2 start val = 20)
if 'start' in args:
param_dict['start'] = '&start=' + args.get('start')
# Search for results near a particular city, if available
if config.near:
param_dict['near'] = '&near=' + urlparse.quote(config.near)
# Set language for results (lr) if source isn't set, otherwise use the
# result language param provided in the results
if 'source' in args:
param_dict['source'] = '&source=' + args.get('source')
param_dict['lr'] = ('&lr=' + ''.join(
[_ for _ in lang if not _.isdigit()]
)) if lang else ''
else:
param_dict['lr'] = (
'&lr=' + config.lang_search
) if config.lang_search else ''
# 'nfpr' defines the exclusion of results from an auto-corrected query
if 'nfpr' in args:
param_dict['nfpr'] = '&nfpr=' + args.get('nfpr')
# 'chips' is used in image tabs to pass the optional 'filter' to add to the
# given search term
if 'chips' in args:
param_dict['chips'] = '&chips=' + args.get('chips')
param_dict['gl'] = (
'&gl=' + config.country
) if config.country else ''
param_dict['hl'] = (
'&hl=' + config.lang_interface.replace('lang_', '')
) if config.lang_interface else ''
param_dict['safe'] = '&safe=' + ('active' if config.safe else 'off')
# Block all sites specified in the user config
unquoted_query = urlparse.unquote(query)
for blocked_site in config.block.replace(' ', '').split(','):
if not blocked_site:
continue
block = (' -site:' + blocked_site)
query += block if block not in unquoted_query else ''
for val in param_dict.values():
if not val:
continue
query += val
return query
class Request:
"""Class used for handling all outbound requests, including search queries,
search suggestions, and loading of external content (images, audio, etc).
Attributes:
normal_ua: the user's current user agent
root_path: the root path of the whoogle instance
config: the user's current whoogle configuration
"""
def __init__(self, normal_ua, root_path, config: Config):
self.search_url = 'https://www.google.com/search?gbv=1&num=' + str(
os.getenv('WHOOGLE_RESULTS_PER_PAGE', 12)) + '&q='
# Send heartbeat to Tor, used in determining if the user can or cannot
# enable Tor for future requests
send_tor_signal(Signal.HEARTBEAT)
self.language = (
config.lang_search if config.lang_search else ''
)
self.country = config.country if config.country else ''
# For setting Accept-language Header
self.lang_interface = ''
if config.accept_language:
self.lang_interface = config.lang_interface
self.mobile = bool(normal_ua) and ('Android' in normal_ua
or 'iPhone' in normal_ua)
self.modified_user_agent = gen_user_agent(self.mobile)
if not self.mobile:
self.modified_user_agent_mobile = gen_user_agent(True)
# Set up proxy, if previously configured
proxy_path = os.environ.get('WHOOGLE_PROXY_LOC', '')
if proxy_path:
proxy_type = os.environ.get('WHOOGLE_PROXY_TYPE', '')
proxy_user = os.environ.get('WHOOGLE_PROXY_USER', '')
proxy_pass = os.environ.get('WHOOGLE_PROXY_PASS', '')
auth_str = ''
if proxy_user:
auth_str = f'{proxy_user}:{proxy_pass}@'
proxy_str = f'{proxy_type}://{auth_str}{proxy_path}'
self.proxies = {
'https': proxy_str,
'http': proxy_str
}
else:
self.proxies = {
'http': 'socks5://127.0.0.1:9050',
'https': 'socks5://127.0.0.1:9050'
} if config.tor else {}
self.tor = config.tor
self.tor_valid = False
self.root_path = root_path
def __getitem__(self, name):
return getattr(self, name)
def autocomplete(self, query) -> list:
"""Sends a query to Google's search suggestion service
Args:
query: The in-progress query to send
Returns:
list: The list of matches for possible search suggestions
"""
ac_query = dict(q=query)
if self.language:
ac_query['lr'] = self.language
if self.country:
ac_query['gl'] = self.country
if self.lang_interface:
ac_query['hl'] = self.lang_interface
response = self.send(base_url=AUTOCOMPLETE_URL,
query=urlparse.urlencode(ac_query)).text
if not response:
return []
try:
root = ET.fromstring(response)
return [_.attrib['data'] for _ in
root.findall('.//suggestion/[@data]')]
except ET.ParseError:
# Malformed XML response
return []
def send(self, base_url='', query='', attempt=0,
force_mobile=True, user_agent='') -> Response:
"""Sends an outbound request to a URL. Optionally sends the request
using Tor, if enabled by the user.
Args:
base_url: The URL to use in the request
query: The optional query string for the request
attempt: The number of attempts made for the request
(used for cycling through Tor identities, if enabled)
force_mobile: Optional flag to enable a mobile user agent
(used for fetching full size images in search results)
Returns:
Response: The Response object returned by the requests call
"""
use_client_user_agent = int(os.environ.get('WHOOGLE_USE_CLIENT_USER_AGENT', '0'))
if user_agent and use_client_user_agent == 1:
modified_user_agent = user_agent
else:
if force_mobile and not self.mobile:
modified_user_agent = self.modified_user_agent_mobile
else:
modified_user_agent = self.modified_user_agent
headers = {
'User-Agent': modified_user_agent
}
# Adding the Accept-Language to the Header if possible
if self.lang_interface:
headers.update({'Accept-Language':
self.lang_interface.replace('lang_', '')
+ ';q=1.0'})
# view is suppressed correctly
now = datetime.now()
cookies = {
'CONSENT': 'PENDING+987',
'SOCS': 'CAESHAgBEhIaAB',
}
# Validate Tor conn and request new identity if the last one failed
if self.tor and not send_tor_signal(
Signal.NEWNYM if attempt > 0 else Signal.HEARTBEAT):
raise TorError(
"Tor was previously enabled, but the connection has been "
"dropped. Please check your Tor configuration and try again.",
disable=True)
# Make sure that the tor connection is valid, if enabled
if self.tor:
try:
tor_check = requests.get('https://check.torproject.org/',
proxies=self.proxies, headers=headers)
self.tor_valid = 'Congratulations' in tor_check.text
if not self.tor_valid:
raise TorError(
"Tor connection succeeded, but the connection could "
"not be validated by torproject.org",
disable=True)
except ConnectionError:
raise TorError(
"Error raised during Tor connection validation",
disable=True)
response = requests.get(
(base_url or self.search_url) + query,
proxies=self.proxies,
headers=headers,
cookies=cookies)
# Retry query with new identity if using Tor (max 10 attempts)
if 'form id="captcha-form"' in response.text and self.tor:
attempt += 1
if attempt > 10:
raise TorError("Tor query failed -- max attempts exceeded 10")
return self.send((base_url or self.search_url), query, attempt)
return response

726
app/routes.py Executable file
View file

@ -0,0 +1,726 @@
import argparse
import base64
import io
import json
import os
import pickle
import re
import urllib.parse as urlparse
import uuid
import validators
import sys
import traceback
from datetime import datetime, timedelta
from functools import wraps
import logging
import traceback
from werkzeug.exceptions import HTTPException
from urllib.parse import unquote
import waitress
from app import app
from app.models.config import Config
from app.models.endpoint import Endpoint
from app.request import Request, TorError
from app.utils.bangs import suggest_bang, resolve_bang
from app.utils.misc import empty_gif, placeholder_img, get_proxy_host_url, \
fetch_favicon
from app.filter import Filter
from app.utils.misc import read_config_bool, get_client_ip, get_request_url, \
check_for_update, encrypt_string
from app.utils.widgets import *
from app.utils.results import bold_search_terms,\
add_currency_card, check_currency, get_tabs_content
from app.utils.search import Search, needs_https, has_captcha
from app.utils.session import valid_user_session
from bs4 import BeautifulSoup as bsoup
from flask import Flask, jsonify, make_response, request, redirect, render_template, \
send_file, session, url_for, g
from requests import exceptions
from requests.models import PreparedRequest
from cryptography.fernet import Fernet, InvalidToken
from cryptography.exceptions import InvalidSignature
from werkzeug.datastructures import MultiDict
ac_var = 'WHOOGLE_AUTOCOMPLETE'
autocomplete_enabled = os.getenv(ac_var, '1')
def get_search_name(tbm):
for tab in app.config['HEADER_TABS'].values():
if tab['tbm'] == tbm:
return tab['name']
def auth_required(f):
@wraps(f)
def decorated(*args, **kwargs):
# do not ask password if cookies already present
if (
valid_user_session(session)
and 'cookies_disabled' not in request.args
and session['auth']
):
return f(*args, **kwargs)
auth = request.authorization
# Skip if username/password not set
whoogle_user = os.getenv('WHOOGLE_USER', '')
whoogle_pass = os.getenv('WHOOGLE_PASS', '')
if (not whoogle_user or not whoogle_pass) or (
auth
and whoogle_user == auth.username
and whoogle_pass == auth.password):
session['auth'] = True
return f(*args, **kwargs)
else:
return make_response('Not logged in', 401, {
'WWW-Authenticate': 'Basic realm="Login Required"'})
return decorated
def session_required(f):
@wraps(f)
def decorated(*args, **kwargs):
if not valid_user_session(session):
session.pop('_permanent', None)
# Note: This sets all requests to use the encryption key determined per
# instance on app init. This can be updated in the future to use a key
# that is unique for their session (session['key']) but this should use
# a config setting to enable the session based key. Otherwise there can
# be problems with searches performed by users with cookies blocked if
# a session based key is always used.
g.session_key = app.enc_key
# Clear out old sessions
invalid_sessions = []
for user_session in os.listdir(app.config['SESSION_FILE_DIR']):
file_path = os.path.join(
app.config['SESSION_FILE_DIR'],
user_session)
try:
# Ignore files that are larger than the max session file size
if os.path.getsize(file_path) > app.config['MAX_SESSION_SIZE']:
continue
with open(file_path, 'rb') as session_file:
_ = pickle.load(session_file)
data = pickle.load(session_file)
if isinstance(data, dict) and 'valid' in data:
continue
invalid_sessions.append(file_path)
except Exception:
# Broad exception handling here due to how instances installed
# with pip seem to have issues storing unrelated files in the
# same directory as sessions
pass
for invalid_session in invalid_sessions:
try:
os.remove(invalid_session)
except FileNotFoundError:
# Don't throw error if the invalid session has been removed
pass
return f(*args, **kwargs)
return decorated
@app.before_request
def before_request_func():
session.permanent = True
# Check for latest version if needed
now = datetime.now()
needs_update_check = now - timedelta(hours=24) > app.config['LAST_UPDATE_CHECK']
if read_config_bool('WHOOGLE_UPDATE_CHECK', True) and needs_update_check:
app.config['LAST_UPDATE_CHECK'] = now
app.config['HAS_UPDATE'] = check_for_update(
app.config['RELEASES_URL'],
app.config['VERSION_NUMBER'])
g.request_params = (
request.args if request.method == 'GET' else request.form
)
default_config = json.load(open(app.config['DEFAULT_CONFIG'])) \
if os.path.exists(app.config['DEFAULT_CONFIG']) else {}
# Generate session values for user if unavailable
if not valid_user_session(session):
session['config'] = default_config
session['uuid'] = str(uuid.uuid4())
session['key'] = app.enc_key
session['auth'] = False
# Establish config values per user session
g.user_config = Config(**session['config'])
# Update user config if specified in search args
g.user_config = g.user_config.from_params(g.request_params)
if not g.user_config.url:
g.user_config.url = get_request_url(request.url_root)
g.user_request = Request(
request.headers.get('User-Agent'),
get_request_url(request.url_root),
config=g.user_config)
g.app_location = g.user_config.url
@app.after_request
def after_request_func(resp):
resp.headers['X-Content-Type-Options'] = 'nosniff'
resp.headers['X-Frame-Options'] = 'DENY'
resp.headers['Cache-Control'] = 'max-age=86400'
if os.getenv('WHOOGLE_CSP', False):
resp.headers['Content-Security-Policy'] = app.config['CSP']
if os.environ.get('HTTPS_ONLY', False):
resp.headers['Content-Security-Policy'] += \
'upgrade-insecure-requests'
return resp
@app.errorhandler(404)
def unknown_page(e):
app.logger.warn(e)
return redirect(g.app_location)
@app.route(f'/{Endpoint.healthz}', methods=['GET'])
def healthz():
return ''
@app.route('/', methods=['GET'])
@app.route(f'/{Endpoint.home}', methods=['GET'])
@auth_required
def index():
# Redirect if an error was raised
if 'error_message' in session and session['error_message']:
error_message = session['error_message']
session['error_message'] = ''
return render_template('error.html', error_message=error_message)
return render_template('index.html',
has_update=app.config['HAS_UPDATE'],
languages=app.config['LANGUAGES'],
countries=app.config['COUNTRIES'],
time_periods=app.config['TIME_PERIODS'],
themes=app.config['THEMES'],
autocomplete_enabled=autocomplete_enabled,
translation=app.config['TRANSLATIONS'][
g.user_config.get_localization_lang()
],
logo=render_template(
'logo.html',
dark=g.user_config.dark),
config_disabled=(
app.config['CONFIG_DISABLE'] or
not valid_user_session(session)),
config=g.user_config,
tor_available=int(os.environ.get('TOR_AVAILABLE')),
version_number=app.config['VERSION_NUMBER'])
@app.route(f'/{Endpoint.opensearch}', methods=['GET'])
def opensearch():
opensearch_url = g.app_location
if opensearch_url.endswith('/'):
opensearch_url = opensearch_url[:-1]
# Enforce https for opensearch template
if needs_https(opensearch_url):
opensearch_url = opensearch_url.replace('http://', 'https://', 1)
get_only = g.user_config.get_only or 'Chrome' in request.headers.get(
'User-Agent')
return render_template(
'opensearch.xml',
main_url=opensearch_url,
request_type='' if get_only else 'method="post"',
search_type=request.args.get('tbm'),
search_name=get_search_name(request.args.get('tbm'))
), 200, {'Content-Type': 'application/xml'}
@app.route(f'/{Endpoint.search_html}', methods=['GET'])
def search_html():
search_url = g.app_location
if search_url.endswith('/'):
search_url = search_url[:-1]
return render_template('search.html', url=search_url)
@app.route(f'/{Endpoint.autocomplete}', methods=['GET', 'POST'])
def autocomplete():
if os.getenv(ac_var) and not read_config_bool(ac_var):
return jsonify({})
q = g.request_params.get('q')
if not q:
# FF will occasionally (incorrectly) send the q field without a
# mimetype in the format "b'q=<query>'" through the request.data field
q = str(request.data).replace('q=', '')
# Search bangs if the query begins with "!", but not "! " (feeling lucky)
if q.startswith('!') and len(q) > 1 and not q.startswith('! '):
return jsonify([q, suggest_bang(q)])
if not q and not request.data:
return jsonify({'?': []})
elif request.data:
q = urlparse.unquote_plus(
request.data.decode('utf-8').replace('q=', ''))
# Return a list of suggestions for the query
#
# Note: If Tor is enabled, this returns nothing, as the request is
# almost always rejected
return jsonify([
q,
g.user_request.autocomplete(q) if not g.user_config.tor else []
])
@app.route(f'/{Endpoint.search}', methods=['GET', 'POST'])
@session_required
@auth_required
def search():
if request.method == 'POST':
# Redirect as a GET request with an encrypted query
post_data = MultiDict(request.form)
post_data['q'] = encrypt_string(g.session_key, post_data['q'])
get_req_str = urlparse.urlencode(post_data)
return redirect(url_for('.search') + '?' + get_req_str)
search_util = Search(request, g.user_config, g.session_key)
query = search_util.new_search_query()
bang = resolve_bang(query)
if bang:
return redirect(bang)
# Redirect to home if invalid/blank search
if not query:
return redirect(url_for('.index'))
# Generate response and number of external elements from the page
try:
response = search_util.generate_response()
except TorError as e:
session['error_message'] = e.message + (
"\\n\\nTor config is now disabled!" if e.disable else "")
session['config']['tor'] = False if e.disable else session['config'][
'tor']
return redirect(url_for('.index'))
if search_util.feeling_lucky:
return redirect(response, code=303)
# If the user is attempting to translate a string, determine the correct
# string for formatting the lingva.ml url
localization_lang = g.user_config.get_localization_lang()
translation = app.config['TRANSLATIONS'][localization_lang]
translate_to = localization_lang.replace('lang_', '')
# removing st-card to only use whoogle time selector
soup = bsoup(response, "html.parser");
for x in soup.find_all(attrs={"id": "st-card"}):
x.replace_with("")
response = str(soup)
# Return 503 if temporarily blocked by captcha
if has_captcha(str(response)):
app.logger.error('503 (CAPTCHA)')
fallback_engine = os.environ.get('WHOOGLE_FALLBACK_ENGINE_URL', '')
if (fallback_engine):
return redirect(fallback_engine + query)
return render_template(
'error.html',
blocked=True,
error_message=translation['ratelimit'],
translation=translation,
farside='https://farside.link',
config=g.user_config,
query=urlparse.unquote(query),
params=g.user_config.to_params(keys=['preferences'])), 503
response = bold_search_terms(response, query)
# check for widgets and add if requested
if search_util.widget != '':
html_soup = bsoup(str(response), 'html.parser')
if search_util.widget == 'ip':
response = add_ip_card(html_soup, get_client_ip(request))
elif search_util.widget == 'calculator' and not 'nojs' in request.args:
response = add_calculator_card(html_soup)
# Update tabs content
tabs = get_tabs_content(app.config['HEADER_TABS'],
search_util.full_query,
search_util.search_type,
g.user_config.preferences,
translation)
# Feature to display currency_card
# Since this is determined by more than just the
# query is it not defined as a standard widget
conversion = check_currency(str(response))
if conversion:
html_soup = bsoup(str(response), 'html.parser')
response = add_currency_card(html_soup, conversion)
preferences = g.user_config.preferences
home_url = f"home?preferences={preferences}" if preferences else "home"
cleanresponse = str(response).replace("andlt;","&lt;").replace("andgt;","&gt;")
return render_template(
'display.html',
has_update=app.config['HAS_UPDATE'],
query=urlparse.unquote(query),
search_type=search_util.search_type,
search_name=get_search_name(search_util.search_type),
config=g.user_config,
autocomplete_enabled=autocomplete_enabled,
lingva_url=app.config['TRANSLATE_URL'],
translation=translation,
translate_to=translate_to,
translate_str=query.replace(
'translate', ''
).replace(
translation['translate'], ''
),
is_translation=any(
_ in query.lower() for _ in [translation['translate'], 'translate']
) and not search_util.search_type, # Standard search queries only
response=cleanresponse,
version_number=app.config['VERSION_NUMBER'],
search_header=render_template(
'header.html',
home_url=home_url,
config=g.user_config,
translation=translation,
languages=app.config['LANGUAGES'],
countries=app.config['COUNTRIES'],
time_periods=app.config['TIME_PERIODS'],
logo=render_template('logo.html', dark=g.user_config.dark),
query=urlparse.unquote(query),
search_type=search_util.search_type,
mobile=g.user_request.mobile,
tabs=tabs)).replace(" ", "")
@app.route(f'/{Endpoint.config}', methods=['GET', 'POST', 'PUT'])
@session_required
@auth_required
def config():
config_disabled = (
app.config['CONFIG_DISABLE'] or
not valid_user_session(session))
name = ''
if 'name' in request.args:
name = os.path.normpath(request.args.get('name'))
if not re.match(r'^[A-Za-z0-9_.+-]+$', name):
return make_response('Invalid config name', 400)
if request.method == 'GET':
return json.dumps(g.user_config.__dict__)
elif request.method == 'PUT' and not config_disabled:
if name:
config_pkl = os.path.join(app.config['CONFIG_PATH'], name)
session['config'] = (pickle.load(open(config_pkl, 'rb'))
if os.path.exists(config_pkl)
else session['config'])
return json.dumps(session['config'])
else:
return json.dumps({})
elif not config_disabled:
config_data = request.form.to_dict()
if 'url' not in config_data or not config_data['url']:
config_data['url'] = g.user_config.url
# Save config by name to allow a user to easily load later
if 'name' in request.args:
pickle.dump(
config_data,
open(os.path.join(
app.config['CONFIG_PATH'],
name), 'wb'))
session['config'] = config_data
return redirect(config_data['url'])
else:
return redirect(url_for('.index'), code=403)
@app.route(f'/{Endpoint.imgres}')
@session_required
@auth_required
def imgres():
return redirect(request.args.get('imgurl'))
@app.route(f'/{Endpoint.element}')
@session_required
@auth_required
def element():
element_url = src_url = request.args.get('url')
if element_url.startswith('gAAAAA'):
try:
cipher_suite = Fernet(g.session_key)
src_url = cipher_suite.decrypt(element_url.encode()).decode()
except (InvalidSignature, InvalidToken) as e:
return render_template(
'error.html',
error_message=str(e)), 401
src_type = request.args.get('type')
# Ensure requested element is from a valid domain
domain = urlparse.urlparse(src_url).netloc
if not validators.domain(domain):
return send_file(io.BytesIO(empty_gif), mimetype='image/gif')
try:
response = g.user_request.send(base_url=src_url)
# Display an empty gif if the requested element couldn't be retrieved
if response.status_code != 200 or len(response.content) == 0:
if 'favicon' in src_url:
favicon = fetch_favicon(src_url)
return send_file(io.BytesIO(favicon), mimetype='image/png')
else:
return send_file(io.BytesIO(empty_gif), mimetype='image/gif')
file_data = response.content
tmp_mem = io.BytesIO()
tmp_mem.write(file_data)
tmp_mem.seek(0)
return send_file(tmp_mem, mimetype=src_type)
except exceptions.RequestException:
pass
return send_file(io.BytesIO(empty_gif), mimetype='image/gif')
@app.route(f'/{Endpoint.window}')
@session_required
@auth_required
def window():
target_url = request.args.get('location')
if target_url.startswith('gAAAAA'):
cipher_suite = Fernet(g.session_key)
target_url = cipher_suite.decrypt(target_url.encode()).decode()
content_filter = Filter(
g.session_key,
root_url=request.url_root,
config=g.user_config)
target = urlparse.urlparse(target_url)
# Ensure requested URL has a valid domain
if not validators.domain(target.netloc):
return render_template(
'error.html',
error_message='Invalid location'), 400
host_url = f'{target.scheme}://{target.netloc}'
get_body = g.user_request.send(base_url=target_url).text
results = bsoup(get_body, 'html.parser')
src_attrs = ['src', 'href', 'srcset', 'data-srcset', 'data-src']
# Parse HTML response and replace relative links w/ absolute
for element in results.find_all():
for attr in src_attrs:
if not element.has_attr(attr) or not element[attr].startswith('/'):
continue
element[attr] = host_url + element[attr]
# Replace or remove javascript sources
for script in results.find_all('script', {'src': True}):
if 'nojs' in request.args:
script.decompose()
else:
content_filter.update_element_src(script, 'application/javascript')
# Replace all possible image attributes
img_sources = ['src', 'data-src', 'data-srcset', 'srcset']
for img in results.find_all('img'):
_ = [
content_filter.update_element_src(img, 'image/png', attr=_)
for _ in img_sources if img.has_attr(_)
]
# Replace all stylesheet sources
for link in results.find_all('link', {'href': True}):
content_filter.update_element_src(link, 'text/css', attr='href')
# Use anonymous view for all links on page
for a in results.find_all('a', {'href': True}):
a['href'] = f'{Endpoint.window}?location=' + a['href'] + (
'&nojs=1' if 'nojs' in request.args else '')
# Remove all iframes -- these are commonly used inside of <noscript> tags
# to enforce loading Google Analytics
for iframe in results.find_all('iframe'):
iframe.decompose()
return render_template(
'display.html',
response=results,
translation=app.config['TRANSLATIONS'][
g.user_config.get_localization_lang()
]
)
@app.route('/robots.txt')
def robots():
response = make_response(
'''User-Agent: *
Disallow: /''', 200)
response.mimetype = 'text/plain'
return response
@app.route('/')
def favicon():
return app.send_static_file('img/favicon.ico')
@app.errorhandler(Exception)
def internal_error(e):
query = ''
if request.method == 'POST':
query = request.form.get('q')
else:
query = request.args.get('q')
# Attempt to parse the query
try:
search_util = Search(request, g.user_config, g.session_key)
query = search_util.new_search_query()
except Exception:
pass
print(traceback.format_exc(), file=sys.stderr)
fallback_engine = os.environ.get('WHOOGLE_FALLBACK_ENGINE_URL', '')
if (fallback_engine):
return redirect(fallback_engine + query)
localization_lang = g.user_config.get_localization_lang()
translation = app.config['TRANSLATIONS'][localization_lang]
# Itt kapja meg a hiba részleteit, és ezeket fogja megjeleníteni.
error_detail = str(e) # Konvertálja a hiba objektumot szöveges formátumba
return render_template(
'error.html',
error_message=f'Internal server error (500): {error_detail}', # Ide kerül a pontos hibaüzenet
translation=translation,
farside='https://farside.link',
config=g.user_config,
query=unquote(query),
params=g.user_config.to_params(keys=['preferences'])), 500
def run_app() -> None:
parser = argparse.ArgumentParser(
description='RaveSearch console runner')
parser.add_argument(
'--port',
default=5000,
metavar='<port number>',
help='Specifies a port to run on (default 5000)')
parser.add_argument(
'--host',
default='127.0.0.1',
metavar='<ip address>',
help='Specifies the host address to use (default 127.0.0.1)')
parser.add_argument(
'--unix-socket',
default='',
metavar='</path/to/unix.sock>',
help='Listen for app on unix socket instead of host:port')
parser.add_argument(
'--unix-socket-perms',
default='600',
metavar='<octal permissions>',
help='Octal permissions to use for the Unix domain socket (default 600)')
parser.add_argument(
'--debug',
default=False,
action='store_true',
help='Activates debug mode for the server (default False)')
parser.add_argument(
'--https-only',
default=False,
action='store_true',
help='Enforces HTTPS redirects for all requests')
parser.add_argument(
'--userpass',
default='',
metavar='<username:password>',
help='Sets a username/password basic auth combo (default None)')
parser.add_argument(
'--proxyauth',
default='',
metavar='<username:password>',
help='Sets a username/password for a HTTP/SOCKS proxy (default None)')
parser.add_argument(
'--proxytype',
default='',
metavar='<socks4|socks5|http>',
help='Sets a proxy type for all connections (default None)')
parser.add_argument(
'--proxyloc',
default='',
metavar='<location:port>',
help='Sets a proxy location for all connections (default None)')
args = parser.parse_args()
if args.userpass:
user_pass = args.userpass.split(':')
os.environ['WHOOGLE_USER'] = user_pass[0]
os.environ['WHOOGLE_PASS'] = user_pass[1]
if args.proxytype and args.proxyloc:
if args.proxyauth:
proxy_user_pass = args.proxyauth.split(':')
os.environ['WHOOGLE_PROXY_USER'] = proxy_user_pass[0]
os.environ['WHOOGLE_PROXY_PASS'] = proxy_user_pass[1]
os.environ['WHOOGLE_PROXY_TYPE'] = args.proxytype
os.environ['WHOOGLE_PROXY_LOC'] = args.proxyloc
if args.https_only:
os.environ['HTTPS_ONLY'] = '1'
if args.debug:
app.run(host=args.host, port=args.port, debug=args.debug)
elif args.unix_socket:
waitress.serve(app, unix_socket=args.unix_socket, unix_socket_perms=args.unix_socket_perms)
else:
waitress.serve(
app,
listen="{}:{}".format(args.host, args.port),
url_prefix=os.environ.get('WHOOGLE_URL_PREFIX', ''))

BIN
app/static/.DS_Store vendored Executable file

Binary file not shown.

View file

@ -0,0 +1,14 @@
{
"!i": {
"url": "search?q={}&tbm=isch",
"suggestion": "!i (Whoogle Images)"
},
"!v": {
"url": "search?q={}&tbm=vid",
"suggestion": "!v (Whoogle Videos)"
},
"!n": {
"url": "search?q={}&tbm=nws",
"suggestion": "!n (Whoogle News)"
}
}

2
app/static/build/.gitignore vendored Executable file
View file

@ -0,0 +1,2 @@
*
!.gitignore

864
app/static/css/dark-theme.css Executable file
View file

@ -0,0 +1,864 @@
:root {
/* DARK THEME COLORS */
--primary: #ed0f59;
--primary-active: #bc134b;
--primary-grad2: #e24c41;
--grad2-active: #a53028;
--primary-grad3: #dc7541;
--grad3-active: #a34e24;
--body-bg: #0a0c0d;
--body-color: #e4e8ea;
--ad-bg: #282e31;
--gray-200: #1b1f21;
--gray-300: #23292c;
--gray-400: #282e31;
--gray-800: #9aa2a6;
--gray-900: #50585c;
--gray-950: #31373a;
--danger: #ff3333;
--danger-active: #e52e2e;
--visited: #8e7263;
}
/* :root {
/* DARK THEME COLORS */
/*--primary: #105d5e;
--primary-active: #137071;
--primary-grad2: #009a6e;
--grad2-active: #00af7e;
--primary-grad3: #b3eda9;
--grad3-active: #d0fdc8;
--primary-grad4: #ebfadb;
--ad-bg: #293e33;
--silver-bg: #767f7d;
--silver-light-bg: #c2cbc9;
--active: #e8e300;
--body-bg: #0a0c0d;
--body-color: #e4e8ea;
--body-color: #edf3f5;
--gray-200: #1b1f21;
--gray-300: #23292c;
--gray-400: #282e31;
--gray-800: #9aa2a6;
--gray-900: #50585c;
--gray-950: #31373a;
--danger: #ff3333;
--danger-active: #e52e2e;
} */
html {
background: var(--body-bg) !important;
color: var(--body-color) !important;
}
body {
background:var(--body-bg) !important;
color: var(--body-color) !important;
}
div {
color: var(--body-color) !important;
}
/* label {
color: var(--whoogle-dark-contrast-text) !important;
}
li a {
color: var(--whoogle-dark-result-url) !important;
}
li {
color: var(--whoogle-dark-text) !important;
}
.anon-view {
color: var(--bo) !important;
padding: 30px;
} */
/* textarea {
background: rgb(29, 29, 29) !important;
color: var(--whoogle-dark-text) !important;
} */
/* Látogatott találat címke */
a:visited h3 div {
color: var(--visited) !important;
}
/* Találat címke */
a:link h3 div {
color: var(--primary-grad3) !important;
}
a:link div {
color: var(--body-color) !important;
}
/* Találat címke hover */
a:link div:hover {
color: var(--grad3-active) !important;
text-decoration: none !important;
}
a:link div:visited {
color: var(--visited) !important;
text-decoration: none !important;
}
a:hover {
text-decoration: none !important;
}
/* Wikipedia cím */
.lU7jec h3 {
font-size: 20px;
font-weight: bold;
line-height: 26px;
color: var(--primary-grad3) !important;
margin: 0 0 24px 0 !important;
}
/* wikipedia favicon */
.BNeawe.s3v9rd.AP7Wnd.has-favicon > .BNeawe.has-favicon > img.site-favicon {
display: none !important;
}
/* Helyek favicon */
div > .other-results > .ZINbbc.xpd.Et0od.pkph0e.has-favicon > .has-favicon > img.site-favicon {
display: none !important;
}
.tAd8D.AP7Wnd {
color: var(--body-color) !important;
}
.tAd8D.AP7Wnd {
padding: 0 !important;
}
div span {
color: var(--primary-grad3) !important;
}
input {
background-color: var(--gray-300) !important;
color: var(--body-color) !important;
}
input:focus-visible {
border: 1px solid var(--primary-active) !important;
outline: none;
border-radius: 100px;
}
select {
background: transparent !important;
color: var(--body-color) !important;
}
/* ---------------------------------------- */
.autocomplete {
background-color: var(--gray-300) !important;
}
.autocomplete:hover {
background-image: linear-gradient(90deg, var(--primary), var(--primary));
background-clip: padding-box, border-box;
background-origin: border-box;
border: 1px solid var(--primary-grad3);
border-radius: 100px;
}
.autocomplete:focus {
background-image: linear-gradient(90deg, var(--primary), var(--primary));
background-clip: padding-box, border-box;
background-origin: border-box;
border: 1px solid var(--primary-grad3);
border-radius: 100px;
}
/* Keresőgomb */
#search-submit {
color: var(--body-color) !important;
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-grad2) 60%, var(--primary-grad3) 100%) !important;
border: 1px solid var(--grad3-active) !important;
border-radius: 100px !important;
}
#search-submit:hover {
color: var(--body-color) !important;
background: linear-gradient(200deg, var(--primary) 0%, var(--primary-grad2) 60%, var(--primary-grad3) 100%) !important;
border: 1px solid var(--grad3-active) !important;
border-radius: 100px !important;
}
.kattide {
color: var(--body-color) !important;
background: linear-gradient(45deg, var(--primary) 0%, var(--primary-grad2) 80%, var(--primary-grad3) 100%);
padding-left: 0.8rem;
padding-right: 0.8rem;
-webkit-box-decoration-break: clone;
box-decoration-break: clone;
}
.kattide:hover {
color: white !important;
}
/* Keresőkategóriák megjelenítése */
.selected-header-button,
button.selected-header-button:active,
button.selected-header-button:focus-within {
color: var(--primary-grad3) !important;
border-bottom: 2px solid var(--primary-grad2) !important;
}
.header-button:hover {
color: var(--primary) !important;
border-bottom: 2px solid var(--primary) !important;
}
/* Keresési találat div keret */
#main > div:focus-within {
border-radius: 8px;
box-shadow: 0 0 6px 1px var(--primary-grad3);
}
.ZINbbc {
background-color: #29292987 !important;
overflow: hidden;
margin-bottom: 25px !important;
padding: 15px !important;
border-radius: 8px !important;
}
/* Ezeket keresték még */
.gGQDvd {
padding: 10px 0 0 10px !important;
height: 0 !important;
}
.kjGX2 {
left: 40px !important;
}
.toMBf {
left: 0 !important;
margin: 0 !important;
}
@media (min-width: 601px) {
.zBAuLc {
line-height: normal;
margin: 0;
padding: 0 0 0 0 !important;
}
}
@media (max-width: 600px) {
.s3v9rd {
padding: 8px 0 0 0 !important;
}
}
.j039Wc {
padding-top: 34px !important;
margin-bottom: -14px !important;
}
/* Keresés tartalom */
.s3v9rd {
font-size: 16px;
padding: 10px 0 0 0;
}
.KP7LCb {
box-shadow: 0 0 0 0 !important;
}
.BVG0Nb {
box-shadow: 0 0 0 0 !important;
background-color: var(--whoogle-dark-page-bg) !important;
}
.ZINbbc.luh4tb {
background: var(--whoogle-dark-result-bg) !important;
margin-bottom: 24px !important;
}
.bRsWnc {
background-color: var(--whoogle-dark-result-bg) !important;
}
.x54gtf {
background-color: var(--whoogle-dark-divider) !important;
}
.Q0HXG {
background-color: var(--whoogle-dark-divider) !important;
}
.LKSyXe {
background-color: var(--whoogle-dark-divider) !important;
}
.tAd8D {
padding: 10px !important;
}
.sa1toc {
background: var(--whoogle-dark-page-bg) !important;
}
.qFvlD {
border: 1px solid !important;
}
.xeDNfc {
display: none !important;
}
.info-text {
color: var(--whoogle-dark-contrast-text) !important;
opacity: 75%;
}
.collapsible {
color: var(--whoogle-dark-text) !important;
}
.collapsible:after {
color: var(--whoogle-dark-text) !important;
}
.active {
background: transparent !important;
border: 2px solid grey !important;
}
.content,
.result-config {
background-color: transparent !important;
color: var(--whoogle-contrast-text) !important;
}
.active:after {
color: var(--whoogle-dark-contrast-text) !important;
}
.link {
color: var(--whoogle-dark-contrast-text);
}
.link-color {
color: var(--whoogle-dark-result-url) !important;
}
/* Keresési ajánlások */
.autocomplete-items {
border: 0;
}
.autocomplete-items div {
color: var(--body-color);
background-color: rgb(51, 51, 51);
border: 0;
}
.autocomplete-items div:first-child {
border-top-left-radius: 10px;
border-top-right-radius: 10px;
}
.autocomplete-items div:last-child {
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
}
.autocomplete-items div:hover {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-grad2) 60%, var(--primary-grad3) 100%) !important;
color: var(--body-color) !important;
}
.autocomplete-active {
background: linear-gradient(200deg, var(--primary) 0%, var(--primary-grad2) 60%, var(--primary-grad3) 100%) !important;
color: var(--body-color) !important;
}
.footer {
margin-bottom: 30px;
color: var(--whoogle-dark-text);
}
.header-div {
margin: 0 0 30px 0;
}
.mobile-search-bar {
background-color: var(--whoogle-dark-result-bg) !important;
color: var(--whoogle-dark-text) !important;
}
.search-bar-desktop {
color: var(--whoogle-dark-text) !important;
}
.ip-text-div,
.update_available,
.cb_label,
.cb {
color: var(--whoogle-dark-secondary-text) !important;
}
.cb:focus {
color: var(--whoogle-dark-contrast-text) !important;
}
.desktop-header,
.mobile-header {
background-color: transparent !important;
}
/* Keresés feletti mini url */
.UPmit.AP7Wnd {
color: #d0bc8e !important;
font-size: 0.8em;
}
.lcJF1d {
margin: 25px 18px 18px 0 !important;
}
.egMi0 {
margin: 0 !important;
}
.qN9Ked {
display: table-caption !important;
}
.kCrYT {
padding: 0 !important;
}
.BsXmcf {
position: relative !important;
height: 0 !important;
background: transparent !important;
}
.Q6Xouf {
max-width: 555px;
margin: 0 auto;
}
/* Helyek cím */
.deIvCb {
font-weight: 600 !important;
font-size: 18px !important;
padding: 0;
}
.nGphre > .site-favicon {
display: none !important;
}
/* Képtalálatok */
.results {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin: 20px 0;
}
.result-item {
padding: 10px;
border-radius: 5px;
background-color: #29292987;
width: calc(33.333% - 16px);
box-sizing: border-box;
text-align: center;
word-wrap: break-word;
}
.result-item img {
max-width: 100%;
height: auto;
border-radius: 5px;
}
.result-item p {
margin: 10px 0 0;
font-size: 14px;
}
.img-title {
color: var(--primary-grad3);
text-transform: uppercase;
}
.img-url {
color: var(--body-color);
}
.img-url:hover {
text-decoration: underline;
color: var(--grad2-active);
}
/* Stílusok a javaslatokhoz */
.suggestions {
margin: 20px 0;
padding: 10px;
background-color: #e9e9e9;
border: 1px solid #ccc;
border-radius: 5px;
}
/* Stílusok a lapozáshoz */
.pagination {
display: flex;
justify-content: center;
margin: 20px 0;
}
.pagination a {
margin: 0 5px;
padding: 10px 15px;
border: 1px solid #ddd;
background-color: #fff;
text-decoration: none;
color: #007bff;
border-radius: 5px;
}
.pagination a:hover {
background-color: #007bff;
color: #fff;
}
.e3goi {
padding: 10px;
width: 100%;
height: 160px;
}
.TxbwNb {
width: 100%;
height: 100%;
}
.RAyV4b {
width: 100%;
height: 130px;
display: flex;
justify-content: center;
align-items: center;
}
.t0fcAb {
scale: 1.8;
}
.Tor4Ec {
text-align: center;
width: 100%;
height: 20px;
display: block;
padding: 10px 0;
}
.qXLe6d {
font-size: 16px;
font-weight: bold;
}
.FbhRzb {
border-left: thin solid #dadce0;
border-right: thin solid #dadce0;
border-top: thin solid #dadce0;
height: 40px;
overflow: hidden;
}
.n692Zd {
margin-bottom: 10px;
}
.cvifge {
height: 40px;
border-spacing: 0;
width: 100%;
}
.QvGUP {
height: 40px;
padding: 0 8px 0 8px;
vertical-align: top;
}
.O4cRJf {
height: 40px;
width: 100%;
padding: 0;
padding-right: 16px;
}
.O1ePr {
height: 40px;
padding: 0;
vertical-align: top;
}
.kgJEQe {
height: 36px;
width: 98px;
vertical-align: top;
margin-top: 4px;
}
.lXLRf {
vertical-align: top;
}
.MhzMZd {
border: 0;
vertical-align: middle;
font-size: 14px;
height: 40px;
padding: 0;
width: 100%;
padding-left: 16px;
}
.xB0fq {
height: 40px;
border: none;
font-size: 14px;
background-color: #4285f4;
color: #fff;
padding: 0 16px;
margin: 0;
vertical-align: top;
cursor: pointer;
}
.xB0fq:focus {
border: 1px solid #000;
}
.M7pB2 {
border: thin solid #dadce0;
margin: 0 0 3px 0;
font-size: 13px;
font-weight: 500;
height: 40px;
}
.euZec {
width: 100%;
height: 40px;
text-align: center;
border-spacing: 0;
}
table.euZec td {
padding: 0;
width: 25%;
}
.QIqI7 {
display: inline-block;
padding-top: 4px;
font-weight: bold;
color: #4285f4;
}
.EY24We {
border-bottom: 2px solid #4285f4;
}
.CsQyDc {
display: inline-block;
color: #70757a;
}
.TuS8Ad {
font-size: 14px;
}
.HddGcc {
padding: 8px;
color: #70757a;
}
.dzp8ae {
font-weight: bold;
color: #3c4043;
}
.rEM8G {
color: #70757a;
}
.bookcf {
table-layout: fixed;
width: 100%;
border-spacing: 0;
}
.InWNIe {
text-align: center;
}
.uZgmoc {
width: 350px;
display: flex;
border: none;
color: #70757a;
font-size: 14px;
text-align: center;
/* background-color: #29292987; */
border-radius: 8px;
margin: 35px auto;
padding: 10px;
justify-content: center;
}
.frGj1b {
margin: 10px;
font-size: 16px;
border: 1px solid;
border-radius: 10px;
padding: 10px 15px;
}
.BnJWBc {
text-align: center;
padding: 6px 0 13px 0;
height: 35px;
}
.X6ZCif {
color: #202124;
font-size: 11px;
line-height: 16px;
display: inline-block;
padding-top: 2px;
overflow: hidden;
padding-bottom: 4px;
width: 100%;
}
.TwVfHd {
border-radius: 16px;
border: thin solid #dadce0;
display: inline-block;
padding: 8px 8px;
margin-right: 8px;
margin-bottom: 4px;
}
.yekiAe {
background-color: #dadce0;
}
.svla5d {
width: 100%;
height: 100%;
background-color: #29292987;
border-radius: 10px;
margin: 10px 0;
display: grid;
margin: 0 auto;
padding: 10px 0;
}
.ezO2md {
border: thin solid #dadce0;
padding: 12px 16px 12px 16px;
margin-bottom: 10px;
}
.K35ahc {
width: 100%;
}
.owohpf {
text-align: center;
}
.fYyStc {
word-break: break-word;
text-align: center;
font-size: 16px;
padding: 0 0 10px 0;
}
.img_button {
padding: 10px;
margin: 0 auto 10px;
border: 1px solid #84dbff;
border-radius: 6px;
color: #3c4043;
background-color: #2e97c3;
}
.ynsChf {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.Fj3V3b {
color: #1967d2;
font-size: 14px;
line-height: 20px;
}
.FrIlee {
color: #202124;
font-size: 11px;
line-height: 16px;
}
.F9iS2e {
color: #70757a;
font-size: 11px;
line-height: 16px;
}
.WMQ2Le {
color: #70757a;
font-size: 12px;
line-height: 16px;
}
.x3G5ab {
color: #202124;
font-size: 12px;
line-height: 16px;
}
.fuLhoc {
color: #1967d2;
font-size: 18px;
line-height: 24px;
}
.epoveb {
font-size: 32px;
line-height: 40px;
font-weight: 400;
color: #202124;
}
.dXDvrc {
color: #0d652d;
font-size: 14px;
line-height: 20px;
word-wrap: break-word;
}
.dloBPe {
font-weight: bold;
}
.YVIcad {
color: #70757a;
}
.JkVVdd {
color: #ea4335;
}
.oXZRFd {
color: #ea4335;
}
.MQHtg {
color: #fbbc04;
}
.pyMRrb {
color: #1e8e3e;
}
.EtTZid {
color: #1e8e3e;
}
.M3vVJe {
color: #1967d2;
}
.NHQNef {
font-style: italic;
}
.Cb8Z7c {
white-space: pre;
}
a.ZWRArf {
text-decoration: none;
}
a .CVA68e:hover {
text-decoration: underline;
}

9
app/static/css/error.css Executable file
View file

@ -0,0 +1,9 @@
html {
font-size: 1.3rem;
}
@media (max-width: 1000px) {
html {
font-size: 3rem;
}
}

284
app/static/css/header.css Executable file
View file

@ -0,0 +1,284 @@
html {
font-family: "Ubuntu", sans-serif !important;
overflow-x: hidden;
}
header {
font-size: 14px;
line-height: 20px;
color: #3c4043;
word-wrap: break-word;
width: 100%;
display: grid;
justify-content: center;
align-content: center;
align-items: center;
}
.header-search {
max-width: 40rem;
margin: auto;
}
.mobile-logo {
width: 100% !important;
padding: 0 !important;
margin: 5px 0 !important;
}
.logo-link,
.logo-letter {
text-decoration: none !important;
letter-spacing: -1px;
text-align: center;
border-radius: 2px 0 0 0;
}
.result-config {
margin-bottom: 10px;
border-radius: 8px;
}
.mobile-logo {
font: 22px/36px Futura, Arial, sans-serif;
padding-left: 5px;
/* display: flex; */
width: 10rem;
align-items: center;
}
.logo-div {
margin-top: 20px;
letter-spacing: -1px;
text-align: center;
font: 22pt Futura, Arial, sans-serif;
padding: 10px 0 5px 0;
height: 37px;
font-smoothing: antialiased;
}
.search-bar-desktop {
border-radius: 8px 8px 0 0;
height: 40px !important;
}
.search-div {
margin-top: 30px;
width: 100%;
}
.search-form {
display: contents;
width: 100%;
margin: 0px;
}
.search-input {
background: none;
margin: 2px 4px 2px 8px;
display: block;
font-size: 16px;
padding: 0 0 0 8px;
flex: 1;
height: 35px;
outline: none;
border: none;
width: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
overflow: hidden;
}
.tracking-link {
font-size: large;
text-align: center;
margin: 15px;
display: block;
}
#mobile-header-logo {
height: 1.75em;
}
.mobile-input-div {
width: 100%;
}
.mobile-search-bar {
display: block;
font-size: 16px;
padding: 0 0 0 20px;
margin-right: -25px;
-webkit-box-flex: 1;
outline: none;
width: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
overflow: hidden;
border: 0px !important;
}
.autocomplete-mobile {
display: -webkit-box;
width: 100%;
}
.desktop-header-logo {
height: 1.65em;
}
.header-autocomplete {
width: 100%;
flex: 1;
}
a {
color: var(--primary-grad2) !important;
text-decoration: none;
tap-highlight-color: rgba(0, 0, 0, 0.1);
}
a:hover {
color: var(--grad2-active) !important;
text-decoration: none;
text-decoration-line: none;
tap-highlight-color: rgba(0, 0, 0, 0.1);
}
.header-tab-div {
overflow: auto;
margin: 30px 0 0 0;
}
.desktop-header {
height: 39px;
display: box;
display: flex;
width: 100%;
}
.header-tab {
box-pack: justify;
font-size: 14px;
line-height: 37px;
justify-content: space-evenly;
}
.desktop-header a,
.desktop-header span {
color: #70757a;
display: block;
flex: none;
padding: 0 16px;
text-align: center;
text-transform: uppercase;
}
span.header-tab-span {
color: #4285f4;
font-weight: bold;
background-color: #1d1d1d;
}
.mobile-header {
height: 65px;
display: table;
margin: 0 auto;
width: 100%;
}
.mobile-header a,
.mobile-header span {
color: #70757a;
text-decoration: none;
display: table-cell;
word-spacing: 30px;
height: auto;
/* padding: 8px 12px 8px 12px; */
}
@media (max-width: 600px) {
.mobile-header a,
.mobile-header span {
padding: 0 22px;
}
}
span.mobile-tab-span {
border-bottom: 2px solid #202124;
color: #202124;
height: 26px;
/* margin: 0 12px; */
/* padding: 0; */
}
.desktop-header input {
margin: 2px 4px 2px 8px;
}
a.header-tab-a:visited {
color: #70757a;
}
.header-tab-div-end {
border-left: none;
}
.adv-search {
font-size: 30px;
margin: 0 0 8px 0;
vertical-align: sub;
}
.adv-search:hover {
cursor: pointer;
}
#adv-search-toggle {
display: none;
}
.result-collapsible {
max-height: 0px;
overflow: hidden;
transition: max-height 0.25s linear;
}
.search-bar-input {
display: block;
font-size: 16px;
padding: 0 0 0 8px;
flex: 1;
height: 35px;
outline: none;
border: none;
width: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
overflow: hidden;
}
#result-country {
max-width: 200px;
}
@media (max-width: 801px) {
.header-tab-div {
margin-bottom: 10px !important;
}
.header-tab-div-2 {
margin: 0;
}
}
@media (min-width: 802px) {
.header-tab-div {
margin-bottom: 10px !important;
}
.header-tab-div-2 {
margin: 50px 20px 0 20px;
}
}
@media (max-width: 600px) {
.header-tab {
font-size: 12px;
}
}

48
app/static/css/input.css Executable file
View file

@ -0,0 +1,48 @@
#search-bar {
height: 48px !important;
background: transparent !important;
}
#search-reset {
all: unset;
margin-left: -40px;
text-align: center;
background-color: transparent !important;
cursor: pointer;
height: 45px;
width: 65px;
}
@media (max-width: 300px) {
#search-reset {
display: none;
}
}
.ZINbbc.xpd.O9g5cc.uUPGi input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.cb {
width: 40%;
overflow: hidden;
text-align: left;
line-height: 28px;
background: transparent;
border-radius: 6px;
border: 1px solid #5f6368;
font-size: 14px !important;
height: 36px;
padding: 0 0 0 12px;
margin: 10px 10px 10px 0;
}
.conversion_box {
margin-top: 15px;
}
.ZINbbc.xpd.O9g5cc.uUPGi input:focus-visible {
outline: 0;
}

205
app/static/css/light-theme.css Executable file
View file

@ -0,0 +1,205 @@
html {
background: var(--whoogle-page-bg) !important;
}
body {
background: var(--whoogle-page-bg) !important;
}
div {
color: var(--whoogle-text) !important;
}
label {
color: var(--whoogle-contrast-text) !important;
}
li a {
color: var(--whoogle-result-url) !important;
}
li {
color: var(--whoogle-text) !important;
}
.anon-view {
color: var(--whoogle-text) !important;
text-decoration: underline;
}
textarea {
background: var(--whoogle-page-bg) !important;
color: var(--whoogle-text) !important;
}
select {
background: var(--whoogle-page-bg) !important;
color: var(--whoogle-text) !important;
}
.ZINbbc {
overflow: hidden;
background-color: var(--whoogle-result-bg) !important;
margin-bottom: 10px !important;
border-radius: 8px !important;
box-shadow: 0 1px 6px rgba(32,33,36,0.28) !important;
}
.BVG0Nb {
background-color: var(--whoogle-result-bg) !important;
}
.ZINbbc.luh4tb {
background: var(--whoogle-result-bg) !important;
margin-bottom: 24px !important;
}
.bRsWnc {
background-color: var(--whoogle-result-bg) !important;
}
.x54gtf {
background-color: var(--whoogle-divider) !important;
}
.Q0HXG {
background-color: var(--whoogle-divider) !important;
}
.LKSyXe {
background-color: var(--whoogle-divider) !important;
}
a:visited h3 div {
color: var(--whoogle-result-visited) !important;
}
a:link h3 div {
color: var(--whoogle-result-title) !important;
}
a:link div {
color: var(--whoogle-result-url) !important;
}
div span {
color: var(--whoogle-secondary-text) !important;
}
input {
background-color: var(--whoogle-page-bg) !important;
color: var(--whoogle-text) !important;
}
#search-bar {
color: var(--whoogle-text) !important;
background-color: var(--whoogle-page-bg);
}
.home-search {
border-color: var(--whoogle-element-bg) !important;
}
.search-container {
background-color: var(--whoogle-page-bg) !important;
}
#search-submit {
border: 1px solid var(--whoogle-element-bg) !important;
background: var(--whoogle-element-bg) !important;
color: var(--whoogle-contrast-text) !important;
}
.info-text {
color: var(--whoogle-contrast-text) !important;
opacity: 75%;
}
.collapsible {
color: var(--whoogle-text) !important;
}
.collapsible:after {
color: var(--whoogle-text);
}
.active {
background-color: var(--whoogle-element-bg) !important;
color: var(--whoogle-contrast-text) !important;
}
.content, .result-config {
background-color: var(--whoogle-element-bg) !important;
color: var(--whoogle-contrast-text) !important;
}
.active:after {
color: var(--whoogle-contrast-text);
}
.link {
color: var(--whoogle-element-bg);
}
.link-color {
color: var(--whoogle-result-url) !important;
}
.autocomplete-items {
border: 1px solid var(--whoogle-element-bg);
}
.autocomplete-items div {
background-color: var(--whoogle-page-bg);
border-bottom: 1px solid var(--whoogle-element-bg);
}
.autocomplete-items div:hover {
background-color: var(--whoogle-element-bg);
color: var(--whoogle-contrast-text) !important;
}
.autocomplete-active {
background-color: var(--whoogle-element-bg) !important;
color: var(--whoogle-contrast-text) !important;
}
.footer {
color: var(--whoogle-text);
}
path {
fill: var(--whoogle-logo);
}
.header-div {
background-color: var(--whoogle-result-bg) !important;
}
#search-reset {
color: var(--whoogle-text) !important;
}
.mobile-search-bar {
background-color: var(--whoogle-result-bg) !important;
color: var(--whoogle-text) !important;
}
.search-bar-desktop {
background-color: var(--whoogle-result-bg) !important;
color: var(--whoogle-text);
border-bottom: 0px;
}
.ip-text-div, .update_available, .cb_label, .cb {
color: var(--whoogle-secondary-text) !important;
}
.cb:focus {
color: var(--whoogle-text) !important;
}
.desktop-header, .mobile-header {
background-color: var(--whoogle-result-bg) !important;
}

30
app/static/css/logo.css Executable file
View file

@ -0,0 +1,30 @@
.cls-1 {
fill: transparent;
}
svg {
height: inherit;
}
a {
height: inherit;
}
@media (max-width: 1000px) {
svg {
margin-top: 0.3em;
height: 70%;
}
}
.rplogo {
max-width: 40rem;
display: block;
margin: 30px auto;
position: relative;
border-radius: inherit;
top: 0;
right: 0;
bottom: 0;
left: 0;
}

380
app/static/css/main.css Executable file
View file

@ -0,0 +1,380 @@
@font-face {
font-family: "Ubuntu";
src: url("fonts/ubuntu.woff2") format("woff");
font-weight: normal;
font-style: normal;
}
body {
font-family: "Ubuntu", sans-serif !important;
max-width: 900px !important;
min-width: 0 !important;
display: flex;
flex-direction: column;
margin: 25px auto !important;
font-size: 1rem;
font-weight: 400;
color: white;
}
body,
main {
margin: 0;
padding: 15px;
}
main {
width: 100%;
margin-bottom: 2rem;
flex: 1;
}
footer {
clear: both;
min-height: 4rem;
width: 100%;
text-align: center;
display: block;
overflow: hidden;
margin-top: 45px;
text-wrap: balance;
}
.logo-container {
background: no-repeat;
min-height: 4rem;
background-position: center;
background-size: contain;
}
.home-search {
background: transparent !important;
}
::placeholder {
font-size: 14px;
}
.social-container {
display: flex;
flex-direction: inherit;
justify-content: center;
}
.social-column {
display: grid;
grid-template-rows: repeat(2, 1fr);
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
grid-auto-flow: dense;
margin-top: 50px;
}
@media (max-width: 500px){
.social-column {
grid-template-columns: repeat(1, 1fr);
}
}
.rpkick {
grid-row-end: span 2;
grid-column-end: span 1;
height: 50px;
width: 200px;
padding: 0 10px;
border: 1px solid rgba(255, 255, 255, 0.04);
border-radius: 10px;
transition: background-color 0.3s ease-out 0.3ms;
}
.rpkick:hover {
background-color: rgba(83, 250, 22, 0.08);
}
.rptwitch {
grid-row-end: span 2;
grid-column-end: span 1;
height: 50px;
width: 200px;
padding: 0 10px;
border: 1px solid rgba(255, 255, 255, 0.04);
border-radius: 10px;
transition: background-color 0.3s ease-out 0.3ms;
}
.rptwitch:hover {
background-color: rgba(101, 66, 166, 0.14);
}
.rpdiscord {
grid-row-end: span 2;
grid-column-end: span 1;
height: 50px;
width: 200px;
padding: 0 10px;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px;
transition: background-color 0.3s ease-out 0.3ms;
}
.rpdiscord:hover {
background-color: rgba(98, 102, 243, 0.08);
}
.rpgit {
grid-row-end: span 2;
grid-column-end: span 1;
height: 50px;
width: 200px;
padding: 0 10px;
border: 1px solid rgba(255, 255, 255, 0.04);
border-radius: 10px;
transition: background-color 0.3s ease-out 0.3ms;
}
.rpgit:hover {
background-color: rgba(204, 204, 204, 0.04);
}
.rpyoutube {
grid-row-end: span 2;
grid-column-end: span 1;
height: 50px;
width: 200px;
padding: 0 10px;
border: 1px solid rgba(255, 255, 255, 0.04);
border-radius: 10px;
transition: background-color 0.3s ease-out 0.3ms;
}
.rpyoutube:hover {
background-color: rgba(129, 0, 0, 0.054);
}
.rpweb {
grid-row-end: span 2;
grid-column-end: span 1;
height: 50px;
width: 200px;
padding: 0 10px;
border: 1px solid rgba(255, 255, 255, 0.04);
border-radius: 10px;
transition: background-color 0.3s ease-out 0.3ms;
}
.rpweb:hover {
background-color: rgba(251, 255, 33, 0.049);
}
.search-container {
margin-top: 26vh;
width: 100%;
margin-bottom: 2rem;
flex: 1;
padding: 0;
margin: 0;
text-align: center;
}
#search-form,
#search-fields {
max-width: 650px;
margin: 0 auto;
background: inherit;
border: inherit;
padding: 0;
display: block;
}
.search-items {
width: 100%;
position: relative;
display: flex;
}
#search-bar {
width: 95%;
margin: 0 auto;
height: 3rem;
display: inline-flex;
flex-direction: row;
white-space: nowrap;
font-size: 24px;
border: none !important;
border-radius: 100px;
padding: 0 0 0 15px;
}
#search-submit {
display: inline-flex;
flex-direction: row;
white-space: nowrap;
margin: 0.5rem auto;
cursor: pointer;
width: 100%;
height: 3rem;
font-size: 18px;
justify-content: center;
}
.config-options {
max-height: 370px;
overflow-y: scroll;
}
.config-buttons {
max-height: 30px;
}
.config-div {
margin-top: 10px;
padding: 15px;
overflow: hidden;
}
button::-moz-focus-inner {
border: 0;
}
.collapsible {
background-color: rgba(0, 0, 0, 0);
cursor: pointer;
padding: 10px 35px 10px 35px;
width: 100%;
max-width: 686px;
border: none;
text-align: left;
outline: none;
font-size: 15px;
border-radius: 10px 10px 0 0;
margin: 1.5rem 0 0 0;
}
.collapsible:after {
content: "\002B";
font-weight: bold;
float: right;
margin-left: 5px;
}
.active:after {
content: "\2212";
}
.content {
max-width: 646px;
margin: 0 auto;
padding: 0 18px;
max-height: 0;
overflow: hidden;
transition: max-height 0.2s ease-out;
border-radius: 0 0 10px 10px;
border-left: 2px solid grey;
border-right: 2px solid grey;
}
.open {
padding-top: 20px;
padding-bottom: 60px;
border-bottom: 2px solid grey;
background-color: #1d1d1d !important;
}
.hidden {
display: none;
}
.info-text {
font-style: italic;
font-size: 12px;
}
#config-style {
resize: none;
width: 100%;
height: 100px;
}
.whoogle-logo {
display: none;
}
.whoogle-svg {
width: 80%;
height: initial;
display: block;
margin: auto;
padding-bottom: 10px;
}
.autocomplete {
width: 100%;
margin: 0 auto;
color: #ffffff !important;
border: 1px solid #ffffff29;
border-radius: 100px;
box-shadow: 0 2px 16px 0 rgba(5, 5, 6, 15%), 0 4px 8px 0 rgba(5, 5, 6, 35%);
}
/* .autocomplete:hover {
border: 1px solid #2e97c3;
box-shadow: 0 1px 4px 0 #2e97c3, 0 1px 2px 0 #2e97c3;
}
*/
.autocomplete-items {
width: 650px;
position: absolute;
z-index: 99;
border: 0;
border-radius: 4px;
background-color: #1d1d1d;
overflow: hidden;
transition: opacity 0s ease 0s, visibility 0s ease 0s, all 0.5s ease 0s;
opacity: 1;
visibility: visible;
animation-delay: 0.1s;
text-align: left;
margin-top: 2px;
}
.autocomplete-items div {
padding: 10px;
cursor: pointer;
}
details summary {
padding: 10px;
font-weight: bold;
}
/* Mobile styles */
.BNeawe.UPmit.AP7Wnd.lRVwie {
font-variant: all-small-caps;
text-wrap: balance;
}
.w1C3Le,
.BmP5tf,
.G5NbBd,
.CS4w5b {
padding: 0 !important;
}
.lcJF1d {
margin-left: 30px !important;
}
.egMi0 {
margin: 0 !important;
}
.footer-bottom {
margin-bottom: 30px;
}
.lRVwie {
text-wrap: balance !important;
}

185
app/static/css/search.css Executable file
View file

@ -0,0 +1,185 @@
body {
max-width: 900px !important;
min-width: 0 !important;
display: flex;
flex-direction: column;
margin: 25px auto !important;
color: white;
padding: 0 8px !important;
}
.vvjwJb {
font-size: 20px !important;
}
.autocomplete {
position: relative;
display: -webkit-box;
width: 100%;
margin: 0 auto;
color: #ffffff !important;
border: 1px solid #ffffff29;
border-radius: 100px;
box-shadow: 0 2px 16px 0 rgba(5, 5, 6, 15%), 0 4px 8px 0 rgba(5, 5, 6, 35%);
background: #1d1d1d !important;
}
.autocomplete-items {
position: absolute;
border-bottom: none;
border-top: none;
z-index: 99;
/*position the autocomplete items to be the same width as the container:*/
top: 100%;
left: 0;
right: 0;
}
.autocomplete-items div {
padding: 10px;
cursor: pointer;
}
details summary {
margin-bottom: 20px;
font-weight: bold;
padding-left: 10px;
}
details summary span {
font-weight: normal;
}
#lingva-iframe {
width: 100%;
height: 650px;
border: 0;
}
.ip-address-div {
padding-bottom: 0 !important;
}
.ip-text-div {
padding-top: 0 !important;
}
.footer {
text-align: center;
}
.site-favicon {
float: left;
width: 18px;
padding: 0 5px 0 0;
}
@media (max-width: 600px) {
.site-favicon {
display: none;
}
}
@media (min-width: 601px) {
.has-favicon .sCuL3 {
padding: 0 0 0 30px !important;
}
}
#flex_text_audio_icon_chunk {
display: none;
}
.rp-credit {
padding: 10px 0 0 0;
margin: 0 auto;
display: table;
}
audio {
display: block;
margin-right: auto;
padding-bottom: 5px;
}
@media (min-width: 801px) {
body {
max-width: 900px !important;
min-width: 0 !important;
}
}
@media (max-width: 801px) {
details summary {
margin-bottom: 10px !important;
}
}
.header-button {
background-color: inherit;
color: #fff;
cursor: pointer;
padding: 10px 35px;
display: table-cell;
align-items: center;
text-transform: uppercase;
border: none;
border-bottom-width: medium;
border-bottom-style: none;
border-bottom-color: currentcolor;
border-bottom: 2px solid transparent;
}
.sCuL3 {
top: -3px !important;
}
.l97dzf {
font-weight: 600 !important;
padding: 0 !important;
}
.tAd8D.AP7Wnd {
padding: 20px 0 20px 40px !important;
}
.skVgpb {
display: ruby !important;
padding: 10px 0 0 40px !important;
}
.VGHMXd {
display: contents !important;
}
.vbShOe {
padding: 10px 0 0 10px !important;
}
/* .BNeawe.uEec3.AP7Wnd {
display: none !important;
} */
.RWuggc.kCrYT > div > .BNeawe.tAd8D.AP7Wnd {
padding: 20px 0 20px 15px !important;
}
.BVG0Nb {
border: none !important;
}
.Xdlr0d {
margin: 0 !important;
padding: 0 !important;
}
* > span > .BNeawe.uEec3.AP7Wnd {
display: none !important;
}
.AVsepf {
padding: 0 0 10px 10px !important;
}

81
app/static/css/variables.css Executable file
View file

@ -0,0 +1,81 @@
/* Colors */
:root {
/* LIGHT THEME COLORS */
--whoogle-logo: #685e79;
--whoogle-page-bg: #ffffff;
--whoogle-element-bg: #4285f4;
--whoogle-text: #000000;
--whoogle-contrast-text: #ffffff;
--whoogle-secondary-text: #70757a;
--whoogle-result-bg: #ffffff;
--whoogle-result-title: #1967d2;
--whoogle-result-url: #0d652d;
--whoogle-result-visited: #4b11a8;
/* DARK THEME COLORS */
/* --primary: #ff1a66;
--primary-active: #e5175c;
--primary-grad2: #ff5448;
--grad2-active: #e54b41;
--primary-grad3: #ff8448;
--grad3-active: #e57741; */
--body-bg: #0a0c0d;
--body-color: #e4e8ea;
--ad-bg: #282e31;
--gray-200: #1b1f21;
--gray-300: #23292c;
--gray-400: #282e31;
--gray-800: #9aa2a6;
--gray-900: #50585c;
--gray-950: #31373a;
--danger: #ff3333;
--danger-active: #e52e2e;
--aa: #D1D1D1;
--ab: #DBDBDB;
--bb: #85C7F2;
--ba: #636363;
--bc: #4C4C4C;
--qw: #F3E8EE;
--primary-grad2: #BACDB0;
--primary: #729B79;
--primary-grad3: #475B63;
--ew: #2E2C2F;
--re: #2D3142;
--er: #BFC0C0;
--rr: #FFFFFF;
--ee: #EF8354;
--rw: #4F5D75;
}
#whoogle-w {
fill: #4285f4;
}
#whoogle-h {
fill: #ea4335;
}
#whoogle-o-1 {
fill: #fbbc05;
}
#whoogle-o-2 {
fill: #4285f4;
}
#whoogle-g {
fill: #34a853;
}
#whoogle-l {
fill: #ea4335;
}
#whoogle-e {
fill: #fbbc05;
}

BIN
app/static/img/.DS_Store vendored Executable file

Binary file not shown.

BIN
app/static/img/favicon.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
app/static/img/favicon/.DS_Store vendored Executable file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 833 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,44 @@
{
"name": "RaveSearch",
"short_name": "RaveSearch",
"display": "fullscreen",
"scope": "/",
"icons": [
{
"src": "favicon-32x32.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "favicon-32x32.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "favicon-32x32.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "android-chrome-192x192.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "android-chrome-192x192.png",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "android-chrome-192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -0,0 +1,523 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="352.000000pt" height="352.000000pt" viewBox="0 0 352.000000 352.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,352.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1300 3362 c0 -104 30 -223 80 -322 25 -48 110 -180 117 -180 3 0 2
15 -1 33 -5 24 -4 27 3 12 5 -11 10 -30 10 -41 1 -12 25 -57 54 -101 92 -139
155 -295 160 -400 2 -40 1 -73 -3 -73 -3 0 -14 23 -24 51 -10 28 -25 53 -32
56 -7 3 -27 29 -44 59 -16 30 -34 54 -39 54 -5 0 -30 34 -56 75 -25 41 -51 77
-56 81 -6 3 2 -12 17 -35 15 -23 25 -43 23 -46 -3 -2 -41 29 -85 70 -85 78
-153 173 -259 365 -60 109 -82 140 -97 140 -17 0 -7 -37 22 -83 38 -61 38 -67
-1 -22 -17 19 -34 33 -39 30 -18 -11 -32 -94 -22 -124 10 -27 9 -30 -8 -28
-28 4 -49 26 -81 83 -16 28 -57 75 -92 105 -34 29 -49 39 -34 22 17 -19 25
-36 21 -46 -4 -12 25 -48 107 -130 119 -118 213 -226 205 -235 -10 -10 -181
135 -210 178 -17 24 -32 38 -34 32 -2 -7 -40 7 -102 38 -60 30 -106 47 -117
44 -15 -5 -15 -4 -4 4 12 9 9 12 -15 18 -16 4 -31 10 -34 14 -6 9 -110 62
-110 56 0 -3 90 -77 200 -166 110 -88 200 -163 200 -165 0 -2 -17 -2 -37 1
-27 3 -14 -4 42 -24 101 -36 140 -56 192 -99 50 -40 152 -168 113 -141 -161
112 -187 120 -43 14 100 -73 124 -96 103 -96 -22 0 -170 81 -265 144 -54 36
-100 65 -102 63 -1 -2 11 -16 27 -32 17 -16 30 -34 30 -41 0 -7 14 -23 32 -35
30 -22 31 -23 10 -32 -31 -14 -141 16 -232 64 -77 40 -220 151 -220 170 0 7
17 9 53 4 67 -9 59 -1 -20 19 -34 9 -112 36 -174 61 -61 25 -113 44 -115 43
-5 -6 67 -113 75 -113 5 0 13 -4 18 -9 5 -5 3 -6 -4 -2 -17 10 -16 0 3 -29 15
-24 15 -25 -2 -26 -34 -1 16 -17 55 -18 19 -1 68 -17 108 -37 71 -35 90 -50
80 -60 -6 -6 -122 32 -162 54 -16 9 -34 17 -40 17 -5 1 -32 10 -60 20 -44 17
-47 20 -25 25 20 5 16 8 -23 14 -27 4 -51 5 -54 3 -2 -3 22 -33 54 -67 l59
-63 88 -2 c86 -3 207 -32 251 -62 11 -7 27 -13 37 -13 10 0 18 -4 18 -8 0 -5
-26 -10 -57 -11 -32 -1 -74 -6 -93 -11 -19 -5 -55 -11 -80 -14 -92 -12 -166
-24 -209 -36 -24 -6 -62 -13 -85 -15 -38 -3 -66 -10 -91 -23 -26 -14 17 25 47
42 28 17 31 17 19 3 -7 -9 -9 -17 -3 -17 5 0 15 7 22 15 11 13 93 55 108 55 3
0 -3 -10 -14 -21 -16 -18 -12 -17 20 5 21 14 47 26 57 26 39 0 25 17 -24 30
-29 7 -57 17 -64 22 -8 6 -18 5 -30 -2 -17 -11 -17 -11 0 -6 11 3 24 3 30 -1
7 -5 28 -14 47 -20 34 -12 33 -12 -23 -8 -32 2 -65 9 -72 15 -8 7 -19 9 -25 5
-20 -12 -9 -23 33 -30 38 -8 39 -9 13 -13 -38 -5 -78 6 -70 19 3 6 -9 3 -27
-7 -36 -18 -155 -132 -147 -140 3 -2 81 10 174 27 155 29 180 31 303 26 74 -4
138 -10 142 -14 5 -5 -70 -29 -167 -53 l-175 -45 53 -9 53 -9 -55 -13 c-30 -7
-66 -13 -80 -14 -42 -1 -1 -20 43 -20 73 0 27 -18 -65 -25 -137 -11 -146 -20
-37 -34 97 -13 125 -19 118 -25 -2 -2 -45 -32 -95 -67 l-90 -64 113 -1 c71 -1
118 -5 126 -13 8 -8 7 -11 -6 -11 -25 0 -206 -98 -267 -145 -50 -39 -89 -75
-79 -75 21 1 179 39 165 39 -31 1 -13 10 38 20 26 5 49 5 52 0 3 -5 13 -5 24
1 24 13 33 13 25 0 -13 -21 49 -8 128 27 76 34 130 66 82 49 -40 -15 -142 -34
-205 -39 l-65 -6 144 71 c188 93 305 117 297 62 -1 -9 -9 -20 -17 -25 -10 -6
-6 -9 15 -9 23 0 27 -3 23 -20 -4 -15 0 -20 14 -20 24 0 24 -9 2 -25 -15 -11
-10 -12 30 -8 38 5 53 1 81 -18 26 -18 32 -27 24 -36 -8 -10 -4 -13 20 -13 40
0 61 -25 33 -40 -12 -6 -21 -16 -21 -22 0 -9 3 -9 12 0 15 15 70 16 85 1 15
-15 -12 -59 -36 -59 -9 0 -23 -5 -31 -11 -11 -7 -6 -9 20 -4 19 4 45 18 58 32
15 16 37 27 59 30 20 2 43 7 52 12 20 10 43 3 35 -11 -3 -5 13 4 37 22 32 23
51 30 70 26 18 -3 32 1 44 14 9 10 30 23 46 29 l29 10 -16 -25 c-9 -13 -15
-27 -12 -29 2 -3 17 16 33 40 28 46 42 58 31 29 -5 -14 -4 -15 9 -4 18 15 21
-1 5 -31 -8 -13 -7 -19 0 -19 6 0 7 -6 1 -17 -4 -10 -25 -67 -45 -128 l-36
-110 5 70 5 69 -39 -66 c-68 -117 -115 -168 -105 -115 3 16 0 14 -10 -8 -8
-16 -45 -60 -83 -96 -37 -37 -78 -80 -91 -95 l-23 -29 22 34 c13 20 18 36 12
40 -6 3 -8 29 -6 56 2 28 4 57 5 65 0 8 11 26 23 40 12 14 30 48 39 77 10 28
31 71 48 95 l31 43 -47 -45 c-25 -24 -46 -40 -46 -36 0 4 -27 -17 -61 -48 -57
-53 -172 -136 -187 -136 -4 0 -23 -13 -42 -28 -45 -37 -80 -56 -80 -43 0 5 5
13 11 17 6 4 25 36 41 73 17 36 37 74 46 84 8 9 12 17 8 17 -4 0 2 17 13 38
23 43 52 112 46 112 -2 0 -34 -39 -72 -86 -43 -54 -95 -106 -142 -140 -73 -54
-91 -62 -91 -45 0 6 -23 0 -51 -11 -34 -15 -46 -24 -37 -29 10 -7 9 -9 -4 -9
-10 0 -32 -14 -50 -31 -20 -18 -25 -26 -13 -21 11 5 40 13 65 16 l45 7 -35
-30 c-40 -35 -31 -40 18 -11 l34 21 -7 -22 c-6 -17 6 -11 51 28 33 28 69 56
82 62 43 22 14 -40 -42 -91 -33 -31 -45 -47 -34 -48 9 0 29 10 45 23 51 41 82
57 113 57 34 0 35 -2 18 -33 -6 -12 21 8 61 46 70 65 91 79 91 56 0 -6 16 1
35 15 37 28 37 28 49 -25 12 -55 -75 -174 -189 -258 -27 -21 -83 -57 -122 -80
-77 -45 -90 -57 -47 -41 14 6 27 10 29 10 2 0 -9 -30 -25 -67 -24 -54 -26 -62
-9 -40 19 25 58 40 44 17 -3 -6 -2 -10 3 -10 13 0 -33 -92 -57 -114 -12 -11
-21 -25 -21 -32 0 -7 -5 -16 -10 -19 -7 -4 -9 2 -4 19 12 50 -14 25 -43 -41
-31 -69 -76 -203 -70 -209 4 -4 110 122 150 177 16 21 33 39 39 39 6 0 3 -10
-8 -21 -11 -13 3 -6 32 14 l51 37 6 -98 c8 -119 35 -220 89 -330 45 -89 68
-118 68 -86 1 24 26 229 39 314 14 86 14 110 1 90 -7 -10 -10 21 -9 97 1 103
2 113 24 133 l24 23 -5 -54 c-3 -30 -7 -74 -9 -99 -4 -41 -3 -42 4 -10 13 51
61 183 72 194 15 16 10 -78 -6 -117 -24 -58 -17 -56 18 5 18 31 41 66 50 76 9
10 17 23 17 28 0 5 24 35 53 68 l54 59 7 -34 c8 -42 0 -133 -21 -221 -17 -78
-12 -109 10 -58 l15 35 10 -25 c29 -73 108 -228 166 -323 36 -60 66 -111 66
-113 0 -3 -6 -2 -12 2 -8 4 -10 3 -5 -2 5 -5 14 -9 20 -9 7 0 26 -25 42 -56
41 -76 129 -218 135 -218 6 0 94 142 135 218 17 31 35 56 42 56 6 0 15 4 20 9
5 5 3 6 -4 2 -7 -4 -13 -5 -13 -2 0 2 30 53 66 113 58 95 137 250 166 323 l10
25 15 -35 c22 -51 27 -20 10 58 -21 88 -29 179 -21 221 l7 34 54 -59 c29 -33
53 -63 53 -68 0 -5 8 -18 17 -28 9 -10 32 -45 50 -76 35 -61 42 -63 18 -5 -16
39 -21 133 -6 117 11 -11 59 -143 72 -194 7 -32 8 -31 4 10 -2 25 -6 69 -9 99
l-5 54 24 -23 c22 -20 23 -30 24 -133 1 -76 -2 -107 -9 -97 -13 20 -13 -4 1
-90 13 -85 38 -290 39 -314 0 -32 23 -3 68 86 54 110 81 211 89 330 l6 98 51
-37 c29 -20 43 -27 32 -14 -11 11 -14 21 -8 21 6 0 23 -18 39 -39 40 -55 146
-181 150 -177 6 6 -39 140 -70 209 -29 66 -55 91 -43 41 5 -17 3 -23 -4 -19
-5 3 -10 12 -10 19 0 7 -9 21 -21 32 -24 22 -70 114 -57 114 5 0 6 5 3 10 -14
23 25 8 44 -18 17 -21 15 -13 -9 41 -16 37 -27 67 -25 67 2 0 15 -4 29 -10 43
-16 30 -4 -46 41 -40 23 -95 59 -123 80 -114 84 -201 203 -189 258 12 53 12
53 49 25 19 -14 35 -21 35 -15 0 23 21 9 91 -56 40 -38 67 -58 61 -46 -17 31
-16 33 18 33 31 0 62 -16 113 -57 16 -13 36 -23 45 -23 11 1 -1 17 -34 48 -56
51 -85 113 -42 91 13 -6 49 -34 82 -62 45 -39 57 -45 51 -28 l-7 22 34 -21
c49 -29 58 -24 18 11 l-35 30 45 -7 c25 -3 54 -11 65 -16 12 -5 7 3 -13 21
-18 17 -40 31 -50 31 -13 0 -14 2 -4 9 9 5 -3 14 -37 29 -28 11 -51 17 -51 11
0 -17 -18 -9 -91 45 -47 34 -99 86 -142 140 -38 47 -70 86 -72 86 -6 0 23 -69
46 -113 11 -20 17 -37 13 -37 -4 0 0 -8 8 -17 9 -10 29 -48 46 -84 16 -37 35
-69 41 -73 6 -4 11 -12 11 -17 0 -13 -35 6 -80 43 -19 15 -38 28 -42 28 -15 0
-130 83 -187 136 -34 31 -61 52 -61 48 0 -4 -21 12 -46 36 l-47 45 31 -43 c17
-24 38 -67 48 -95 9 -29 27 -63 39 -77 12 -14 23 -32 23 -40 1 -8 3 -37 5 -65
2 -27 0 -53 -6 -56 -6 -4 -1 -20 12 -40 l22 -34 -23 29 c-13 15 -54 58 -91 95
-38 36 -75 80 -83 96 -10 22 -13 24 -10 8 10 -53 -37 -2 -105 115 l-39 66 5
-69 5 -70 -36 110 c-20 61 -41 118 -45 128 -6 11 -5 17 1 17 7 0 8 6 0 19 -16
30 -13 46 5 31 13 -11 14 -10 9 4 -11 29 3 17 31 -29 16 -24 31 -43 33 -40 3
2 -3 16 -12 29 l-16 25 29 -10 c16 -6 37 -19 46 -29 12 -13 26 -17 44 -14 19
4 38 -3 70 -26 24 -18 40 -27 37 -22 -8 14 15 21 35 11 9 -5 32 -10 52 -12 22
-3 44 -14 59 -30 13 -14 39 -28 58 -32 26 -5 31 -3 20 4 -8 6 -22 11 -31 11
-24 0 -51 44 -36 59 15 15 70 14 85 -1 9 -9 12 -9 12 0 0 6 -9 16 -21 22 -28
15 -7 40 33 40 24 0 28 3 20 13 -8 9 -2 18 24 36 28 19 43 23 81 18 40 -4 45
-3 30 8 -22 16 -22 25 2 25 14 0 18 5 14 20 -4 17 0 20 23 20 21 0 25 3 15 9
-8 5 -16 16 -17 25 -8 55 109 31 297 -62 l144 -71 -65 6 c-63 5 -165 24 -205
39 -48 17 6 -15 82 -49 79 -35 141 -48 128 -27 -8 13 1 13 25 0 11 -6 21 -6
24 -1 3 5 26 5 52 0 51 -10 69 -19 38 -20 -14 0 144 -38 165 -39 10 0 -29 36
-79 75 -61 47 -242 145 -267 145 -13 0 -14 3 -6 11 8 8 55 12 126 13 l113 1
-90 64 c-50 35 -93 65 -95 67 -7 6 21 12 118 25 109 14 100 23 -37 34 -92 7
-138 25 -65 25 44 0 85 19 43 20 -14 1 -50 7 -80 14 l-55 13 53 9 53 9 -175
45 c-97 24 -172 48 -167 53 4 4 68 10 142 14 123 5 148 3 303 -26 93 -17 171
-29 174 -27 8 8 -111 122 -147 140 -18 10 -30 13 -27 7 8 -13 -32 -24 -70 -19
-26 4 -25 5 14 13 41 7 52 18 32 30 -6 4 -17 2 -25 -5 -7 -6 -40 -13 -72 -15
-56 -4 -57 -4 -23 8 19 6 40 15 47 20 6 4 19 4 30 1 17 -5 17 -5 0 6 -12 7
-22 8 -30 2 -7 -5 -35 -15 -64 -22 -49 -13 -63 -30 -24 -30 10 0 36 -12 57
-26 32 -22 36 -23 20 -5 -11 11 -17 21 -14 21 15 0 97 -42 108 -55 7 -8 17
-15 22 -15 6 0 4 8 -3 17 -12 14 -9 14 19 -3 41 -24 64 -48 36 -38 -10 4 -23
9 -28 12 -5 3 -28 6 -51 8 -23 1 -62 8 -86 14 -43 12 -117 24 -209 36 -25 3
-61 9 -80 14 -19 5 -61 10 -92 11 -32 1 -58 6 -58 11 0 4 8 8 18 8 10 0 26 6
37 13 44 30 165 59 251 62 l88 2 59 63 c32 34 56 64 54 67 -3 2 -27 1 -54 -3
-39 -6 -43 -9 -23 -14 22 -5 19 -8 -25 -25 -28 -10 -54 -19 -60 -20 -5 0 -24
-8 -40 -17 -40 -22 -156 -60 -162 -54 -10 10 9 25 80 60 40 20 89 36 108 37
39 1 89 17 55 18 -17 1 -17 2 -2 26 19 29 20 39 4 29 -8 -4 -10 -3 -5 2 5 5
13 9 18 9 8 0 80 107 75 113 -2 1 -54 -18 -115 -43 -62 -25 -140 -52 -174 -61
-79 -20 -87 -28 -19 -19 35 5 52 3 52 -4 0 -19 -143 -130 -220 -170 -91 -48
-201 -78 -232 -64 -21 9 -20 10 10 32 18 12 32 28 32 35 0 7 13 25 30 41 16
16 28 30 27 32 -2 2 -48 -27 -102 -63 -95 -63 -243 -144 -265 -144 -21 0 3 23
103 96 144 106 118 98 -43 -14 -39 -27 63 101 113 141 52 43 91 63 192 99 56
20 69 27 43 24 -21 -3 -38 -3 -38 -1 0 2 90 77 200 165 110 89 200 163 200
166 0 6 -104 -47 -110 -56 -3 -4 -18 -10 -34 -14 -24 -6 -27 -9 -15 -18 11 -8
11 -9 -4 -4 -11 3 -57 -14 -117 -44 -62 -31 -100 -45 -102 -38 -2 6 -17 -8
-34 -32 -29 -43 -200 -188 -210 -178 -8 9 86 117 205 235 82 82 111 118 107
130 -4 10 4 27 21 46 15 17 0 7 -34 -22 -35 -30 -76 -77 -92 -105 -32 -57 -53
-79 -81 -83 -17 -2 -18 1 -8 28 10 30 -4 113 -22 124 -5 3 -22 -11 -39 -30
-39 -45 -39 -39 -1 22 29 46 39 83 22 83 -15 0 -37 -31 -97 -140 -106 -192
-174 -287 -259 -365 -44 -41 -82 -72 -85 -70 -2 3 8 23 23 46 15 23 23 38 17
35 -5 -4 -31 -40 -56 -81 -26 -41 -51 -75 -56 -75 -5 0 -23 -24 -39 -54 -17
-30 -37 -56 -44 -59 -7 -3 -22 -28 -32 -56 -10 -28 -21 -51 -24 -51 -4 0 -5
33 -3 73 5 105 68 261 161 400 28 44 52 89 53 101 0 11 5 30 10 41 7 15 8 12
3 -12 -3 -18 -4 -33 -1 -33 6 0 92 131 116 178 54 106 84 232 79 332 l-3 65
-24 -65 c-13 -36 -42 -100 -63 -142 -21 -43 -38 -83 -38 -90 0 -7 -10 -26 -21
-43 -12 -16 -18 -22 -15 -11 7 20 -32 116 -47 116 -5 0 -6 -8 -3 -17 6 -14 3
-14 -15 3 -13 11 -19 13 -13 4 4 -8 7 -23 5 -34 -1 -11 2 -16 8 -12 6 4 11 2
11 -3 0 -6 -4 -11 -10 -11 -5 0 -10 -9 -10 -20 0 -11 -6 -20 -12 -20 -10 0
-10 -2 0 -9 10 -6 10 -11 -3 -21 -13 -11 -18 -11 -24 -1 -5 7 -10 12 -12 10
-2 -1 -15 2 -28 6 l-24 7 24 13 c13 7 27 11 30 9 4 -2 5 4 2 15 -3 13 -11 17
-24 14 -10 -3 -27 1 -37 8 -15 12 -16 12 -5 -2 10 -12 10 -18 0 -28 -10 -11
-8 -11 11 -1 13 6 26 9 29 7 2 -3 -5 -10 -16 -17 -21 -11 -21 -11 9 -41 30
-30 40 -69 19 -69 -6 0 -9 9 -6 20 3 11 1 18 -4 14 -5 -3 -9 -12 -9 -19 0 -9
-6 -12 -17 -8 -15 5 -15 3 2 -11 18 -16 18 -17 2 -12 -10 3 -21 6 -23 6 -2 0
-4 9 -4 20 0 11 -4 20 -10 20 -5 0 -10 -6 -10 -13 0 -15 45 -57 61 -57 6 0 7
-5 2 -12 -8 -13 -10 -19 -16 -50 -1 -9 -14 -26 -27 -37 l-25 -20 23 -1 c16 0
21 -4 15 -12 -4 -7 -6 -21 -5 -30 2 -13 -8 -20 -42 -29 l-46 -11 0 -184 c0
-116 -4 -184 -10 -184 -6 0 -10 68 -10 183 l0 182 -31 17 c-17 10 -28 22 -24
27 3 5 1 12 -5 16 -5 3 -10 11 -10 16 0 6 5 7 10 4 14 -9 58 19 93 59 20 22
30 27 34 18 4 -10 11 -5 24 15 10 15 19 33 19 38 -1 6 -5 2 -11 -9 -9 -15 -13
-16 -20 -5 -7 12 -9 11 -9 -1 0 -8 -18 -26 -40 -39 -21 -14 -37 -27 -35 -31 2
-3 -4 -6 -13 -6 -9 -1 -24 -3 -32 -5 -8 -2 -15 3 -15 11 -2 26 5 30 28 18 20
-10 21 -10 8 6 -13 15 -12 16 15 6 25 -9 26 -8 12 3 -15 12 -15 14 0 25 14 10
13 11 -5 2 -30 -14 -48 -13 -37 3 11 14 11 37 1 54 -4 7 1 14 14 18 14 5 18
10 11 17 -6 6 -16 7 -24 2 -20 -13 -40 -14 -57 -3 -12 8 -10 8 8 3 22 -6 23
-4 19 24 -4 26 -2 29 11 22 13 -6 16 -3 16 16 0 13 -3 24 -7 24 -5 0 -8 10 -7
23 0 21 1 21 9 2 6 -14 9 -16 10 -5 2 8 2 18 1 23 -2 13 17 8 34 -9 9 -9 14
-22 12 -29 -3 -6 1 -12 9 -12 7 0 11 4 8 8 -5 9 26 49 39 49 4 0 5 -4 2 -10
-3 -5 -2 -10 3 -10 6 0 12 7 16 15 3 8 13 15 21 15 9 0 13 5 10 10 -3 6 -15
10 -26 10 -12 0 -17 -4 -13 -11 5 -8 1 -8 -14 0 -25 13 -43 15 -23 2 12 -8 12
-12 -2 -26 -15 -15 -19 -15 -39 -1 -12 9 -17 16 -10 16 6 0 12 5 12 11 0 8 -4
8 -13 1 -8 -7 -29 -9 -50 -5 -25 4 -37 2 -37 -6 0 -8 8 -10 22 -5 20 6 21 5 9
-10 -20 -24 -40 -20 -36 9 1 14 -2 28 -7 31 -5 4 8 17 29 30 35 21 36 23 13
24 -15 0 -40 -14 -63 -35 -21 -19 -41 -35 -45 -35 -4 0 2 7 12 15 11 8 16 15
11 15 -4 0 0 10 11 21 19 22 19 22 -1 10 -11 -7 -31 -28 -45 -46 -19 -26 -27
-30 -33 -19 -4 8 -5 19 -2 24 3 6 4 10 1 10 -13 0 -56 -105 -50 -124 4 -15 -1
-12 -15 9 -12 17 -21 36 -21 43 0 7 -17 47 -38 90 -21 42 -49 105 -62 140 -12
34 -24 62 -26 62 -2 0 -4 -30 -4 -68z m320 -182 c0 -5 -2 -10 -4 -10 -3 0 -8
5 -11 10 -3 6 -1 10 4 10 6 0 11 -4 11 -10z m-750 -112 c0 -6 -7 -5 -15 2 -8
7 -15 17 -15 22 0 6 7 5 15 -2 8 -7 15 -17 15 -22z m1780 7 c-7 -9 -15 -13
-18 -10 -3 2 1 11 8 20 7 9 15 13 18 10 3 -2 -1 -11 -8 -20z m-1906 -90 c11
-8 16 -15 10 -15 -5 0 -18 7 -28 15 -11 8 -16 15 -10 15 5 0 18 -7 28 -15z
m922 0 c-11 -8 -25 -15 -30 -15 -6 0 -2 7 8 15 11 8 25 15 30 15 6 0 2 -7 -8
-15z m1108 0 c-10 -8 -23 -15 -28 -15 -6 0 -1 7 10 15 10 8 23 15 28 15 6 0 1
-7 -10 -15z m-1219 -136 c47 -92 56 -131 16 -65 -21 34 -54 116 -46 116 3 0
16 -23 30 -51z m410 10 c-15 -40 -53 -109 -61 -109 -5 0 12 42 41 99 31 62 39
66 20 10z m-736 -44 c0 -5 -13 6 -29 25 -17 19 -30 39 -30 45 0 5 14 -6 30
-25 16 -19 29 -39 29 -45z m1076 30 c-15 -20 -29 -34 -32 -31 -3 2 7 21 22 41
15 19 30 33 33 31 3 -3 -8 -22 -23 -41z m-1074 -105 c57 -55 108 -100 113
-100 11 0 -10 26 -78 98 -28 28 -46 52 -41 52 12 0 121 -103 158 -148 15 -19
27 -28 27 -22 0 18 56 -20 120 -82 68 -67 139 -197 142 -263 1 -22 6 -46 11
-53 5 -7 8 -15 5 -17 -2 -3 -17 21 -34 51 -16 31 -34 56 -39 56 -18 -1 -36 21
-62 71 -37 73 -117 191 -121 178 -2 -6 22 -51 53 -102 65 -104 69 -142 7 -53
-43 59 -199 200 -211 189 -3 -4 19 -29 49 -57 139 -128 161 -150 176 -181 15
-28 9 -25 -44 26 -33 31 -64 57 -69 57 -5 0 -31 21 -59 48 -28 26 -74 67 -103
90 -36 30 -50 49 -46 61 3 12 -10 25 -50 49 -31 18 -53 37 -50 42 3 5 29 0 58
-10 28 -9 65 -21 81 -24 l28 -7 -25 23 c-49 46 -120 128 -109 128 5 0 57 -45
113 -100z m1114 48 c-26 -29 -59 -63 -72 -76 l-25 -23 28 7 c16 3 53 15 81 24
29 10 55 15 58 10 3 -5 -19 -24 -50 -42 -40 -24 -53 -37 -50 -49 4 -12 -10
-31 -46 -61 -29 -23 -75 -64 -103 -90 -28 -27 -54 -48 -59 -48 -5 0 -36 -26
-69 -57 -53 -51 -59 -54 -44 -26 15 31 37 53 176 181 30 28 52 53 49 57 -12
11 -168 -130 -211 -189 -62 -89 -58 -51 7 53 31 51 55 96 53 102 -4 13 -84
-105 -122 -179 -25 -49 -41 -67 -60 -67 -5 0 -23 -26 -40 -57 -16 -32 -32 -56
-34 -53 -3 2 0 10 5 17 5 7 10 31 11 53 3 66 74 196 142 263 64 62 120 100
120 82 0 -6 12 3 27 22 37 45 146 148 158 148 5 0 -13 -24 -41 -52 -68 -72
-89 -98 -78 -98 5 0 56 45 113 100 110 106 155 135 76 48z m-1807 -81 c34 -33
46 -63 13 -33 -11 10 -24 15 -29 12 -13 -8 -112 72 -112 90 0 19 82 -25 128
-69z m2552 69 c0 -18 -99 -98 -112 -90 -5 3 -18 -2 -29 -12 -33 -30 -21 0 14
33 25 24 109 80 125 83 1 0 2 -6 2 -14z m-1386 -32 c17 -6 22 -24 8 -24 -5 0
-17 7 -28 15 -19 15 -8 20 20 9z m112 -9 c-23 -17 -36 -19 -36 -6 0 11 18 19
40 20 11 0 10 -3 -4 -14z m-1408 -99 c60 -28 86 -55 40 -41 -31 10 -105 53
-114 66 -4 7 -3 10 2 8 5 -2 37 -17 72 -33z m2752 19 c-16 -20 -119 -69 -127
-61 -9 8 -6 10 62 44 69 34 82 38 65 17z m-2895 -195 c4 -7 -5 -9 -26 -5 -44
8 -50 15 -11 15 17 0 34 -5 37 -10z m3034 6 c-10 -11 -70 -16 -64 -6 3 5 20
10 37 10 17 0 29 -2 27 -4z m-2205 -15 c3 -5 18 -12 33 -16 36 -8 93 -52 93
-70 0 -7 12 -27 26 -44 14 -17 23 -31 20 -31 -19 0 -105 52 -127 77 -39 43
-72 93 -60 93 5 0 12 -4 15 -9z m124 -20 c32 -16 75 -48 95 -70 20 -23 37 -38
37 -34 0 3 -7 15 -17 25 -9 10 -14 21 -10 24 13 13 131 -71 122 -87 -4 -5 1
-6 9 -3 24 9 66 -74 66 -132 l-1 -49 -14 32 c-8 18 -25 38 -39 44 -14 7 -44
38 -68 71 -24 32 -44 54 -46 48 -2 -6 -22 9 -44 32 -40 42 -40 42 4 -12 24
-30 48 -61 53 -69 14 -24 -82 54 -111 90 -15 19 -46 53 -68 77 -22 23 -37 42
-33 42 4 0 34 -13 65 -29z m1096 -13 c-22 -24 -53 -58 -68 -77 -29 -36 -125
-114 -111 -90 5 8 29 39 53 69 44 54 44 54 4 12 -22 -23 -42 -38 -44 -32 -2 6
-22 -16 -46 -48 -24 -33 -54 -64 -68 -71 -14 -6 -31 -26 -39 -44 l-14 -32 -1
49 c0 58 42 141 66 132 8 -3 13 -2 9 3 -9 16 109 100 122 87 4 -3 -1 -14 -10
-24 -10 -10 -17 -22 -17 -25 0 -4 17 11 37 34 32 36 131 97 158 99 5 0 -9 -19
-31 -42z m95 10 c-39 -58 -73 -89 -121 -114 -61 -31 -65 -30 -34 7 14 17 26
37 26 44 0 18 57 62 93 70 15 4 30 11 33 16 3 5 10 9 15 9 6 0 0 -15 -12 -32z
m-1702 6 c-3 -3 -12 -4 -19 -1 -8 3 -5 6 6 6 11 1 17 -2 13 -5z m2110 0 c-3
-3 -12 -4 -19 -1 -8 3 -5 6 6 6 11 1 17 -2 13 -5z m-1255 -131 c30 -36 30 -47
0 -19 -18 17 -32 31 -32 33 0 9 19 1 32 -14z m408 14 c0 -2 -14 -16 -32 -33
-30 -28 -30 -17 0 19 13 15 32 23 32 14z m-1263 -29 c-2 -7 3 -18 12 -25 14
-11 14 -13 -4 -27 -11 -8 -26 -16 -32 -16 -27 0 -11 -18 22 -24 47 -9 52 -22
14 -41 l-31 -17 32 -19 c20 -13 30 -25 26 -34 -3 -8 0 -15 7 -15 19 -1 -35
-38 -62 -43 -52 -10 -210 51 -234 90 -4 7 -17 13 -27 13 -26 0 -26 17 0 25 11
3 25 15 30 25 6 10 28 29 50 41 22 13 40 27 40 31 0 5 19 17 43 28 49 22 121
27 114 8z m924 -10 c11 -21 11 -22 -4 -9 -10 7 -17 17 -17 22 0 15 9 10 21
-13z m245 -6 c-18 -16 -18 -16 -6 6 6 13 14 21 18 18 3 -4 -2 -14 -12 -24z
m1028 8 c25 -10 46 -23 46 -28 0 -4 18 -18 40 -31 22 -12 44 -31 50 -41 5 -10
19 -22 30 -25 26 -8 26 -25 0 -25 -10 0 -23 -6 -27 -13 -24 -39 -182 -100
-234 -90 -27 5 -81 42 -62 43 7 0 10 7 7 15 -4 9 6 21 26 34 l32 19 -31 17
c-38 19 -33 32 14 41 33 6 49 24 23 24 -7 0 -22 8 -33 16 -18 14 -18 16 -4 27
9 7 14 18 12 25 -7 19 59 15 111 -8z m-1681 -67 c3 -5 -6 -16 -18 -26 -27 -19
-45 -6 -25 18 13 16 36 20 43 8z m1097 -8 c20 -24 2 -37 -25 -18 -12 10 -21
21 -18 26 7 12 30 8 43 -8z m-842 -60 c-3 -3 -9 2 -12 12 -6 14 -5 15 5 6 7
-7 10 -15 7 -18z m555 10 c-3 -9 -8 -14 -10 -11 -3 3 -2 9 2 15 9 16 15 13 8
-4z m-713 -9 c0 -3 -4 -8 -10 -11 -5 -3 -10 -1 -10 4 0 6 5 11 10 11 6 0 10
-2 10 -4z m880 -6 c0 -5 -2 -10 -4 -10 -3 0 -8 5 -11 10 -3 6 -1 10 4 10 6 0
11 -4 11 -10z m-470 -29 c0 -4 9 -7 20 -7 11 0 20 3 20 7 0 4 10 9 21 12 17 4
19 3 10 -6 -20 -20 -11 -30 11 -11 18 16 21 17 15 2 -3 -9 -3 -19 1 -21 4 -2
1 -2 -5 -1 -18 5 -16 -7 4 -21 14 -10 15 -16 5 -28 -18 -21 -55 -30 -48 -11 5
14 1 16 -34 16 -35 0 -39 -2 -34 -16 7 -19 -30 -10 -48 11 -10 12 -9 18 5 28
20 14 22 26 5 21 -7 -1 -10 -1 -6 1 4 2 4 12 1 21 -6 15 -3 14 15 -2 22 -19
31 -9 11 11 -9 9 -7 10 10 6 11 -3 21 -8 21 -12z m-112 -47 c12 -8 12 -19 5
-57 -9 -42 -8 -49 15 -82 16 -23 28 -31 33 -24 4 6 12 8 18 5 25 -16 9 12 -24
41 -19 17 -35 37 -35 45 1 7 14 -2 30 -21 16 -19 31 -33 33 -30 2 2 11 16 20
32 10 15 26 27 37 27 11 0 27 -12 37 -27 9 -16 18 -30 20 -32 2 -3 17 11 33
30 16 19 29 28 30 21 0 -8 -16 -28 -35 -45 -33 -29 -49 -57 -24 -41 6 3 14 1
18 -5 5 -7 17 1 33 24 23 33 24 40 15 82 -7 38 -7 49 5 57 8 6 17 18 20 26 6
17 38 -51 38 -81 0 -11 4 -19 9 -19 5 0 15 -38 21 -83 11 -72 11 -90 -3 -128
l-15 -44 -2 55 c-1 52 -1 54 -17 35 -15 -19 -15 -19 -10 10 2 17 2 38 -2 47
-4 12 -14 4 -38 -37 -31 -53 -31 -54 -7 -45 30 12 31 -2 5 -52 -11 -19 -18
-38 -16 -40 2 -2 -14 -24 -36 -48 -44 -48 -59 -72 -59 -92 1 -7 7 2 15 20 8
17 19 32 25 32 7 0 7 -7 -2 -22 -7 -13 -19 -52 -28 -88 -10 -40 -22 -65 -30
-65 -8 0 -20 25 -30 65 -9 36 -21 75 -28 88 -9 15 -9 22 -2 22 6 0 17 -15 25
-32 8 -18 14 -27 15 -20 0 20 -15 44 -59 92 -22 24 -38 46 -36 48 2 2 -5 21
-16 40 -26 50 -25 64 5 52 24 -9 24 -8 -7 45 -24 41 -34 49 -38 37 -4 -9 -4
-30 -2 -47 5 -29 5 -29 -10 -10 -16 19 -16 17 -17 -35 l-2 -55 -15 44 c-14 38
-14 56 -3 128 6 45 16 83 21 83 5 0 9 8 9 19 0 30 32 98 38 81 3 -8 12 -20 20
-26z m-1196 16 c65 -19 58 -33 -13 -25 -35 3 -73 9 -84 12 -18 6 -18 7 5 14
36 10 51 10 92 -1z m2748 1 c23 -7 23 -8 5 -14 -44 -13 -148 -19 -141 -8 15
23 90 35 136 22z m-1960 -27 c0 -8 -4 -12 -10 -9 -5 3 -10 10 -10 16 0 5 5 9
10 9 6 0 10 -7 10 -16z m1100 7 c0 -6 -4 -13 -10 -16 -5 -3 -10 1 -10 9 0 9 5
16 10 16 6 0 10 -4 10 -9z m-619 -58 c12 -15 12 -16 -6 -9 -16 6 -18 5 -7 -5
17 -18 15 -34 -2 -20 -19 16 -27 51 -11 51 7 0 19 -8 26 -17z m152 -3 c-3 -10
-11 -25 -19 -31 -17 -14 -19 2 -1 20 10 10 8 11 -8 5 -18 -7 -18 -6 -6 9 19
24 40 22 34 -3z m-1363 -40 c-36 -17 -72 -34 -80 -40 -29 -20 -121 -60 -138
-60 -15 1 -14 2 3 15 11 8 25 18 30 23 18 15 142 80 161 85 10 2 35 5 54 5 33
1 31 -1 -30 -28z m2646 -16 c38 -21 74 -42 79 -46 6 -5 19 -15 30 -23 17 -13
18 -14 3 -15 -17 0 -109 40 -138 60 -8 6 -44 24 -80 41 l-65 30 51 -5 c33 -3
75 -18 120 -42z m-1661 -54 c-10 -11 -20 -18 -22 -16 -6 5 30 44 36 39 2 -1
-4 -12 -14 -23z m595 -17 c0 -3 -9 2 -20 12 -11 10 -20 22 -20 27 0 4 9 -1 20
-12 11 -11 20 -23 20 -27z m-926 -14 c-6 -11 -24 -2 -24 11 0 5 7 7 15 4 8 -4
12 -10 9 -15z m1251 2 c-3 -6 -11 -8 -17 -5 -6 4 -5 9 3 15 16 10 23 4 14 -10z
m-1013 -18 c-6 -9 -18 -18 -28 -19 -15 -2 -14 1 6 16 31 24 33 24 22 3z m798
-18 c0 -11 -34 5 -42 19 -9 17 -9 17 16 1 14 -10 26 -19 26 -20z m-990 15 c0
-5 -9 -10 -21 -10 -11 0 -17 5 -14 10 3 6 13 10 21 10 8 0 14 -4 14 -10z
m1175 0 c3 -5 -3 -10 -14 -10 -12 0 -21 5 -21 10 0 6 6 10 14 10 8 0 18 -4 21
-10z m-1139 -15 c-26 -19 -42 -19 -26 0 7 8 20 15 29 15 13 -1 13 -3 -3 -15z
m1094 0 c16 -19 0 -19 -26 0 -16 12 -16 14 -3 15 9 0 22 -7 29 -15z m-1029 -9
c-7 -8 -21 -14 -31 -13 -26 2 -5 27 22 27 17 0 18 -3 9 -14z m977 1 c8 -10 6
-13 -8 -14 -10 -1 -24 5 -31 13 -9 11 -8 14 9 14 11 0 24 -6 30 -13z m-617
-169 c11 -22 18 -42 15 -45 -2 -3 -14 12 -25 34 -11 21 -20 31 -21 23 -1 -22
-30 19 -30 43 0 10 -4 15 -10 12 -11 -7 -14 8 -3 18 9 9 50 -37 74 -85z m-20
75 c13 -16 12 -17 -3 -4 -10 7 -18 15 -18 17 0 8 8 3 21 -13z m279 13 c0 -2
-8 -10 -17 -17 -16 -13 -17 -12 -4 4 13 16 21 21 21 13z m40 -17 c0 -6 -4 -7
-10 -4 -5 3 -10 -2 -10 -12 0 -24 -29 -65 -30 -43 -1 8 -10 -2 -21 -23 -11
-22 -23 -37 -25 -34 -8 7 31 79 58 110 22 24 38 27 38 6z m-374 -116 c-3 -10
-7 -36 -10 -58 l-6 -40 15 34 c9 18 18 31 20 29 9 -9 -43 -187 -72 -248 -39
-80 -41 -62 -13 105 11 66 20 135 20 154 0 25 4 32 15 27 8 -3 15 -1 15 4 0 6
5 10 11 10 6 0 8 -8 5 -17z m384 7 c0 -5 7 -7 15 -4 11 5 15 -2 15 -27 0 -19
9 -88 20 -154 28 -167 26 -185 -13 -105 -29 61 -81 239 -72 248 2 2 11 -11 20
-29 l15 -34 -6 40 c-3 22 -7 48 -10 58 -3 9 -1 17 5 17 6 0 11 -4 11 -10z
m-450 -52 c0 -73 -28 -180 -66 -255 -37 -74 -173 -238 -159 -192 7 24 -24 -15
-69 -84 -9 -16 -19 -27 -22 -25 -6 7 52 178 83 243 16 33 41 79 56 102 15 24
27 47 27 53 0 16 42 101 47 96 3 -3 0 -21 -7 -41 -7 -20 -13 -51 -14 -68 -2
-18 -6 -45 -10 -62 -10 -46 24 30 64 140 18 50 36 100 41 113 14 36 29 25 29
-20z m563 -73 c46 -128 82 -208 71 -160 -4 17 -8 44 -10 62 -1 17 -7 48 -14
68 -7 20 -10 38 -7 41 5 5 47 -80 47 -96 0 -6 12 -29 27 -53 15 -23 40 -69 56
-102 31 -65 89 -236 83 -243 -3 -2 -13 9 -22 25 -45 69 -76 108 -69 84 14 -46
-122 118 -159 192 -38 75 -66 182 -66 255 0 69 19 47 63 -73z m-362 -60 c16
-33 32 -75 35 -92 3 -18 10 -33 14 -33 4 0 11 15 14 33 6 34 55 141 77 169 9
11 19 15 27 10 10 -7 7 -26 -11 -90 -27 -92 -21 -100 14 -16 18 47 24 53 30
37 10 -28 -21 -134 -68 -228 -22 -44 -48 -104 -57 -132 -10 -29 -21 -53 -26
-53 -5 0 -16 24 -26 53 -9 28 -35 88 -57 132 -47 94 -78 200 -68 228 6 16 12
10 30 -37 35 -84 41 -76 14 16 -18 64 -21 83 -11 90 16 10 32 -10 69 -87z
m-412 -60 c-19 -36 -37 -65 -41 -65 -10 0 -10 1 3 41 13 38 58 103 66 95 3 -3
-10 -35 -28 -71z m944 12 c22 -47 29 -77 20 -77 -8 0 -73 119 -73 134 0 16 38
-25 53 -57z m-649 -106 c9 -52 15 -96 12 -98 -9 -9 -36 81 -42 144 -10 100 11
69 30 -46z m362 43 c-6 -60 -33 -150 -42 -141 -5 5 25 188 33 200 12 20 15 1
9 -59z m-755 -119 c-25 -40 -65 -96 -89 -125 -24 -29 -50 -63 -58 -74 -12 -17
-16 -18 -23 -7 -7 11 -9 11 -14 0 -8 -18 2 57 13 96 4 17 14 56 21 89 12 57
33 81 49 56 10 -16 8 -28 -9 -59 -8 -14 -9 -21 -2 -17 6 4 18 21 27 38 8 16
18 27 21 25 3 -3 -2 -20 -10 -36 -9 -17 -12 -31 -7 -31 4 0 13 14 20 31 7 17
31 59 53 92 l41 62 7 -33 c6 -29 1 -44 -40 -107z m1162 18 c17 -35 35 -63 39
-63 3 0 0 14 -9 31 -8 16 -13 33 -10 36 3 2 13 -9 21 -25 9 -17 21 -34 27 -38
7 -4 6 3 -2 17 -17 31 -19 43 -9 59 16 25 37 1 49 -56 7 -33 17 -72 21 -89 11
-39 21 -114 13 -96 -5 11 -7 11 -14 0 -7 -11 -11 -10 -23 7 -8 11 -34 45 -58
74 -24 29 -64 85 -89 125 -38 59 -45 78 -41 105 l5 34 23 -30 c13 -16 39 -57
57 -91z m-1324 -22 c-42 -94 -119 -232 -144 -257 -7 -8 -18 -14 -23 -14 -6 0
-16 -14 -24 -30 -7 -17 -21 -33 -30 -36 -9 -3 -18 -15 -20 -26 -4 -25 -23 -35
-31 -15 -3 8 6 27 19 42 27 32 32 60 9 51 -9 -3 -15 0 -15 10 0 10 6 14 15 10
11 -4 20 4 29 27 44 104 60 130 117 190 55 58 63 56 20 -6 -15 -23 -41 -67
-55 -97 -15 -30 -32 -63 -38 -73 -5 -11 -8 -21 -5 -23 7 -7 74 99 129 204 61
116 78 145 84 140 2 -3 -15 -46 -37 -97z m1479 -24 c61 -117 132 -230 139
-223 3 2 0 12 -5 23 -6 10 -23 43 -38 73 -14 30 -40 74 -55 97 -43 62 -35 64
20 6 57 -60 73 -86 117 -190 9 -23 18 -31 29 -27 9 4 15 0 15 -10 0 -10 -6
-13 -15 -10 -23 9 -18 -19 9 -51 13 -15 22 -34 19 -42 -8 -20 -27 -10 -31 15
-2 11 -11 23 -20 26 -9 3 -23 19 -30 36 -8 16 -18 30 -24 30 -31 0 -122 160
-206 359 -3 9 -1 12 6 8 6 -4 37 -58 70 -120z m-960 26 c23 -77 72 -276 68
-280 -6 -6 -55 86 -72 136 -19 57 -31 181 -17 181 5 0 15 -17 21 -37z m429
-30 c-3 -38 -13 -89 -21 -114 -17 -50 -66 -142 -72 -136 -4 4 45 203 68 280
18 60 32 45 25 -30z m-287 -63 c29 -66 56 -120 60 -120 4 0 31 54 60 120 29
66 56 120 60 120 9 0 -1 -47 -24 -109 -26 -71 -17 -81 14 -16 16 34 29 52 29
41 1 -20 1 -20 16 0 8 10 15 16 15 12 0 -15 -61 -171 -93 -235 -19 -37 -43
-105 -53 -150 -10 -46 -21 -83 -24 -83 -3 0 -14 37 -24 83 -10 45 -34 113 -53
150 -32 64 -93 220 -93 235 0 4 7 -2 15 -12 15 -20 15 -20 16 0 0 11 13 -7 29
-41 31 -65 40 -55 14 16 -23 62 -33 109 -24 109 4 0 31 -54 60 -120z m-190
-25 c16 -35 30 -69 30 -75 0 -6 6 -23 14 -38 7 -15 16 -45 18 -67 3 -22 10
-51 17 -64 23 -42 3 -34 -29 12 -41 57 -83 159 -69 167 9 6 4 39 -16 108 -14
44 5 21 35 -43z m532 28 c-18 -49 -24 -86 -13 -92 14 -9 -28 -110 -69 -168
-32 -46 -52 -54 -29 -12 7 13 14 42 17 64 2 22 11 52 18 67 8 15 14 32 14 38
0 18 61 142 67 136 3 -3 1 -18 -5 -33z m-572 -129 c17 -43 42 -99 55 -124 13
-25 29 -59 34 -75 l11 -30 -25 30 c-13 16 -32 45 -42 64 -10 20 -30 57 -45 83
-30 52 -61 156 -51 171 7 12 23 -18 63 -119z m634 53 c-8 -31 -27 -79 -42
-105 -15 -26 -35 -63 -45 -83 -10 -19 -29 -48 -42 -64 l-25 -30 11 30 c5 16
21 50 34 75 13 25 37 79 54 120 43 110 54 131 62 123 5 -5 2 -34 -7 -66z
m-1314 -2 c-13 -14 -28 -25 -33 -25 -6 0 0 11 13 25 13 14 28 25 33 25 6 0 0
-11 -13 -25z m1960 0 c13 -14 19 -25 13 -25 -5 0 -20 11 -33 25 -13 14 -19 25
-13 25 5 0 20 -11 33 -25z m-1051 -220 c0 -12 -12 5 -30 40 -16 33 -29 69 -28
80 0 12 12 -5 30 -40 16 -33 29 -69 28 -80z m154 43 c-16 -37 -29 -56 -31 -46
-2 10 9 46 25 80 16 37 29 56 31 46 2 -10 -9 -46 -25 -80z m-903 -125 c-1 -27
-9 -66 -20 -88 -11 -22 -19 -47 -19 -55 0 -8 10 5 21 29 21 42 22 43 30 20 5
-13 7 -42 3 -64 -3 -22 -8 -65 -11 -95 l-6 -54 -15 39 c-8 22 -18 56 -20 75
-5 29 -7 32 -13 15 -6 -16 -9 -12 -15 20 -3 22 -11 49 -16 61 -16 33 -7 89 14
88 9 -1 17 3 17 9 0 12 31 45 43 46 4 1 7 -20 7 -46z m753 -16 c20 -34 39 -75
42 -90 9 -35 21 -35 30 0 7 29 75 156 80 151 2 -2 -16 -59 -39 -128 -27 -79
-47 -125 -56 -125 -9 0 -28 41 -51 110 -20 61 -40 118 -44 128 -15 38 5 14 38
-46z m897 46 c11 -10 20 -23 20 -29 0 -7 8 -11 18 -10 20 1 29 -55 13 -88 -5
-12 -13 -39 -16 -61 -6 -32 -9 -36 -15 -20 -6 17 -8 14 -13 -15 -2 -19 -12
-53 -20 -75 l-15 -39 -6 54 c-3 30 -8 73 -11 95 -4 22 -2 51 3 64 8 23 9 22
30 -20 11 -24 21 -37 21 -29 0 8 -8 33 -19 55 -17 35 -28 135 -15 135 3 0 14
-8 25 -17z m-972 -38 c-3 -3 -13 7 -23 22 l-17 28 23 -22 c12 -12 20 -25 17
-28z m286 20 c-9 -14 -19 -23 -22 -20 -5 4 26 45 35 45 2 0 -4 -11 -13 -25z
m-295 -72 l33 -48 -36 39 c-31 34 -44 56 -33 56 2 0 18 -21 36 -47z m303 9
c-12 -16 -29 -35 -39 -43 -9 -8 -2 7 17 33 18 27 35 46 38 44 2 -3 -5 -18 -16
-34z m-380 -89 c30 -31 108 -176 108 -201 0 -6 -85 105 -105 137 -5 9 -20 33
-32 52 -39 62 -25 67 29 12z m488 31 c0 -4 -10 -23 -22 -43 -13 -19 -27 -43
-33 -52 -20 -32 -105 -143 -105 -137 0 25 78 170 108 201 34 35 52 45 52 31z
m-1047 -263 c-16 -20 -32 124 -17 156 5 12 10 -11 14 -67 4 -47 5 -87 3 -89z
m1575 80 c-3 -62 -11 -93 -21 -80 -2 2 -1 42 1 87 3 56 8 80 14 74 6 -6 9 -41
6 -81z"/>
<path d="M1591 2353 c7 -12 15 -20 18 -17 3 2 -3 12 -13 22 -17 16 -18 16 -5
-5z"/>
<path d="M1900 2355 c-7 -9 -11 -17 -9 -20 3 -2 10 5 17 15 14 24 10 26 -8 5z"/>
<path d="M1376 2320 c14 -27 65 -100 70 -100 8 0 -25 55 -51 85 -15 17 -23 23
-19 15z"/>
<path d="M1460 2225 c0 -5 5 -17 10 -25 5 -8 10 -10 10 -5 0 6 -5 17 -10 25
-5 8 -10 11 -10 5z"/>
<path d="M2098 2298 c-28 -37 -51 -78 -44 -78 4 0 45 58 70 98 15 24 -2 11
-26 -20z"/>
<path d="M2026 2215 c-9 -26 -7 -32 5 -12 6 10 9 21 6 23 -2 3 -7 -2 -11 -11z"/>
<path d="M1600 1926 c0 -35 32 -100 41 -84 6 9 15 6 35 -12 l28 -25 -18 29
c-10 16 -34 49 -52 73 -31 41 -34 43 -34 19z"/>
<path d="M1865 1906 c-16 -22 -39 -54 -49 -71 l-20 -30 28 25 c20 18 29 21 35
12 10 -17 44 57 39 85 -2 17 -8 14 -33 -21z"/>
<path d="M1660 1720 c11 -22 23 -40 25 -40 3 0 -4 18 -15 40 -11 22 -23 40
-25 40 -3 0 4 -18 15 -40z"/>
<path d="M1830 1720 c-11 -22 -18 -40 -15 -40 2 0 14 18 25 40 11 22 18 40 15
40 -2 0 -14 -18 -25 -40z"/>
<path d="M1260 1150 c-6 -11 -8 -20 -6 -20 3 0 10 9 16 20 6 11 8 20 6 20 -3
0 -10 -9 -16 -20z"/>
<path d="M2230 1150 c6 -11 13 -20 16 -20 2 0 0 9 -6 20 -6 11 -13 20 -16 20
-2 0 0 -9 6 -20z"/>
<path d="M1640 1505 c0 -5 5 -17 10 -25 5 -8 10 -10 10 -5 0 6 -5 17 -10 25
-5 8 -10 11 -10 5z"/>
<path d="M1846 1495 c-9 -26 -7 -32 5 -12 6 10 9 21 6 23 -2 3 -7 -2 -11 -11z"/>
<path d="M1056 1088 c-9 -12 -15 -32 -15 -43 1 -17 2 -17 6 -2 2 9 9 17 14 17
5 0 9 9 9 19 0 11 6 22 13 24 9 4 9 6 0 6 -6 1 -19 -9 -27 -21z"/>
<path d="M2418 1103 c6 -2 12 -13 12 -24 0 -10 4 -19 9 -19 5 0 12 -8 14 -17
4 -15 5 -15 6 2 1 25 -25 65 -42 64 -9 0 -9 -2 1 -6z"/>
<path d="M932 570 c0 -19 2 -27 5 -17 2 9 2 25 0 35 -3 9 -5 1 -5 -18z"/>
<path d="M2562 570 c0 -19 2 -27 5 -17 2 9 2 25 0 35 -3 9 -5 1 -5 -18z"/>
<path d="M810 3416 c0 -31 191 -431 196 -411 1 6 4 38 6 73 3 49 1 62 -9 58
-8 -3 -15 5 -19 19 -3 14 -10 25 -15 25 -5 0 -9 7 -9 16 0 9 -9 28 -21 42 -11
15 -45 63 -75 106 -30 44 -54 76 -54 72z"/>
<path d="M2622 3324 c-35 -52 -68 -97 -73 -100 -5 -3 -9 -14 -9 -25 0 -10 -4
-19 -9 -19 -5 0 -12 -11 -15 -25 -4 -14 -11 -22 -19 -19 -10 4 -12 -9 -9 -58
2 -35 5 -67 6 -73 3 -13 116 210 161 319 20 48 35 89 33 91 -2 2 -32 -39 -66
-91z"/>
<path d="M1095 3393 c4 -10 19 -68 34 -128 30 -119 18 -99 180 -313 35 -46 86
-124 113 -174 27 -50 68 -116 92 -147 69 -90 117 -162 131 -194 7 -16 15 -25
18 -19 7 11 -47 96 -135 212 -31 41 -70 102 -86 135 -31 62 -77 193 -88 252
-3 18 -12 37 -20 44 -8 6 -14 17 -14 24 0 6 -20 43 -45 81 -55 85 -57 74 -6
-28 36 -72 57 -128 47 -128 -11 0 -98 170 -112 219 -9 31 -37 84 -62 119 -47
63 -60 76 -47 45z"/>
<path d="M2358 3348 c-25 -35 -53 -88 -62 -119 -14 -49 -101 -219 -112 -219
-10 0 11 56 47 128 51 102 49 113 -6 28 -25 -38 -45 -75 -45 -81 0 -7 -6 -18
-14 -24 -8 -7 -17 -26 -20 -44 -11 -59 -57 -190 -88 -252 -16 -33 -55 -94 -86
-135 -88 -116 -142 -201 -135 -212 3 -6 11 3 18 19 14 32 62 104 131 194 24
31 65 97 92 147 27 50 78 128 113 174 162 214 150 194 180 313 15 60 30 118
34 128 13 31 0 18 -47 -45z"/>
<path d="M1810 3250 c0 -13 5 -18 15 -14 12 5 13 10 4 20 -15 19 -19 18 -19
-6z"/>
<path d="M1784 3231 c5 -8 2 -11 -9 -9 -10 2 -20 -2 -22 -9 -3 -7 3 -9 17 -6
26 7 34 19 19 29 -8 5 -9 3 -5 -5z"/>
<path d="M1880 3210 c0 -13 5 -18 15 -14 12 5 13 10 4 20 -15 19 -19 18 -19
-6z"/>
<path d="M1174 3082 c8 -16 22 -35 32 -43 10 -8 5 4 -11 29 -32 48 -42 55 -21
14z"/>
<path d="M1807 3097 c-3 -8 3 -14 15 -14 16 -2 17 1 8 13 -15 17 -17 17 -23 1z"/>
<path d="M2305 3068 c-16 -25 -21 -37 -11 -29 16 14 51 71 43 71 -3 0 -17 -19
-32 -42z"/>
<path d="M1750 3066 c0 -8 -3 -22 -7 -32 -4 -11 -3 -15 4 -11 15 9 23 46 12
53 -5 3 -9 -1 -9 -10z"/>
<path d="M1750 2959 c0 -5 9 -9 20 -9 11 0 20 2 20 4 0 2 -9 6 -20 9 -11 3
-20 1 -20 -4z"/>
<path d="M1707 2799 c7 -7 15 -10 18 -7 3 3 -2 9 -12 12 -14 6 -15 5 -6 -5z"/>
<path d="M820 2720 c-13 -8 -12 -10 3 -10 9 0 17 5 17 10 0 12 -1 12 -20 0z"/>
<path d="M2660 2720 c0 -6 7 -10 15 -10 8 0 15 2 15 4 0 2 -7 6 -15 10 -8 3
-15 1 -15 -4z"/>
<path d="M471 2512 c8 -13 20 -17 37 -14 24 5 24 5 -8 18 -41 18 -42 18 -29
-4z"/>
<path d="M540 2519 c0 -5 9 -9 20 -9 11 0 20 2 20 4 0 2 -9 6 -20 9 -11 3 -20
1 -20 -4z"/>
<path d="M2935 2520 c-19 -8 -19 -9 3 -9 12 -1 22 4 22 9 0 6 -1 10 -2 9 -2 0
-12 -4 -23 -9z"/>
<path d="M2996 2516 l-29 -13 25 -5 c17 -3 29 1 37 14 13 21 7 21 -33 4z"/>
<path d="M580 1854 c-30 -7 -68 -17 -85 -22 -27 -9 -25 -10 25 -10 39 -1 49 2
35 8 -18 8 -18 9 5 10 28 2 114 28 90 28 -8 0 -40 -6 -70 -14z"/>
<path d="M2875 1855 c22 -8 51 -14 65 -15 23 -1 23 -2 5 -10 -14 -6 -4 -9 35
-8 50 0 52 1 25 9 -34 12 -157 40 -165 38 -3 -1 13 -7 35 -14z"/>
<path d="M700 1840 c-8 -5 -40 -10 -70 -11 -40 -1 -48 -3 -30 -8 22 -6 13 -12
-60 -40 -47 -18 -116 -52 -155 -76 -67 -41 -230 -173 -223 -180 4 -5 102 18
108 24 3 4 41 7 85 9 l80 2 -54 -32 c-72 -44 -202 -146 -198 -157 1 -4 -9 -18
-23 -29 -16 -14 -20 -21 -11 -22 8 0 23 7 33 17 14 13 18 13 18 2 1 -11 3 -11
13 1 25 32 134 81 151 68 2 -2 13 6 25 16 12 11 21 16 21 12 0 -5 17 2 38 13
l37 21 -35 -30 -35 -31 30 7 c38 9 146 98 186 154 24 33 27 41 13 36 -33 -13
-158 -46 -173 -46 -7 0 7 18 33 41 25 23 71 74 102 114 30 41 71 87 92 104 34
28 36 43 2 21z m-90 -95 c0 -3 -15 -11 -32 -20 -18 -8 -40 -19 -48 -25 -26
-17 -148 -69 -177 -75 l-28 -6 22 19 c13 11 48 28 78 37 57 18 75 32 28 21
-24 -5 -25 -5 -9 5 48 29 166 61 166 44z m-72 -84 c-6 -17 -90 -81 -112 -85
-48 -10 -139 -16 -133 -9 15 14 97 41 97 32 0 -6 16 2 35 17 19 14 35 22 35
16 0 -6 12 0 26 13 26 24 59 34 52 16z m-83 -171 c-3 -5 -14 -10 -24 -10 -10
0 -24 -7 -31 -15 -7 -8 -18 -15 -25 -15 -7 0 -18 -7 -25 -15 -7 -8 -23 -15
-35 -15 -13 0 -25 -3 -27 -7 -4 -11 -76 -43 -91 -42 -7 0 10 11 36 24 27 12
43 25 35 27 -7 3 2 8 20 12 17 4 32 11 32 16 0 4 8 11 18 14 9 3 28 13 42 21
28 17 85 20 75 5z"/>
<path d="M2802 1820 c21 -17 62 -64 92 -105 31 -40 77 -91 102 -114 26 -23 40
-41 33 -41 -15 0 -140 33 -173 46 -14 5 -11 -3 13 -36 40 -56 148 -145 186
-154 l30 -7 -35 31 -35 30 38 -21 c20 -11 37 -18 37 -13 0 4 8 0 18 -9 9 -8
20 -16 25 -16 42 1 130 -40 154 -71 10 -12 12 -12 13 -1 0 11 4 11 18 -2 10
-10 25 -17 33 -17 9 1 5 8 -11 22 -14 11 -24 25 -23 29 4 11 -126 113 -198
157 l-54 32 80 -2 c44 -2 82 -5 85 -9 6 -6 104 -29 108 -24 7 7 -156 139 -223
180 -38 24 -108 58 -155 76 -72 28 -81 34 -60 39 18 5 9 8 -35 12 -33 2 -69 8
-80 12 -11 4 -3 -7 17 -24z m220 -105 c29 -13 41 -22 26 -19 -47 10 -31 -2 27
-21 30 -9 65 -26 78 -37 l22 -19 -28 6 c-29 6 -151 58 -177 75 -8 6 -30 17
-47 25 -18 9 -33 18 -33 21 0 12 80 -6 132 -31z m-8 -70 c14 -13 26 -19 26
-13 0 6 16 -2 35 -16 19 -15 35 -23 35 -17 0 9 82 -18 97 -32 6 -7 -85 -1
-133 9 -22 4 -106 68 -112 85 -7 18 26 8 52 -16z m106 -160 c14 -8 33 -18 43
-21 9 -3 17 -10 17 -14 0 -5 15 -12 33 -16 17 -4 26 -9 19 -12 -8 -2 8 -15 35
-27 26 -13 43 -24 36 -24 -15 -1 -87 31 -91 42 -2 4 -14 7 -27 7 -12 0 -28 7
-35 15 -7 8 -18 15 -25 15 -7 0 -18 7 -25 15 -7 8 -21 15 -31 15 -10 0 -21 5
-24 10 -10 15 47 12 75 -5z"/>
<path d="M1246 1578 c-43 -51 -83 -104 -90 -118 -16 -29 -37 -92 -33 -96 5 -6
105 131 126 173 11 23 29 48 38 55 15 11 48 78 39 78 -2 0 -38 -42 -80 -92z"/>
<path d="M2183 1640 c6 -19 20 -41 30 -48 9 -7 27 -32 38 -55 21 -42 121 -179
126 -173 3 3 -16 61 -30 93 -9 19 -120 158 -163 203 -12 12 -12 9 -1 -20z"/>
<path d="M980 1637 c-50 -19 -70 -31 -70 -39 0 -5 9 -4 19 2 11 5 32 15 47 20
25 10 26 9 13 -7 -22 -28 -42 -42 -61 -43 -25 0 -33 -20 -8 -20 26 0 93 61 89
82 -3 12 -9 13 -29 5z"/>
<path d="M2490 1631 c0 -24 64 -81 91 -81 24 0 15 20 -9 20 -19 1 -39 15 -61
43 -13 16 -12 17 13 7 15 -5 36 -15 47 -20 10 -6 19 -7 19 -2 0 9 -19 19 -72
41 -25 9 -28 8 -28 -8z"/>
<path d="M1286 1573 c-6 -14 -5 -15 5 -6 7 7 10 15 7 18 -3 3 -9 -2 -12 -12z"/>
<path d="M2200 1581 c0 -6 4 -13 10 -16 6 -3 7 1 4 9 -7 18 -14 21 -14 7z"/>
<path d="M930 1535 c-7 -9 -11 -18 -8 -20 3 -3 11 1 18 10 7 9 11 18 8 20 -3
3 -11 -1 -18 -10z"/>
<path d="M2550 1542 c0 -5 7 -15 15 -22 8 -7 15 -8 15 -2 0 5 -7 15 -15 22 -8
7 -15 8 -15 2z"/>
<path d="M555 1290 l-40 -19 42 0 c41 -1 64 13 47 29 -5 4 -27 -1 -49 -10z"/>
<path d="M2894 1298 c-11 -18 12 -29 52 -26 l39 3 -42 17 c-27 10 -45 13 -49
6z"/>
<path d="M1431 538 c17 -34 99 -148 106 -148 3 0 -2 10 -10 23 -9 12 -35 50
-59 85 -43 64 -58 79 -37 40z"/>
<path d="M2032 498 c-24 -35 -50 -73 -59 -85 -8 -13 -13 -23 -10 -23 7 0 89
114 106 148 21 39 6 24 -37 -40z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 36 KiB

View file

@ -0,0 +1,19 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-256x256.png",
"sizes": "256x256",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

BIN
app/static/img/rp/.DS_Store vendored Executable file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
app/static/img/rp/rpwebsearch.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
app/static/img/rplogo.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

127
app/static/js/autocomplete.js Executable file
View file

@ -0,0 +1,127 @@
let searchInput;
let currentFocus;
let originalSearch;
let autocompleteResults;
const handleUserInput = () => {
let xhrRequest = new XMLHttpRequest();
xhrRequest.open("POST", "autocomplete");
xhrRequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhrRequest.onload = function () {
if (xhrRequest.readyState === 4 && xhrRequest.status !== 200) {
// Do nothing if failed to fetch autocomplete results
return;
}
// Fill autocomplete with fetched results
autocompleteResults = JSON.parse(xhrRequest.responseText)[1];
updateAutocompleteList();
};
xhrRequest.send('q=' + searchInput.value);
};
const removeActive = suggestion => {
// Remove "autocomplete-active" class from previously active suggestion
for (let i = 0; i < suggestion.length; i++) {
suggestion[i].classList.remove("autocomplete-active");
}
};
const addActive = (suggestion) => {
// Handle navigation outside of suggestion list
if (!suggestion || !suggestion[currentFocus]) {
if (currentFocus >= suggestion.length) {
// Move selection back to the beginning
currentFocus = 0;
} else if (currentFocus < 0) {
// Retrieve original search and remove active suggestion selection
currentFocus = -1;
searchInput.value = originalSearch;
removeActive(suggestion);
return;
} else {
return;
}
}
removeActive(suggestion);
suggestion[currentFocus].classList.add("autocomplete-active");
// Autofill search bar with suggestion content (minus the "bang name" if using a bang operator)
let searchContent = suggestion[currentFocus].textContent;
if (searchContent.indexOf('(') > 0) {
searchInput.value = searchContent.substring(0, searchContent.indexOf('('));
} else {
searchInput.value = searchContent;
}
searchInput.focus();
};
const autocompleteInput = (e) => {
// Handle navigation between autocomplete suggestions
let suggestion = document.getElementById("autocomplete-list");
if (suggestion) suggestion = suggestion.getElementsByTagName("div");
if (e.keyCode === 40) { // down
e.preventDefault();
currentFocus++;
addActive(suggestion);
} else if (e.keyCode === 38) { //up
e.preventDefault();
currentFocus--;
addActive(suggestion);
} else if (e.keyCode === 13) { // enter
e.preventDefault();
if (currentFocus > -1) {
if (suggestion) suggestion[currentFocus].click();
}
} else {
originalSearch = searchInput.value;
}
};
const updateAutocompleteList = () => {
let autocompleteItem, i;
let val = originalSearch;
let autocompleteList = document.getElementById("autocomplete-list");
autocompleteList.innerHTML = "";
if (!val || !autocompleteResults) {
return false;
}
currentFocus = -1;
for (i = 0; i < autocompleteResults.length; i++) {
if (autocompleteResults[i].substr(0, val.length).toUpperCase() === val.toUpperCase()) {
autocompleteItem = document.createElement("div");
autocompleteItem.setAttribute("class", "autocomplete-item");
autocompleteItem.innerHTML = "<strong>" + autocompleteResults[i].substr(0, val.length) + "</strong>";
autocompleteItem.innerHTML += autocompleteResults[i].substr(val.length);
autocompleteItem.innerHTML += "<input type=\"hidden\" value=\"" + autocompleteResults[i] + "\">";
autocompleteItem.addEventListener("click", function () {
searchInput.value = this.getElementsByTagName("input")[0].value;
autocompleteList.innerHTML = "";
document.getElementById("search-form").submit();
});
autocompleteList.appendChild(autocompleteItem);
}
}
};
document.addEventListener("DOMContentLoaded", function() {
let autocompleteList = document.createElement("div");
autocompleteList.setAttribute("id", "autocomplete-list");
autocompleteList.setAttribute("class", "autocomplete-items");
searchInput = document.getElementById("search-bar");
searchInput.parentNode.appendChild(autocompleteList);
searchInput.addEventListener("keydown", (event) => autocompleteInput(event));
document.addEventListener("click", function (e) {
autocompleteList.innerHTML = "";
});
});

88
app/static/js/controller.js Executable file
View file

@ -0,0 +1,88 @@
const setupSearchLayout = () => {
// Setup search field
const searchBar = document.getElementById("search-bar");
const searchBtn = document.getElementById("search-submit");
const arrowKeys = [37, 38, 39, 40];
let searchValue = searchBar.value;
// Automatically focus on search field
searchBar.focus();
searchBar.select();
searchBar.addEventListener("keyup", function(event) {
if (event.keyCode === 13) {
event.preventDefault();
searchBtn.click();
} else if (searchBar.value !== searchValue && !arrowKeys.includes(event.keyCode)) {
searchValue = searchBar.value;
handleUserInput();
}
});
};
const setupConfigLayout = () => {
// Setup whoogle config
const collapsible = document.getElementById("config-collapsible");
collapsible.addEventListener("click", function() {
this.classList.toggle("active");
let content = this.nextElementSibling;
if (content.style.maxHeight) {
content.style.maxHeight = null;
} else {
content.style.maxHeight = "400px";
}
content.classList.toggle("open");
});
};
const loadConfig = event => {
event.preventDefault();
let config = prompt("Enter name of config:");
if (!config) {
alert("Must specify a name for the config to load");
return;
}
let xhrPUT = new XMLHttpRequest();
xhrPUT.open("PUT", "config?name=" + config + ".conf");
xhrPUT.onload = function() {
if (xhrPUT.readyState === 4 && xhrPUT.status !== 200) {
alert("Error loading Whoogle config");
return;
}
location.reload(true);
};
xhrPUT.send();
};
const saveConfig = event => {
event.preventDefault();
let config = prompt("Enter name for this config:");
if (!config) {
alert("Must specify a name for the config to save");
return;
}
let configForm = document.getElementById("config-form");
configForm.action = 'config?name=' + config + ".conf";
configForm.submit();
};
document.addEventListener("DOMContentLoaded", function() {
setTimeout(function() {
document.getElementById("main");
}, 100);
setupSearchLayout();
setupConfigLayout();
document.getElementById("config-load").addEventListener("click", loadConfig);
document.getElementById("config-save").addEventListener("click", saveConfig);
// Focusing on the search input field requires a delay for elements to finish
// loading (seemingly only on FF)
setTimeout(function() { document.getElementById("search-bar").focus(); }, 250);
});

9
app/static/js/currency.js Executable file
View file

@ -0,0 +1,9 @@
const convert = (n1, n2, conversionFactor) => {
// id's for currency input boxes
let id1 = "cb" + n1;
let id2 = "cb" + n2;
// getting the value of the input box that just got filled
let inputBox = document.getElementById(id1).value;
// updating the other input box after conversion
document.getElementById(id2).value = ((inputBox * conversionFactor).toFixed(2));
}

67
app/static/js/header.js Executable file
View file

@ -0,0 +1,67 @@
document.addEventListener("DOMContentLoaded", () => {
const advSearchToggle = document.getElementById("adv-search-toggle");
const advSearchDiv = document.getElementById("adv-search-div");
const searchBar = document.getElementById("search-bar");
const countrySelect = document.getElementById("result-country");
const timePeriodSelect = document.getElementById("result-time-period");
const arrowKeys = [37, 38, 39, 40];
let searchValue = searchBar.value;
countrySelect.onchange = () => {
let str = window.location.href;
n = str.lastIndexOf("/search");
if (n > 0) {
str = str.substring(0, n) + `/search?q=${searchBar.value}`;
str = tackOnParams(str);
window.location.href = str;
}
}
timePeriodSelect.onchange = () => {
let str = window.location.href;
n = str.lastIndexOf("/search");
if (n > 0) {
str = str.substring(0, n) + `/search?q=${searchBar.value}`;
str = tackOnParams(str);
window.location.href = str;
}
}
function tackOnParams(str) {
if (timePeriodSelect.value != "") {
str = str + `&tbs=${timePeriodSelect.value}`;
}
if (countrySelect.value != "") {
str = str + `&country=${countrySelect.value}`;
}
return str;
}
const toggleAdvancedSearch = on => {
if (on) {
advSearchDiv.style.maxHeight = "70px";
} else {
advSearchDiv.style.maxHeight = "0px";
}
localStorage.advSearchToggled = on;
}
try {
toggleAdvancedSearch(JSON.parse(localStorage.advSearchToggled));
} catch (error) {
console.warn("Did not recover advanced search toggle state");
}
advSearchToggle.onclick = () => {
toggleAdvancedSearch(advSearchToggle.checked);
}
searchBar.addEventListener("keyup", function(event) {
if (event.keyCode === 13) {
document.getElementById("search-form").submit();
} else if (searchBar.value !== searchValue && !arrowKeys.includes(event.keyCode)) {
searchValue = searchBar.value;
handleUserInput();
}
});
});

62
app/static/js/keyboard.js Executable file
View file

@ -0,0 +1,62 @@
(function () {
let searchBar, results;
let shift = false;
const keymap = {
ArrowUp: goUp,
ArrowDown: goDown,
ShiftTab: goUp,
Tab: goDown,
k: goUp,
j: goDown,
'/': focusSearch,
};
let activeIdx = -1;
document.addEventListener('DOMContentLoaded', () => {
searchBar = document.querySelector('#search-bar');
results = document.querySelectorAll('#main>div>div>div>a');
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Shift') {
shift = true;
}
if (e.target.tagName === 'INPUT') return true;
if (typeof keymap[e.key] === 'function') {
e.preventDefault();
keymap[`${shift && e.key == 'Tab' ? 'Shift' : ''}${e.key}`]();
}
});
document.addEventListener('keyup', (e) => {
if (e.key === 'Shift') {
shift = false;
}
});
function goUp () {
if (activeIdx > 0) focusResult(activeIdx - 1);
else focusSearch();
}
function goDown () {
if (activeIdx < results.length - 1) focusResult(activeIdx + 1);
}
function focusResult (idx) {
activeIdx = idx;
results[activeIdx].scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
results[activeIdx].focus();
}
function focusSearch () {
if (window.usingCalculator) {
// if this function exists, it means the calculator widget has been displayed
if (usingCalculator()) return;
}
activeIdx = -1;
searchBar.focus();
}
}());

76
app/static/js/utils.js Executable file
View file

@ -0,0 +1,76 @@
const checkForTracking = () => {
const mainDiv = document.getElementById("main");
const searchBar = document.getElementById("search-bar");
// some pages (e.g. images) do not have these
if (!mainDiv || !searchBar)
return;
const query = searchBar.value.replace(/\s+/g, '');
// Note: regex functions for checking for tracking queries were derived
// from here -- https://stackoverflow.com/questions/619977
const matchTracking = {
"ups": {
"link": `https://www.ups.com/track?tracknum=${query}`,
"expr": [
/\b(1Z ?[0-9A-Z]{3} ?[0-9A-Z]{3} ?[0-9A-Z]{2} ?[0-9A-Z]{4} ?[0-9A-Z]{3} ?[0-9A-Z]|[\dT]\d\d\d ?\d\d\d\d ?\d\d\d)\b/
]
},
"usps": {
"link": `https://tools.usps.com/go/TrackConfirmAction_input?origTrackNum=${query}`,
"expr": [
/(\b\d{30}\b)|(\b91\d+\b)|(\b\d{20}\b)/,
/^E\D{1}\d{9}\D{2}$|^9\d{15,21}$/,
/^91[0-9]+$/,
/^[A-Za-z]{2}[0-9]+US$/
]
},
"fedex": {
"link": `https://www.fedex.com/apps/fedextrack/?tracknumbers=${query}`,
"expr": [
/(\b96\d{20}\b)|(\b\d{15}\b)|(\b\d{12}\b)/,
/\b((98\d\d\d\d\d?\d\d\d\d|98\d\d) ?\d\d\d\d ?\d\d\d\d( ?\d\d\d)?)\b/,
/^[0-9]{15}$/
]
}
};
// Creates a link to a UPS/USPS/FedEx tracking page
const createTrackingLink = href => {
let link = document.createElement("a");
link.className = "tracking-link";
link.innerHTML = "View Tracking Info";
link.href = href;
mainDiv.prepend(link);
};
// Compares the query against a set of regex patterns
// for tracking numbers
const compareQuery = provider => {
provider.expr.some(regex => {
if (query.match(regex)) {
createTrackingLink(provider.link);
return true;
}
});
};
for (const key of Object.keys(matchTracking)) {
compareQuery(matchTracking[key]);
}
};
document.addEventListener("DOMContentLoaded", function() {
checkForTracking();
// Clear input if reset button tapped
const searchBar = document.getElementById("search-bar");
const resetBtn = document.getElementById("search-reset");
// some pages (e.g. images) do not have these
if (!searchBar || !resetBtn)
return;
resetBtn.addEventListener("click", event => {
event.preventDefault();
searchBar.value = "";
searchBar.focus();
});
});

View file

@ -0,0 +1,978 @@
[
{
"name": "-------",
"value": ""
},
{
"name": "Hungary",
"value": "HU"
},
{
"name": "Afghanistan",
"value": "AF"
},
{
"name": "Albania",
"value": "AL"
},
{
"name": "Algeria",
"value": "DZ"
},
{
"name": "American Samoa",
"value": "AS"
},
{
"name": "Andorra",
"value": "AD"
},
{
"name": "Angola",
"value": "AO"
},
{
"name": "Anguilla",
"value": "AI"
},
{
"name": "Antarctica",
"value": "AQ"
},
{
"name": "Antigua and Barbuda",
"value": "AG"
},
{
"name": "Argentina",
"value": "AR"
},
{
"name": "Armenia",
"value": "AM"
},
{
"name": "Aruba",
"value": "AW"
},
{
"name": "Australia",
"value": "AU"
},
{
"name": "Austria",
"value": "AT"
},
{
"name": "Azerbaijan",
"value": "AZ"
},
{
"name": "Bahamas",
"value": "BS"
},
{
"name": "Bahrain",
"value": "BH"
},
{
"name": "Bangladesh",
"value": "BD"
},
{
"name": "Barbados",
"value": "BB"
},
{
"name": "Belarus",
"value": "BY"
},
{
"name": "Belgium",
"value": "BE"
},
{
"name": "Belize",
"value": "BZ"
},
{
"name": "Benin",
"value": "BJ"
},
{
"name": "Bermuda",
"value": "BM"
},
{
"name": "Bhutan",
"value": "BT"
},
{
"name": "Bolivia",
"value": "BO"
},
{
"name": "Bosnia and Herzegovina",
"value": "BA"
},
{
"name": "Botswana",
"value": "BW"
},
{
"name": "Bouvet Island",
"value": "BV"
},
{
"name": "Brazil",
"value": "BR"
},
{
"name": "British Indian Ocean Territory",
"value": "IO"
},
{
"name": "Brunei Darussalam",
"value": "BN"
},
{
"name": "Bulgaria",
"value": "BG"
},
{
"name": "Burkina Faso",
"value": "BF"
},
{
"name": "Burundi",
"value": "BI"
},
{
"name": "Cambodia",
"value": "KH"
},
{
"name": "Cameroon",
"value": "CM"
},
{
"name": "Canada",
"value": "CA"
},
{
"name": "Cape Verde",
"value": "CV"
},
{
"name": "Cayman Islands",
"value": "KY"
},
{
"name": "Central African Republic",
"value": "CF"
},
{
"name": "Chad",
"value": "TD"
},
{
"name": "Chile",
"value": "CL"
},
{
"name": "China",
"value": "CN"
},
{
"name": "Christmas Island",
"value": "CX"
},
{
"name": "Cocos (Keeling) Islands",
"value": "CC"
},
{
"name": "Colombia",
"value": "CO"
},
{
"name": "Comoros",
"value": "KM"
},
{
"name": "Congo",
"value": "CG"
},
{
"name": "Congo, Democratic Republic of the",
"value": "CD"
},
{
"name": "Cook Islands",
"value": "CK"
},
{
"name": "Costa Rica",
"value": "CR"
},
{
"name": "Cote D'ivoire",
"value": "CI"
},
{
"name": "Croatia (Hrvatska)",
"value": "HR"
},
{
"name": "Cuba",
"value": "CU"
},
{
"name": "Cyprus",
"value": "CY"
},
{
"name": "Czech Republic",
"value": "CZ"
},
{
"name": "Denmark",
"value": "DK"
},
{
"name": "Djibouti",
"value": "DJ"
},
{
"name": "Dominica",
"value": "DM"
},
{
"name": "Dominican Republic",
"value": "DO"
},
{
"name": "East Timor",
"value": "TP"
},
{
"name": "Ecuador",
"value": "EC"
},
{
"name": "Egypt",
"value": "EG"
},
{
"name": "El Salvador",
"value": "SV"
},
{
"name": "Equatorial Guinea",
"value": "GQ"
},
{
"name": "Eritrea",
"value": "ER"
},
{
"name": "Estonia",
"value": "EE"
},
{
"name": "Ethiopia",
"value": "ET"
},
{
"name": "European Union",
"value": "EU"
},
{
"name": "Falkland Islands (Malvinas)",
"value": "FK"
},
{
"name": "Faroe Islands",
"value": "FO"
},
{
"name": "Fiji",
"value": "FJ"
},
{
"name": "Finland",
"value": "FI"
},
{
"name": "France",
"value": "FR"
},
{
"name": "France, Metropolitan",
"value": "FX"
},
{
"name": "French Guiana",
"value": "GF"
},
{
"name": "French Polynesia",
"value": "PF"
},
{
"name": "French Southern Territories",
"value": "TF"
},
{
"name": "Gabon",
"value": "GA"
},
{
"name": "Gambia",
"value": "GM"
},
{
"name": "Georgia",
"value": "GE"
},
{
"name": "Germany",
"value": "DE"
},
{
"name": "Ghana",
"value": "GH"
},
{
"name": "Gibraltar",
"value": "GI"
},
{
"name": "Greece",
"value": "GR"
},
{
"name": "Greenland",
"value": "GL"
},
{
"name": "Grenada",
"value": "GD"
},
{
"name": "Guadeloupe",
"value": "GP"
},
{
"name": "Guam",
"value": "GU"
},
{
"name": "Guatemala",
"value": "GT"
},
{
"name": "Guinea",
"value": "GN"
},
{
"name": "Guinea-Bissau",
"value": "GW"
},
{
"name": "Guyana",
"value": "GY"
},
{
"name": "Haiti",
"value": "HT"
},
{
"name": "Heard Island and Mcdonald Islands",
"value": "HM"
},
{
"name": "Holy See (Vatican City State)",
"value": "VA"
},
{
"name": "Honduras",
"value": "HN"
},
{
"name": "Hong Kong",
"value": "HK"
},
{
"name": "Hungary",
"value": "HU"
},
{
"name": "Iceland",
"value": "IS"
},
{
"name": "India",
"value": "IN"
},
{
"name": "Indonesia",
"value": "ID"
},
{
"name": "Iran, Islamic Republic of",
"value": "IR"
},
{
"name": "Iraq",
"value": "IQ"
},
{
"name": "Ireland",
"value": "IE"
},
{
"name": "Israel",
"value": "IL"
},
{
"name": "Italy",
"value": "IT"
},
{
"name": "Jamaica",
"value": "JM"
},
{
"name": "Japan",
"value": "JP"
},
{
"name": "Jordan",
"value": "JO"
},
{
"name": "Kazakhstan",
"value": "KZ"
},
{
"name": "Kenya",
"value": "KE"
},
{
"name": "Kiribati",
"value": "KI"
},
{
"name": "Korea, Democratic People's Republic of",
"value": "KP"
},
{
"name": "Korea, Republic of",
"value": "KR"
},
{
"name": "Kuwait",
"value": "KW"
},
{
"name": "Kyrgyzstan",
"value": "KG"
},
{
"name": "Lao People's Democratic Republic",
"value": "LA"
},
{
"name": "Latvia",
"value": "LV"
},
{
"name": "Lebanon",
"value": "LB"
},
{
"name": "Lesotho",
"value": "LS"
},
{
"name": "Liberia",
"value": "LR"
},
{
"name": "Libyan Arab Jamahiriya",
"value": "LY"
},
{
"name": "Liechtenstein",
"value": "LI"
},
{
"name": "Lithuania",
"value": "LT"
},
{
"name": "Luxembourg",
"value": "LU"
},
{
"name": "Macao",
"value": "MO"
},
{
"name": "Macedonia, the Former Yugosalv Republic of",
"value": "MK"
},
{
"name": "Madagascar",
"value": "MG"
},
{
"name": "Malawi",
"value": "MW"
},
{
"name": "Malaysia",
"value": "MY"
},
{
"name": "Maldives",
"value": "MV"
},
{
"name": "Mali",
"value": "ML"
},
{
"name": "Malta",
"value": "MT"
},
{
"name": "Marshall Islands",
"value": "MH"
},
{
"name": "Martinique",
"value": "MQ"
},
{
"name": "Mauritania",
"value": "MR"
},
{
"name": "Mauritius",
"value": "MU"
},
{
"name": "Mayotte",
"value": "YT"
},
{
"name": "Mexico",
"value": "MX"
},
{
"name": "Micronesia, Federated States of",
"value": "FM"
},
{
"name": "Moldova, Republic of",
"value": "MD"
},
{
"name": "Monaco",
"value": "MC"
},
{
"name": "Mongolia",
"value": "MN"
},
{
"name": "Montserrat",
"value": "MS"
},
{
"name": "Morocco",
"value": "MA"
},
{
"name": "Mozambique",
"value": "MZ"
},
{
"name": "Myanmar",
"value": "MM"
},
{
"name": "Namibia",
"value": "NA"
},
{
"name": "Nauru",
"value": "NR"
},
{
"name": "Nepal",
"value": "NP"
},
{
"name": "Netherlands",
"value": "NL"
},
{
"name": "Netherlands Antilles",
"value": "AN"
},
{
"name": "New Caledonia",
"value": "NC"
},
{
"name": "New Zealand",
"value": "NZ"
},
{
"name": "Nicaragua",
"value": "NI"
},
{
"name": "Niger",
"value": "NE"
},
{
"name": "Nigeria",
"value": "NG"
},
{
"name": "Niue",
"value": "NU"
},
{
"name": "Norfolk Island",
"value": "NF"
},
{
"name": "Northern Mariana Islands",
"value": "MP"
},
{
"name": "Norway",
"value": "NO"
},
{
"name": "Oman",
"value": "OM"
},
{
"name": "Pakistan",
"value": "PK"
},
{
"name": "Palau",
"value": "PW"
},
{
"name": "Palestinian Territory",
"value": "PS"
},
{
"name": "Panama",
"value": "PA"
},
{
"name": "Papua New Guinea",
"value": "PG"
},
{
"name": "Paraguay",
"value": "PY"
},
{
"name": "Peru",
"value": "PE"
},
{
"name": "Philippines",
"value": "PH"
},
{
"name": "Pitcairn",
"value": "PN"
},
{
"name": "Poland",
"value": "PL"
},
{
"name": "Portugal",
"value": "PT"
},
{
"name": "Puerto Rico",
"value": "PR"
},
{
"name": "Qatar",
"value": "QA"
},
{
"name": "Reunion",
"value": "RE"
},
{
"name": "Romania",
"value": "RO"
},
{
"name": "Russian Federation",
"value": "RU"
},
{
"name": "Rwanda",
"value": "RW"
},
{
"name": "Saint Helena",
"value": "SH"
},
{
"name": "Saint Kitts and Nevis",
"value": "KN"
},
{
"name": "Saint Lucia",
"value": "LC"
},
{
"name": "Saint Pierre and Miquelon",
"value": "PM"
},
{
"name": "Saint Vincent and the Grenadines",
"value": "VC"
},
{
"name": "Samoa",
"value": "WS"
},
{
"name": "San Marino",
"value": "SM"
},
{
"name": "Sao Tome and Principe",
"value": "ST"
},
{
"name": "Saudi Arabia",
"value": "SA"
},
{
"name": "Senegal",
"value": "SN"
},
{
"name": "Serbia and Montenegro",
"value": "CS"
},
{
"name": "Seychelles",
"value": "SC"
},
{
"name": "Sierra Leone",
"value": "SL"
},
{
"name": "Singapore",
"value": "SG"
},
{
"name": "Slovakia",
"value": "SK"
},
{
"name": "Slovenia",
"value": "SI"
},
{
"name": "Solomon Islands",
"value": "SB"
},
{
"name": "Somalia",
"value": "SO"
},
{
"name": "South Africa",
"value": "ZA"
},
{
"name": "South Georgia and the South Sandwich Islands",
"value": "GS"
},
{
"name": "Spain",
"value": "ES"
},
{
"name": "Sri Lanka",
"value": "LK"
},
{
"name": "Sudan",
"value": "SD"
},
{
"name": "Suriname",
"value": "SR"
},
{
"name": "Svalbard and Jan Mayen",
"value": "SJ"
},
{
"name": "Swaziland",
"value": "SZ"
},
{
"name": "Sweden",
"value": "SE"
},
{
"name": "Switzerland",
"value": "CH"
},
{
"name": "Syrian Arab Republic",
"value": "SY"
},
{
"name": "Taiwan",
"value": "TW"
},
{
"name": "Tajikistan",
"value": "TJ"
},
{
"name": "Tanzania, United Republic of",
"value": "TZ"
},
{
"name": "Thailand",
"value": "TH"
},
{
"name": "Togo",
"value": "TG"
},
{
"name": "Tokelau",
"value": "TK"
},
{
"name": "Tonga",
"value": "TO"
},
{
"name": "Trinidad and Tobago",
"value": "TT"
},
{
"name": "Tunisia",
"value": "TN"
},
{
"name": "Turkey",
"value": "TR"
},
{
"name": "Turkmenistan",
"value": "TM"
},
{
"name": "Turks and Caicos Islands",
"value": "TC"
},
{
"name": "Tuvalu",
"value": "TV"
},
{
"name": "Uganda",
"value": "UG"
},
{
"name": "Ukraine",
"value": "UA"
},
{
"name": "United Arab Emirates",
"value": "AE"
},
{
"name": "United Kingdom",
"value": "UK"
},
{
"name": "United States",
"value": "US"
},
{
"name": "United States Minor Outlying Islands",
"value": "UM"
},
{
"name": "Uruguay",
"value": "UY"
},
{
"name": "Uzbekistan",
"value": "UZ"
},
{
"name": "Vanuatu",
"value": "VU"
},
{
"name": "Venezuela",
"value": "VE"
},
{
"name": "Vietnam",
"value": "VN"
},
{
"name": "Virgin Islands, British",
"value": "VG"
},
{
"name": "Virgin Islands, U.S.",
"value": "VI"
},
{
"name": "Wallis and Futuna",
"value": "WF"
},
{
"name": "Western Sahara",
"value": "EH"
},
{
"name": "Yemen",
"value": "YE"
},
{
"name": "Yugoslavia",
"value": "YU"
},
{
"name": "Zambia",
"value": "ZM"
},
{
"name": "Zimbabwe",
"value": "ZW"
}
]

View file

@ -0,0 +1,32 @@
{
"all": {
"tbm": null,
"href": "search?q={query}",
"name": "All",
"selected": true
},
"images": {
"tbm": "isch",
"href": "search?q={query}",
"name": "Images",
"selected": false
},
"maps": {
"tbm": null,
"href": "https://www.openstreetmap.org/search?query={map_query}",
"name": "Maps",
"selected": false
},
"videos": {
"tbm": "vid",
"href": "search?q={query}",
"name": "Videos",
"selected": false
},
"news": {
"tbm": "nws",
"href": "search?q={query}",
"name": "News",
"selected": false
}
}

View file

@ -0,0 +1,210 @@
[
{
"name": "English",
"value": "lang_en"
},
{
"name": "Hungarian (Magyar)",
"value": "lang_hu"
},
{
"name": "Afrikaans (Afrikaans)",
"value": "lang_af"
},
{
"name": "Arabic (عربى)",
"value": "lang_ar"
},
{
"name": "Armenian (հայերեն)",
"value": "lang_hy"
},
{
"name": "Azerbaijani (Azərbaycanca)",
"value": "lang_az"
},
{
"name": "Belarusian (Беларуская)",
"value": "lang_be"
},
{
"name": "Bulgarian (български)",
"value": "lang_bg"
},
{
"name": "Catalan (Català)",
"value": "lang_ca"
},
{
"name": "Chinese, Simplified (简体中文)",
"value": "lang_zh-CN"
},
{
"name": "Chinese, Traditional (正體中文)",
"value": "lang_zh-TW"
},
{
"name": "Croatian (Hrvatski)",
"value": "lang_hr"
},
{
"name": "Czech (čeština)",
"value": "lang_cs"
},
{
"name": "Danish (Dansk)",
"value": "lang_da"
},
{
"name": "Dutch (Nederlands)",
"value": "lang_nl"
},
{
"name": "Esperanto (Esperanto)",
"value": "lang_eo"
},
{
"name": "Estonian (Eestlane)",
"value": "lang_et"
},
{
"name": "Filipino (Pilipino)",
"value": "lang_tl"
},
{
"name": "Finnish (Suomalainen)",
"value": "lang_fi"
},
{
"name": "French (Français)",
"value": "lang_fr"
},
{
"name": "German (Deutsch)",
"value": "lang_de"
},
{
"name": "Greek (Ελληνικά)",
"value": "lang_el"
},
{
"name": "Hebrew (עִברִית)",
"value": "lang_iw"
},
{
"name": "Hindi (हिंदी)",
"value": "lang_hi"
},
{
"name": "Icelandic (Íslenska)",
"value": "lang_is"
},
{
"name": "Indonesian (Indonesian)",
"value": "lang_id"
},
{
"name": "Italian (Italiano)",
"value": "lang_it"
},
{
"name": "Japanese (日本語)",
"value": "lang_ja"
},
{
"name": "Korean (한국어)",
"value": "lang_ko"
},
{
"name": "Kurdish (Kurdî)",
"value": "lang_ku"
},
{
"name": "Latvian (Latvietis)",
"value": "lang_lv"
},
{
"name": "Lithuanian (Lietuvis)",
"value": "lang_lt"
},
{
"name": "Norwegian (Norwegian)",
"value": "lang_no"
},
{
"name": "Persian (فارسی)",
"value": "lang_fa"
},
{
"name": "Polish (Polskie)",
"value": "lang_pl"
},
{
"name": "Portuguese (Português)",
"value": "lang_pt"
},
{
"name": "Romanian (Română)",
"value": "lang_ro"
},
{
"name": "Russian (русский)",
"value": "lang_ru"
},
{
"name": "Serbian (Српски)",
"value": "lang_sr"
},
{
"name": "Sinhala (සිංහල)",
"value": "lang_si"
},
{
"name": "Slovak (Slovák)",
"value": "lang_sk"
},
{
"name": "Slovenian (Slovenščina)",
"value": "lang_sl"
},
{
"name": "Spanish (Español)",
"value": "lang_es"
},
{
"name": "Swahili (Kiswahili)",
"value": "lang_sw"
},
{
"name": "Swedish (Svenska)",
"value": "lang_sv"
},
{
"name": "Thai (ไทย)",
"value": "lang_th"
},
{
"name": "Turkish (Türk)",
"value": "lang_tr"
},
{
"name": "Ukrainian (Український)",
"value": "lang_uk"
},
{
"name": "Vietnamese (Tiếng Việt)",
"value": "lang_vi"
},
{
"name": "Welsh (Cymraeg)",
"value": "lang_cy"
},
{
"name": "Xhosa (isiXhosa)",
"value": "lang_xh"
},
{
"name": "Zulu (isiZulu)",
"value": "lang_zu"
}
]

View file

@ -0,0 +1,5 @@
[
"light",
"dark",
"system"
]

View file

@ -0,0 +1,8 @@
[
{"name": "Any time", "value": ""},
{"name": "Past hour", "value": "qdr:h"},
{"name": "Past 24 hours", "value": "qdr:d"},
{"name": "Past week", "value": "qdr:w"},
{"name": "Past month", "value": "qdr:m"},
{"name": "Past year", "value": "qdr:y"}
]

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,260 @@
<!--
Calculator widget.
This file should contain all required
CSS, HTML, and JS for it.
-->
<style>
#calc-text {
background: var(--whoogle-dark-page-bg);
padding: 8px;
border-radius: 8px;
text-align: right;
font-family: monospace;
font-size: 16px;
color: var(--whoogle-dark-text);
}
#prev-equation {
text-align: right;
}
.error-border {
border: 1px solid red;
}
#calc-btns {
display: grid;
grid-template-columns: repeat(6, 1fr);
grid-template-rows: repeat(5, 1fr);
gap: 5px;
}
#calc-btns button {
background: #313141;
color: var(--whoogle-dark-text);
border: none;
border-radius: 8px;
padding: 8px;
cursor: pointer;
}
#calc-btns button:hover {
background: #414151;
}
#calc-btns .common {
background: #51516a;
}
#calc-btns .common:hover {
background: #61617a;
}
#calc-btn-0 { grid-row: 5; grid-column: 3; }
#calc-btn-1 { grid-row: 4; grid-column: 3; }
#calc-btn-2 { grid-row: 4; grid-column: 4; }
#calc-btn-3 { grid-row: 4; grid-column: 5; }
#calc-btn-4 { grid-row: 3; grid-column: 3; }
#calc-btn-5 { grid-row: 3; grid-column: 4; }
#calc-btn-6 { grid-row: 3; grid-column: 5; }
#calc-btn-7 { grid-row: 2; grid-column: 3; }
#calc-btn-8 { grid-row: 2; grid-column: 4; }
#calc-btn-9 { grid-row: 2; grid-column: 5; }
#calc-btn-EQ { grid-row: 5; grid-column: 5; }
#calc-btn-PT { grid-row: 5; grid-column: 4; }
#calc-btn-BCK { grid-row: 5; grid-column: 6; }
#calc-btn-ADD { grid-row: 4; grid-column: 6; }
#calc-btn-SUB { grid-row: 3; grid-column: 6; }
#calc-btn-MLT { grid-row: 2; grid-column: 6; }
#calc-btn-DIV { grid-row: 1; grid-column: 6; }
#calc-btn-CLR { grid-row: 1; grid-column: 5; }
#calc-btn-PRC{ grid-row: 1; grid-column: 4; }
#calc-btn-RP { grid-row: 1; grid-column: 3; }
#calc-btn-LP { grid-row: 1; grid-column: 2; }
#calc-btn-ABS { grid-row: 1; grid-column: 1; }
#calc-btn-SIN { grid-row: 2; grid-column: 2; }
#calc-btn-COS { grid-row: 3; grid-column: 2; }
#calc-btn-TAN { grid-row: 4; grid-column: 2; }
#calc-btn-SQR { grid-row: 5; grid-column: 2; }
#calc-btn-EXP { grid-row: 2; grid-column: 1; }
#calc-btn-E { grid-row: 3; grid-column: 1; }
#calc-btn-PI { grid-row: 4; grid-column: 1; }
#calc-btn-LOG { grid-row: 5; grid-column: 1; }
</style>
<p id="prev-equation"></p>
<div id="calculator-widget">
<p id="calc-text">0</p>
<div id="calc-btns">
<button id="calc-btn-0" class="common">0</button>
<button id="calc-btn-1" class="common">1</button>
<button id="calc-btn-2" class="common">2</button>
<button id="calc-btn-3" class="common">3</button>
<button id="calc-btn-4" class="common">4</button>
<button id="calc-btn-5" class="common">5</button>
<button id="calc-btn-6" class="common">6</button>
<button id="calc-btn-7" class="common">7</button>
<button id="calc-btn-8" class="common">8</button>
<button id="calc-btn-9" class="common">9</button>
<button id="calc-btn-EQ" class="common">=</button>
<button id="calc-btn-PT" class="common">.</button>
<button id="calc-btn-BCK"></button>
<button id="calc-btn-ADD">+</button>
<button id="calc-btn-SUB">-</button>
<button id="calc-btn-MLT">x</button>
<button id="calc-btn-DIV">/</button>
<button id="calc-btn-CLR">C</button>
<button id="calc-btn-PRC">%</button>
<button id="calc-btn-RP">)</button>
<button id="calc-btn-LP">(</button>
<button id="calc-btn-ABS">|x|</button>
<button id="calc-btn-SIN">sin</button>
<button id="calc-btn-COS">cos</button>
<button id="calc-btn-TAN">tan</button>
<button id="calc-btn-SQR"></button>
<button id="calc-btn-EXP">^</button>
<button id="calc-btn-E"></button>
<button id="calc-btn-PI">π</button>
<button id="calc-btn-LOG">log</button>
</div>
</div>
<script>
// JS does not have this by default.
// from https://www.freecodecamp.org/news/how-to-factorialize-a-number-in-javascript-9263c89a4b38/
function factorial(num) {
if (num < 0)
return -1;
else if (num === 0)
return 1;
else {
return (num * factorial(num - 1));
}
}
// returns true if the user is currently focused on the calculator widget
function usingCalculator() {
let activeElement = document.activeElement;
while (true) {
if (!activeElement) return false;
if (activeElement.id === "calculator-wrapper") return true;
activeElement = activeElement.parentElement;
}
}
const $ = q => document.querySelectorAll(q);
// key bindings for commonly used buttons
const keybindings = {
"0": "0",
"1": "1",
"2": "2",
"3": "3",
"4": "4",
"5": "5",
"6": "6",
"7": "7",
"8": "8",
"9": "9",
"Enter": "EQ",
".": "PT",
"+": "ADD",
"-": "SUB",
"*": "MLT",
"/": "DIV",
"%": "PRC",
"c": "CLR",
"(": "LP",
")": "RP",
"Backspace": "BCK",
}
window.addEventListener("keydown", event => {
if (!usingCalculator()) return;
if (event.key === "Enter" && document.activeElement.id !== "search-bar")
event.preventDefault();
if (keybindings[event.key])
document.getElementById("calc-btn-" + keybindings[event.key]).click();
})
// calculates the string
const calc = () => {
var mathtext = document.getElementById("calc-text");
var statement = mathtext.innerHTML
// remove empty ()
.replace("()", "")
// special constants
.replace("π", "(Math.PI)")
.replace("ℇ", "(Math.E)")
// turns 3(1+2) into 3*(1+2) (for example)
.replace(/(?<=[0-9\)])(?<=[^+\-x*\/%^])\(/, "x(")
// same except reversed
.replace(/\)(?=[0-9\(])(?=[^+\-x*\/%^])/, ")x")
// replace human friendly x with JS *
.replace("x", "*")
// trig & misc functions
.replace("sin", "Math.sin")
.replace("cos", "Math.cos")
.replace("tan", "Math.tan")
.replace("√", "Math.sqrt")
.replace("^", "**")
.replace("abs", "Math.abs")
.replace("log", "Math.log")
;
// add any missing )s to the end
while(true) if (
(statement.match(/\(/g) || []).length >
(statement.match(/\)/g) || []).length
) statement += ")"; else break;
// evaluate the expression.
console.log("calculating [" + statement + "]");
try {
var result = eval(statement);
document.getElementById("prev-equation").innerHTML = mathtext.innerHTML + " = ";
mathtext.innerHTML = result;
mathtext.classList.remove("error-border");
} catch (e) {
mathtext.classList.add("error-border");
console.error(e);
}
}
const updateCalc = (e) => {
// character(s) recieved from button
var c = event.target.innerHTML;
var mathtext = document.getElementById("calc-text");
if (mathtext.innerHTML === "0") mathtext.innerHTML = "";
// special cases
switch (c) {
case "C":
// Clear
mathtext.innerHTML = "0";
break;
case "⬅":
// Delete
mathtext.innerHTML = mathtext.innerHTML.slice(0, -1);
if (mathtext.innerHTML.length === 0) {
mathtext.innerHTML = "0";
}
break;
case "=":
calc()
break;
case "sin":
case "cos":
case "tan":
case "log":
case "√":
mathtext.innerHTML += `${c}(`;
break;
case "|x|":
mathtext.innerHTML += "abs("
break;
case "+":
case "-":
case "x":
case "/":
case "%":
case "^":
if (mathtext.innerHTML.length === 0) mathtext.innerHTML = "0";
// prevent typing 2 operators in a row
if (mathtext.innerHTML.match(/[+\-x\/%^] $/))
mathtext.innerHTML = mathtext.innerHTML.slice(0, -3);
mathtext.innerHTML += ` ${c} `;
break;
default:
mathtext.innerHTML += c;
}
}
for (let i of $("#calc-btns button")) {
i.addEventListener('click', event => {
updateCalc(event);
})
}
</script>

40
app/templates/display.html Executable file
View file

@ -0,0 +1,40 @@
<html>
<head>
<link rel="shortcut icon" href="static/img/favicon.ico" type="image/x-icon">
<link rel="icon" href="static/img/favicon.ico" type="image/x-icon">
{% if not search_type %}
<link rel="search" href="opensearch.xml" type="application/opensearchdescription+xml" title="RaveSearch">
{% else %}
<link rel="search" href="opensearch.xml?tbm={{ search_type }}" type="application/opensearchdescription+xml" title="RaveSearch ({{ search_name }})">
{% endif %}
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="no-referrer">
<link rel="stylesheet" href="{{ cb_url('logo.css') }}">
<link rel="stylesheet" href="{{ cb_url('input.css') }}">
<link rel="stylesheet" href="{{ cb_url('search.css') }}">
<link rel="stylesheet" href="{{ cb_url('header.css') }}">
<style>
@import "{{ cb_url('dark-theme.css') }}";
</style>
<title>{{ clean_query(query) }} - RaveSearch</title>
</head>
<body>
{{ search_header|safe }}
{% if is_translation %}
<iframe
id="lingva-iframe"
src="{{ lingva_url }}/auto/{{ translate_to }}/{{ translate_str }}">
</iframe>
{% endif %}
{{ response|safe }}
</body>
{% include 'footer.html' %}
{% if autocomplete_enabled == '1' %}
<script src="{{ cb_url('autocomplete.js') }}"></script>
{% endif %}
<script src="{{ cb_url('utils.js') }}"></script>
<script src="{{ cb_url('keyboard.js') }}"></script>
<script src="{{ cb_url('currency.js') }}"></script>
</html>

106
app/templates/error.html Executable file
View file

@ -0,0 +1,106 @@
{% if config.theme %}
{% if config.theme == 'system' %}
<style>
@import "{{ cb_url('light-theme.css') }}" screen;
@import "{{ cb_url('dark-theme.css') }}" screen and (prefers-color-scheme: dark);
</style>
{% else %}
<link rel="stylesheet" href="{{ cb_url(config.theme + '-theme.css') }}"/>
{% endif %}
{% else %}
<link rel="stylesheet" href="{{ cb_url(('dark' if config.dark else 'light') + '-theme.css') }}"/>
{% endif %}
<link rel="stylesheet" href="{{ cb_url('main.css') }}">
<link rel="stylesheet" href="{{ cb_url('error.css') }}">
<style>{{ config.style }}</style>
<div>
<h1>Error</h1>
<p>
{{ error_message }}
</p>
<hr>
{% if query and translation %}
<p>
<h4><a class="link" href="https://farside.link">{{ translation['continue-search'] }}</a></h4>
<ul>
<li>
<a href="https://github.com/benbusby/whoogle-search">Whoogle</a>
<ul>
<li>
<a class="link-color" href="{{farside}}/whoogle/search?q={{query}}{{params}}">
{{farside}}/whoogle/search?q={{query}}
</a>
</li>
</ul>
</li>
<li>
<a href="https://github.com/searxng/searxng">SearXNG</a>
<ul>
<li>
<a class="link-color" href="{{farside}}/searxng/search?q={{query}}">
{{farside}}/searxng/search?q={{query}}
</a>
</li>
</ul>
</li>
</ul>
<hr>
<h4>Other options:</h4>
<ul>
<li>
<a href="https://kagi.com">Kagi</a>
<ul>
<li>Requires account</li>
<li>
<a class="link-color" href="https://kagi.com/search?q={{query}}">
kagi.com/search?q={{query}}
</a>
</li>
</ul>
</li>
<li>
<a href="https://duckduckgo.com">DuckDuckGo</a>
<ul>
<li>
<a class="link-color" href="https://duckduckgo.com/search?q={{query}}">
duckduckgo.com/search?q={{query}}
</a>
</li>
</ul>
</li>
<li>
<a href="https://search.brave.com">Brave Search</a>
<ul>
<li>
<a class="link-color" href="https://search.brave.com/search?q={{query}}">
search.brave.com/search?q={{query}}
</a>
</li>
</ul>
</li>
<li>
<a href="https://ecosia.com">Ecosia</a>
<ul>
<li>
<a class="link-color" href="https://ecosia.com/search?q={{query}}">
ecosia.com/search?q={{query}}
</a>
</li>
</ul>
</li>
<li>
<a href="https://google.com">Google</a>
<ul>
<li>
<a class="link-color" href="https://google.com/search?q={{query}}">
google.com/search?q={{query}}
</a>
</li>
</ul>
</li>
</ul>
<hr>
</p>
{% endif %}
<a class="link" href="home">Return Home</a>
</div>

18
app/templates/footer.html Executable file
View file

@ -0,0 +1,18 @@
</div>
<footer>
<p class="footer">
A RaveSearch a Whoogle v{{ version_number }} alapjait használja ||
<a class="link" href="https://github.com/benbusby/whoogle-search"
>{{ translation['github-link'] }}</a
></p>
<!-- {% if has_update %} ||
<span class="update_available">Update Available 🟢</span>
{% endif %} -->
<span>
<p class="rp-credit" style="color: white">
Szeretnél többet megtudni a keresőről? <a class="kattide" href="https://rp1.hu/leirasok/ravepriest1-kereso/">RavePriest1</a>
</p>
</span>
</footer>

89
app/templates/header.html Executable file
View file

@ -0,0 +1,89 @@
<div class="header-div">
<div id="search_header">
<a id="search_logo" href="{{ home_url }}" tabindex="0">
{{ logo|safe }}
</a>
</div>
<form class="search-form header" id="search-form" method="{{ 'get' if config.get_only else 'post' }}" role="search" action="search">
<div class="header-search">
<div class="autocomplete">
{% if config.preferences %}
<input type="hidden" name="preferences" value="{{ config.preferences }}" />
{% endif %}
<input
id="search-bar"
class="mobile-search-bar"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
name="q"
type="text"
value="{{ clean_query(query) }}"
dir="auto"
/>
<input id="search-reset" type="reset" value="x">
<input name="tbm" value="{{ search_type }}" style="display: none">
<input name="country" value="{{ config.country }}" style="display: none;">
<input type="submit" id="search-submit" style="display: none;">
</div>
</div>
</form>
<div class="header-tab-div">
<div class="mobile-header header-tab">
{% for tab_id, tab_content in tabs.items() %}
{% if tab_content['selected'] %}
<a><button class="header-button selected-header-button">{{ tab_content['name'] }}</button></a>
{% else %}
<a href="{{ tab_content['href'] }}"><button class="header-button">{{ tab_content['name'] }}</button></a>
{% endif %}
{% endfor %}
<label for="adv-search-toggle" id="adv-search-label" class="adv-search"></label>
<input id="adv-search-toggle" type="checkbox">
<div class="header-tab-div-end"></div>
</div>
</div>
</div>
<div class="result-collapsible" id="adv-search-div">
<div class="result-config">
<label for="config-country">{{ translation['config-country'] }}: </label>
<select name="country" id="result-country">
{% for country in countries %}
<option value="{{ country.value }}"
{% if (
config.country != '' and config.country in country.value
) or (
config.country == '' and country.value == '')
%}
selected
{% endif %}>
{{ country.name }}
</option>
{% endfor %}
</select>
<br /><br />
<label for="config-time-period">{{ translation['config-time-period'] }}: </label>
<select name="tbs" id="result-time-period">
{% for time_period in time_periods %}
<option value="{{ time_period.value }}"
{% if (
config.tbs != '' and config.tbs in time_period.value
) or (
config.tbs == '' and time_period.value == '')
%}
selected
{% endif %}>
{{ translation[time_period.value] }}
</option>
{% endfor %}
</select>
</div>
</div>
<script type="text/javascript" src="{{ cb_url('header.js') }}"></script>

210
app/templates/header2.html Executable file
View file

@ -0,0 +1,210 @@
{% if mobile %}
<header>
<div class="header-div">
<form class="search-form header"
id="search-form"
method="{{ 'GET' if config.get_only else 'POST' }}">
<a class="logo-link mobile-logo" href="{{ home_url }}">
<div id="mobile-header-logo">
{{ logo|safe }}
</div>
</a>
<div class="H0PQec mobile-input-div">
<div class="autocomplete-mobile esbc autocomplete">
{% if config.preferences %}
<input type="hidden" name="preferences" value="{{ config.preferences }}" />
{% endif %}
<input
id="search-bar"
class="mobile-search-bar"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
class="search-bar-input"
name="q"
type="text"
value="{{ clean_query(query) }}"
dir="auto">
<input id="search-reset" type="reset" value="x">
<input name="tbm" value="{{ search_type }}" style="display: none">
<input name="country" value="{{ config.country }}" style="display: none;">
<input type="submit" style="display: none;">
<div class="sc"></div>
</div>
</div>
</form>
</div>
<div>
<div class="header-tab-div">
<div class="header-tab-div-2">
<div class="header-tab-div-3">
<div class="mobile-header header-tab">
{% for tab_id, tab_content in tabs.items() %}
{% if tab_content['selected'] %}
<span class="mobile-tab-span">{{ tab_content['name'] }}</span>
{% else %}
<a class="header-tab-a" href="{{ tab_content['href'] }}">{{ tab_content['name'] }}</a>
{% endif %}
{% endfor %}
<label for="adv-search-toggle" id="adv-search-label" class="adv-search"></label>
<input id="adv-search-toggle" type="checkbox">
<div class="header-tab-div-end"></div>
</div>
</div>
</div>
</div>
<div class="" id="s">
</div>
</header>
{% else %}
<header>
<div class="logo-div">
<a class="logo-link" href="{{ home_url }}">
<div class="desktop-header-logo">
{{ logo|safe }}
</div>
</a>
</div>
<div class="search-div">
<form id="search-form"
class="search-form"
id="sf"
method="{{ 'GET' if config.get_only else 'POST' }}">
<div class="autocomplete header-autocomplete">
<div style="width: 100%; display: flex">
{% if config.preferences %}
<input type="hidden" name="preferences" value="{{ config.preferences }}" />
{% endif %}
<input
id="search-bar"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
class="search-bar-desktop search-bar-input"
name="q"
spellcheck="false"
type="text"
value="{{ clean_query(query) }}"
dir="auto">
<input name="tbm" value="{{ search_type }}" style="display: none">
<input name="country" value="{{ config.country }}" style="display: none;">
<input name="tbs" value="{{ config.tbs }}" style="display: none;">
<input type="submit" style="display: none;">
<div class="sc"></div>
</div>
</div>
</form>
</div>
</header>
<div>
<div class="header-tab-div">
<div class="header-tab-div-2">
<div class="header-tab-div-3">
<div class="desktop-header header-tab">
{% for tab_id, tab_content in tabs.items() %}
{% if tab_content['selected'] %}
<span class="header-tab-span">{{ tab_content['name'] }}</span>
{% else %}
<a class="header-tab-a" href="{{ tab_content['href'] }}">{{ tab_content['name'] }}</a>
{% endif %}
{% endfor %}
<label for="adv-search-toggle" id="adv-search-label" class="adv-search"></label>
<input id="adv-search-toggle" type="checkbox">
</div>
</div>
</div>
</div>
<div class="" id="s">
</div>
{% endif %}
<div class="result-collapsible" id="adv-search-div">
<div class="result-config">
<label for="config-country">{{ translation['config-country'] }}: </label>
<select name="country" id="result-country">
{% for country in countries %}
<option value="{{ country.value }}"
{% if (
config.country != '' and config.country in country.value
) or (
config.country == '' and country.value == '')
%}
selected
{% endif %}>
{{ country.name }}
</option>
{% endfor %}
</select>
<br />
<label for="config-time-period">{{ translation['config-time-period'] }}: </label>
<select name="tbs" id="result-time-period">
{% for time_period in time_periods %}
<option value="{{ time_period.value }}"
{% if (
config.tbs != '' and config.tbs in time_period.value
) or (
config.tbs == '' and time_period.value == '')
%}
selected
{% endif %}>
{{ translation[time_period.value] }}
</option>
{% endfor %}
</select>
</div>
</div>
<script type="text/javascript" src="{{ cb_url('header.js') }}"></script>
<div id="search_view">
<div class="search_box">
<input id="q" name="q" type="text" placeholder="{{ _('Search for...') }}" tabindex="1" autocomplete="off" autocapitalize="none" spellcheck="false" autocorrect="off" dir="auto" value="{{ q or '' }}">
<button id="clear_search" type="reset" aria-label="{{ _('clear') }}" class="hide_if_nojs"><span>{{ icon_big('close') }}</span><span class="show_if_nojs">{{ _('clear') }}</span></button>
<button id="send_search" type="submit" {%- if search_on_category_select -%}name="category_{{ selected_categories[0]|replace(' ', '_') }}"{%- endif -%} aria-label="{{ _('search') }}"><span class="hide_if_nojs">{{ icon_big('search-outline') }}</span><span class="show_if_nojs">{{ _('search') }}</span></button>
</div>
</div>
{% set display_tooltip = true %}
{% include 'simple/categories.html' %}
</div>
<div class="search_filters">
{% include 'simple/filters/languages.html' %}
{% include 'simple/filters/time_range.html' %}
{% include 'simple/filters/safesearch.html' %}
</div>
<input type="hidden" name="theme" value="{{ theme }}" >
{% if timeout_limit %}<input type="hidden" name="timeout_limit" value="{{ timeout_limit|e }}" >{% endif %}
</form>
<div id="categories" class="search_categories">{{- '' -}}
<div id="categories_container">
{%- if not search_on_category_select or not display_tooltip -%}
{%- for category in categories_as_tabs -%}
<div class="category category_checkbox">{{- '' -}}
<input type="checkbox" id="checkbox_{{ category|replace(' ', '_') }}" name="category_{{ category }}"{% if category in selected_categories %} checked="checked"{% endif %}/>
<label for="checkbox_{{ category|replace(' ', '_') }}" class="tooltips">
{{- icon_big(category_icons[category]) if category in category_icons else icon_big('globe-outline') -}}
<div class="category_name">{{- _(category) -}}</div>
</label>
</div>
{%- endfor -%}
{%- if display_tooltip %}<div class="help">{{ _('Click on the magnifier to perform search') }}</div>{% endif -%}
{%- else -%}
{%- for category in categories_as_tabs -%}{{- '\n' -}}
<button type="submit" name="category_{{ category }}" class="category category_button {% if category in selected_categories %}selected{% endif %}">
{{- icon_big(category_icons[category]) if category in category_icons else icon_big('globe-outline') -}}
<div class="category_name">{{- _(category) -}}</div>{{- '' -}}
</button>{{- '' -}}
{%- endfor -%}
{{- '\n' -}}
{%- endif -%}
</div>{{- '' -}}
</div>

451
app/templates/imageresults.html Executable file
View file

@ -0,0 +1,451 @@
<div>
<!-- <style>
html {
font-family:
Roboto,
Helvetica Neue,
Arial,
sans-serif;
font-size: 14px;
line-height: 20px;
text-size-adjust: 100%;
color: #3c4043;
word-wrap: break-word;
background-color: #fff;
}
body {
padding: 0 8px;
margin: 0 auto;
max-width: 900px;
}
a {
text-decoration: none;
color: inherit;
}
a:hover {
text-decoration: underline;
}
a img {
border: 0;
}
.FbhRzb {
border-left: thin solid #dadce0;
border-right: thin solid #dadce0;
border-top: thin solid #dadce0;
height: 40px;
overflow: hidden;
}
.n692Zd {
margin-bottom: 10px;
}
.cvifge {
height: 40px;
border-spacing: 0;
width: 100%;
}
.QvGUP {
height: 40px;
padding: 0 8px 0 8px;
vertical-align: top;
}
.O4cRJf {
height: 40px;
width: 100%;
padding: 0;
padding-right: 16px;
}
.O1ePr {
height: 40px;
padding: 0;
vertical-align: top;
}
.kgJEQe {
height: 36px;
width: 98px;
vertical-align: top;
margin-top: 4px;
}
.lXLRf {
vertical-align: top;
}
.MhzMZd {
border: 0;
vertical-align: middle;
font-size: 14px;
height: 40px;
padding: 0;
width: 100%;
padding-left: 16px;
}
.xB0fq {
height: 40px;
border: none;
font-size: 14px;
background-color: #4285f4;
color: #fff;
padding: 0 16px;
margin: 0;
vertical-align: top;
cursor: pointer;
}
.xB0fq:focus {
border: 1px solid #000;
}
.M7pB2 {
border: thin solid #dadce0;
margin: 0 0 3px 0;
font-size: 13px;
font-weight: 500;
height: 40px;
}
.euZec {
width: 100%;
height: 40px;
text-align: center;
border-spacing: 0;
}
table.euZec td {
padding: 0;
width: 25%;
}
.QIqI7 {
display: inline-block;
padding-top: 4px;
font-weight: bold;
color: #4285f4;
}
.EY24We {
border-bottom: 2px solid #4285f4;
}
.CsQyDc {
display: inline-block;
color: #70757a;
}
.TuS8Ad {
font-size: 14px;
}
.HddGcc {
padding: 8px;
color: #70757a;
}
.dzp8ae {
font-weight: bold;
color: #3c4043;
}
.rEM8G {
color: #70757a;
}
.bookcf {
table-layout: fixed;
width: 100%;
border-spacing: 0;
}
.InWNIe {
text-align: center;
}
.uZgmoc {
width: 350px;
display: flex;
border: none;
color: #70757a;
font-size: 14px;
text-align: center;
/* background-color: #29292987; */
border-radius: 8px;
margin: 35px auto;
padding: 10px;
justify-content: center;
}
.frGj1b {
margin: 10px;
font-size: 16px;
border: 1px solid;
border-radius: 10px;
padding: 10px 15px;
}
.BnJWBc {
text-align: center;
padding: 6px 0 13px 0;
height: 35px;
}
.X6ZCif {
color: #202124;
font-size: 11px;
line-height: 16px;
display: inline-block;
padding-top: 2px;
overflow: hidden;
padding-bottom: 4px;
width: 100%;
}
.TwVfHd {
border-radius: 16px;
border: thin solid #dadce0;
display: inline-block;
padding: 8px 8px;
margin-right: 8px;
margin-bottom: 4px;
}
.yekiAe {
background-color: #dadce0;
}
.svla5d {
width: 100%;
height: 100%;
background-color: #29292987;
border-radius: 10px;
margin: 10px 0;
display: grid;
margin: 0 auto;
padding: 10px 0;
}
.ezO2md {
border: thin solid #dadce0;
padding: 12px 16px 12px 16px;
margin-bottom: 10px;
font-family: Roboto, Helvetica, Arial, sans-serif;
}
.K35ahc {
width: 100%;
}
.owohpf {
text-align: center;
}
.fYyStc {
word-break: break-word;
text-align: center;
font-size: 16px;
padding: 0 0 10px 0;
}
.img_button {
padding: 10px;
margin: 0 auto 10px;
border: 1px solid #84dbff;
border-radius: 6px;
color: #3c4043;
background-color: #2e97c3;
}
.ynsChf {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.Fj3V3b {
color: #1967d2;
font-size: 14px;
line-height: 20px;
}
.FrIlee {
color: #202124;
font-size: 11px;
line-height: 16px;
}
.F9iS2e {
color: #70757a;
font-size: 11px;
line-height: 16px;
}
.WMQ2Le {
color: #70757a;
font-size: 12px;
line-height: 16px;
}
.x3G5ab {
color: #202124;
font-size: 12px;
line-height: 16px;
}
.fuLhoc {
color: #1967d2;
font-size: 18px;
line-height: 24px;
}
.epoveb {
font-size: 32px;
line-height: 40px;
font-weight: 400;
color: #202124;
}
.dXDvrc {
color: #0d652d;
font-size: 14px;
line-height: 20px;
word-wrap: break-word;
}
.dloBPe {
font-weight: bold;
}
.YVIcad {
color: #70757a;
}
.JkVVdd {
color: #ea4335;
}
.oXZRFd {
color: #ea4335;
}
.MQHtg {
color: #fbbc04;
}
.pyMRrb {
color: #1e8e3e;
}
.EtTZid {
color: #1e8e3e;
}
.M3vVJe {
color: #1967d2;
}
.NHQNef {
font-style: italic;
}
.Cb8Z7c {
white-space: pre;
}
a.ZWRArf {
text-decoration: none;
}
a .CVA68e:hover {
text-decoration: underline;
}
</style>
<style>
.results {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin: 20px 0;
}
.result-item {
padding: 10px;
border-radius: 5px;
background-color: #29292987;
width: calc(33.333% - 16px);
box-sizing: border-box;
text-align: center;
}
.result-item img {
max-width: 100%;
height: auto;
border-radius: 5px;
}
.result-item p {
margin: 10px 0 0;
font-size: 14px;
}
.img-title {
color: var(--primary-grad3);
text-transform: uppercase;
}
.img-url {
color: var(--body-color);
}
.img-url:hover {
text-decoration: underline;
color: var(--grad2-active);
}
/* Stílusok a javaslatokhoz */
.suggestions {
margin: 20px 0;
padding: 10px;
background-color: #e9e9e9;
border: 1px solid #ccc;
border-radius: 5px;
}
/* Stílusok a lapozáshoz */
.pagination {
display: flex;
justify-content: center;
margin: 20px 0;
}
.pagination a {
margin: 0 5px;
padding: 10px 15px;
border: 1px solid #ddd;
background-color: #fff;
text-decoration: none;
color: #007bff;
border-radius: 5px;
}
.pagination a:hover {
background-color: #007bff;
color: #fff;
}
.e3goi {
padding: 10px;
width: 100%;
height: 160px;
}
.TxbwNb {
width: 100%;
height: 100%;
}
.RAyV4b {
width: 100%;
height: 130px;
display: flex;
justify-content: center;
align-items: center;
}
.t0fcAb {
scale: 1.8;
}
.Tor4Ec {
text-align: center;
width: 100%;
height: 20px;
display: block;
padding: 10px 0;
}
.qXLe6d {
font-size: 16px;
font-weight: bold;
}
</style> -->
<div class="results">
{% for result in results %}
<div class="result-item">
<a href="{{ result.web_page }}">
<img src="{{ result.img_tbn }}" alt="Image thumbnail">
</a>
<a href="{{ result.web_page }}"><p class="img-title">{{ result.domain }}</p></a>
<a href="{{ result.img_url }}"><p class="img-url">{{ result.img_url }}</p></a>
</div>
{% endfor %}
</div>
<div class="uZgmoc">
<!-- next page object -->
</div>
<br />
<div class="lIMUZd">
<div class="By0U9">
<!-- correction suggested -->
</div>
</div>
</div>

303
app/templates/index.html Executable file
View file

@ -0,0 +1,303 @@
<!DOCTYPE html>
<html>
<head>
<link rel="apple-touch-icon" sizes="180x180" href="static/img/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="static/img/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="static/img/favicon/favicon-16x16.png">
<link rel="manifest" href="static/img/favicon/site.webmanifest">
<link rel="mask-icon" href="static/img/favicon/safari-pinned-tab.svg" color="#00566f">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff">
{% if autocomplete_enabled == '1' %}
<script src="{{ cb_url('autocomplete.js') }}"></script>
{% endif %}
<script type="text/javascript" src="{{ cb_url('controller.js') }}"></script>
<link rel="search" href="opensearch.xml" type="application/opensearchdescription+xml" title="RaveSearch">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{{ cb_url('logo.css') }}">
<style>
@import "{{ cb_url('dark-theme.css') }}";
</style>
<link rel="stylesheet" href="{{ cb_url('main.css') }}">
<title>RaveSearch</title>
</head>
<body id="main_index">
<main class="search-container">
<div class="logo-container">
{{ logo|safe }}
</div>
<form id="search-form" action="search" method="{{ 'get' if config.get_only else 'post' }}">
<div class="search-fields">
<div class="autocomplete">
{% if config.preferences %}
<input type="hidden" name="preferences" value="{{ config.preferences }}" />
{% endif %}
<input
placeholder="{{ translation['input-text'] }}"
type="text"
name="q"
id="search-bar"
autofocus="autofocus"
autocapitalize="none"
spellcheck="false"
autocorrect="off"
autocomplete="off"
dir="auto" />
</div>
<input type="submit" id="search-submit" value="{{ translation['search'] }}">
</div>
</form>
{% if not config_disabled %}
<button id="config-collapsible" class="collapsible">{{ translation['config'] }}</button>
<div class="content">
<div class="config-fields">
<form id="config-form" action="config" method="post">
<div class="config-options">
<!--<div class="config-div config-div-country">
<label for="config-country">{{ translation['config-country'] }}: </label>
<select name="country" id="config-country">
{% for country in countries %}
<option value="{{ country.value }}"
{% if (
config.country != '' and config.country in country.value
) or (
config.country == '' and country.value == '')
%}
selected
{% endif %}>
{{ country.name }}
</option>
{% endfor %}
</select>
</div> -->
<!-- <div class="config-div">
<label for="config-time-period">{{ translation['config-time-period'] }}</label>
<select name="tbs" id="config-time-period">
{% for time_period in time_periods %}
<option value="{{ time_period.value }}"
{% if (
config.tbs != '' and config.tbs in time_period.value
) or (
config.tbs == '' and time_period.value == '')
%}
selected
{% endif %}>
{{ translation[time_period.value] }}
</option>
{% endfor %}
</select>
</div> -->
<div class="config-div config-div-lang">
<label for="config-lang-interface">{{ translation['config-lang'] }}: </label>
<select name="lang_interface" id="config-lang-interface">
{% for lang in languages %}
<option value="{{ lang.value }}"
{% if lang.value in config.lang_interface %}
selected
{% endif %}>
{{ lang.name }}
</option>
{% endfor %}
</select>
</div>
<div class="config-div config-div-search-lang">
<label for="config-lang-search">{{ translation['config-lang-search'] }}: </label>
<select name="lang_search" id="config-lang-search">
{% for lang in languages %}
<option value="{{ lang.value }}"
{% if lang.value in config.lang_search %}
selected
{% endif %}>
{{ lang.name }}
</option>
{% endfor %}
</select>
</div>
<!-- <div class="config-div config-div-autocomplete">
<label for="config-autocomplete">autocomplete</label>
<input type="checkbox" name="autocomplete" id="config-autocomplete" {{ 'checked' if config.autocomplete else '' }}>
</div> -->
<!-- <div class="config-div config-div-near">
<label for="config-near">{{ translation['config-near'] }}: </label>
<input type="text" name="near" id="config-near"
placeholder="{{ translation['config-near-help'] }}" value="{{ config.near }}">
</div> -->
<!-- <div class="config-div config-div-block">
<label for="config-block">{{ translation['config-block'] }}: </label>
<input type="text" name="block" id="config-block"
placeholder="{{ translation['config-block-help'] }}" value="{{ config.block }}">
</div> -->
<!-- <div class="config-div config-div-block">
<label for="config-block-title">{{ translation['config-block-title'] }}: </label>
<input type="text" name="block_title" id="config-block"
placeholder="{{ translation['config-block-title-help'] }}"
value="{{ config.block_title }}">
</div> -->
<!-- <div class="config-div config-div-block">
<label for="config-block-url">{{ translation['config-block-url'] }}: </label>
<input type="text" name="block_url" id="config-block"
placeholder="{{ translation['config-block-url-help'] }}" value="{{ config.block_url }}">
</div> -->
<div class="config-div config-div-anon-view">
<label for="config-anon-view">{{ translation['config-anon-view'] }}: </label>
<input type="checkbox" name="anon_view" id="config-anon-view" {{ 'checked' if config.anon_view else '' }}>
</div>
<div class="config-div config-div-nojs">
<label for="config-nojs">{{ translation['config-nojs'] }}: </label>
<input type="checkbox" name="nojs" id="config-nojs" {{ 'checked' if config.nojs else '' }}>
</div>
<!-- DEPRECATED -->
<!--<div class="config-div config-div-dark">-->
<!--<label for="config-dark">{{ translation['config-dark'] }}: </label>-->
<!--<input type="checkbox" name="dark" id="config-dark" {{ 'checked' if config.dark else '' }}>-->
<!--</div>-->
<div class="config-div config-div-safe">
<label for="config-safe">{{ translation['config-safe'] }}: </label>
<input type="checkbox" name="safe" id="config-safe" {{ 'checked' if config.safe else '' }}>
</div>
<div class="config-div config-div-alts">
<label class="tooltip" for="config-alts">{{ translation['config-alts'] }}: </label>
<input type="checkbox" name="alts" id="config-alts" {{ 'checked' if config.alts else '' }}>
<div><span class="info-text"> — {{ translation['config-alts-help'] }}</span></div>
</div>
<div class="config-div config-div-new-tab">
<label for="config-new-tab">{{ translation['config-new-tab'] }}: </label>
<input type="checkbox" name="new_tab"
id="config-new-tab" {{ 'checked' if config.new_tab else '' }}>
</div>
<!-- <div class="config-div config-div-view-image">
<label for="config-view-image">{{ translation['config-images'] }}: </label>
<input checked type="checkbox" name="view_image"
id="config-view-image" {{ 'checked' if config.view_image else '' }}>
<div><span class="info-text"> — {{ translation['config-images-help'] }}</span></div>
</div> -->
<div class="config-div config-div-tor">
<label for="config-tor">{{ translation['config-tor'] }}: {{ '' if tor_available else 'Unavailable' }}</label>
<input type="checkbox" name="tor"
id="config-tor" {{ '' if tor_available else 'hidden' }} {{ 'checked' if config.tor else '' }}>
</div>
<div class="config-div config-div-get-only">
<label for="config-get-only">{{ translation['config-get-only'] }}: </label>
<input type="checkbox" name="get_only"
id="config-get-only" {{ 'checked' if config.get_only else '' }}>
</div>
<!-- <div class="config-div config-div-accept-language">
<label for="config-accept-language">Set Accept-Language: </label>
<input type="checkbox" name="accept_language"
id="config-accept-language" {{ 'checked' if config.accept_language else '' }}>
</div> -->
<div class="config-div config-div-root-url">
<label for="config-url">{{ translation['config-url'] }}: </label>
<input type="text" name="url" id="config-url" value="{{ config.url }}">
</div>
<div class="config-div config-div-custom-css">
<a id="css-link"
href="https://github.com/benbusby/whoogle-search/wiki/User-Contributed-CSS-Themes">
{{ translation['config-css'] }}:
</a>
<textarea
name="style_modified"
id="config-style"
autocapitalize="off"
autocomplete="off"
spellcheck="false"
autocorrect="off"
value="">{{ config.style_modified.replace('\t', '') }}</textarea>
</div>
<div class="config-div config-div-pref-url">
<label for="config-pref-encryption">{{ translation['config-pref-encryption'] }}: </label>
<input type="checkbox" name="preferences_encrypted"
id="config-pref-encryption" {{ 'checked' if config.preferences_encrypted and config.preferences_key else '' }}>
<div><span class="info-text"> — {{ translation['config-pref-help'] }}</span></div>
<label for="config-pref-url">{{ translation['config-pref-url'] }}: </label>
<input type="text" name="pref-url" id="config-pref-url" value="{{ config.url }}?preferences={{ config.preferences }}">
</div>
</div>
<div class="config-div config-buttons">
<input type="submit" id="config-load" value="{{ translation['load'] }}">&nbsp;
<input type="submit" id="config-submit" value="{{ translation['apply'] }}">&nbsp;
<input type="submit" id="config-save" value="{{ translation['save-as'] }}">
</div>
</form>
</div>
</div>
{% endif %}
<div class="social-container">
<div class="social-column">
<div class="rpkick">
<a href="https://kick.com/ravepriest1">
<img
src="static/img/rp/rpkicksearch.png"
alt="rpkick"
title="RP Kick"
width="200px"
height="50px"
/></a>
</div>
<div class="rpdiscord">
<a href="https://discord.com/invite/MwUydP4gc5">
<img
src="static/img/rp/rpdiscordsearch.png"
alt="rpdiscord"
title="RP Discord"
width="200px"
height="50px"
/></a>
</div>
<div class="rptwitch">
<a href="https://www.twitch.tv/ravepriest1">
<img
src="static/img/rp/rptwitchsearch.png"
alt="rptwitch"
title="RP Twitch"
width="200px"
height="50px"
/></a>
</div>
<div class="rpgit">
<a href="https://git.rp1.hu/explore/repos">
<img
src="static/img/rp/rpgiteasearch.png"
alt="rpgit"
title="RP Gitea"
width="200px"
height="50px"
/></a>
</div>
<div class="rpyoutube">
<a href="https://www.youtube.com/@RPslair">
<img
src="static/img/rp/rpyoutubesearch.png"
alt="rpyoutube"
title="RP YouTube"
width="200px"
height="50px"
/></a>
</div>
<div class="rpweb">
<a href="https://rp1.hu">
<img
src="static/img/rp/rpwebsearch.png"
alt="rpweb"
title="RP Website"
width="200px"
height="50px"
/></a>
</div>
</div>
</div>
</main>
{% include 'footer.html' %}
</body>
</html>

8
app/templates/logo.html Executable file
View file

@ -0,0 +1,8 @@
<div class="rplogo">
<img
src="static/img/rplogo.webp"
alt="rplogo"
title="RP Kereső"
style="object-fit: contain; height: auto; width: 100%;"
/>
</div>

25
app/templates/opensearch.xml Executable file

File diff suppressed because one or more lines are too long

15
app/templates/search.html Executable file
View file

@ -0,0 +1,15 @@
<form id="search-form" action="search" method="post">
<input
type="text"
name="q"
style="width: 90%"
autofocus="autofocus"
autocapitalize="none"
spellcheck="false"
autocorrect="off"
placeholder="RaveSearch"
autocomplete="off"
dir="auto"
/>
<input type="submit" style="width: 9%" id="search-submit" value="Search" />
</form>

BIN
app/utils/.DS_Store vendored Executable file

Binary file not shown.

0
app/utils/__init__.py Executable file
View file

146
app/utils/bangs.py Executable file
View file

@ -0,0 +1,146 @@
import json
import requests
import urllib.parse as urlparse
import os
import glob
bangs_dict = {}
DDG_BANGS = 'https://duckduckgo.com/bang.js'
def load_all_bangs(ddg_bangs_file: str, ddg_bangs: dict = {}):
"""Loads all the bang files in alphabetical order
Args:
ddg_bangs_file: The str path to the new DDG bangs json file
ddg_bangs: The dict of ddg bangs. If this is empty, it will load the
bangs from the file
Returns:
None
"""
global bangs_dict
ddg_bangs_file = os.path.normpath(ddg_bangs_file)
if (bangs_dict and not ddg_bangs) or os.path.getsize(ddg_bangs_file) <= 4:
return
bangs = {}
bangs_dir = os.path.dirname(ddg_bangs_file)
bang_files = glob.glob(os.path.join(bangs_dir, '*.json'))
# Normalize the paths
bang_files = [os.path.normpath(f) for f in bang_files]
# Move the ddg bangs file to the beginning
bang_files = sorted([f for f in bang_files if f != ddg_bangs_file])
if ddg_bangs:
bangs |= ddg_bangs
else:
bang_files.insert(0, ddg_bangs_file)
for i, bang_file in enumerate(bang_files):
try:
bangs |= json.load(open(bang_file))
except json.decoder.JSONDecodeError:
# Ignore decoding error only for the ddg bangs file, since this can
# occur if file is still being written
if i != 0:
raise
bangs_dict = dict(sorted(bangs.items()))
def gen_bangs_json(bangs_file: str) -> None:
"""Generates a json file from the DDG bangs list
Args:
bangs_file: The str path to the new DDG bangs json file
Returns:
None
"""
try:
# Request full list from DDG
r = requests.get(DDG_BANGS)
r.raise_for_status()
except requests.exceptions.HTTPError as err:
raise SystemExit(err)
# Convert to json
data = json.loads(r.text)
# Set up a json object (with better formatting) for all available bangs
bangs_data = {}
for row in data:
bang_command = '!' + row['t']
bangs_data[bang_command] = {
'url': row['u'].replace('{{{s}}}', '{}'),
'suggestion': bang_command + ' (' + row['s'] + ')'
}
json.dump(bangs_data, open(bangs_file, 'w'))
print('* Finished creating ddg bangs json')
load_all_bangs(bangs_file, bangs_data)
def suggest_bang(query: str) -> list[str]:
"""Suggests bangs for a user's query
Args:
query: The search query
Returns:
list[str]: A list of bang suggestions
"""
global bangs_dict
return [bangs_dict[_]['suggestion'] for _ in bangs_dict if _.startswith(query)]
def resolve_bang(query: str) -> str:
"""Transform's a user's query to a bang search, if an operator is found
Args:
query: The search query
Returns:
str: A formatted redirect for a bang search, or an empty str if there
wasn't a match or didn't contain a bang operator
"""
global bangs_dict
#if ! not in query simply return (speed up processing)
if '!' not in query:
return ''
split_query = query.strip().split(' ')
# look for operator in query if one is found, list operator should be of
# length 1, operator should not be case-sensitive here to remove it later
operator = [
word
for word in split_query
if word.lower() in bangs_dict
]
if len(operator) == 1:
# get operator
operator = operator[0]
# removes operator from query
split_query.remove(operator)
# rebuild the query string
bang_query = ' '.join(split_query).strip()
# Check if operator is a key in bangs and get bang if exists
bang = bangs_dict.get(operator.lower(), None)
if bang:
bang_url = bang['url']
if bang_query:
return bang_url.replace('{}', bang_query, 1)
else:
parsed_url = urlparse.urlparse(bang_url)
return f'{parsed_url.scheme}://{parsed_url.netloc}'
return ''

137
app/utils/misc.py Executable file
View file

@ -0,0 +1,137 @@
import base64
import hashlib
import contextlib
import io
import os
import re
from requests import exceptions, get
from urllib.parse import urlparse
from bs4 import BeautifulSoup as bsoup
from cryptography.fernet import Fernet
from flask import Request
ddg_favicon_site = 'http://icons.duckduckgo.com/ip2'
empty_gif = base64.b64decode(
'R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==')
placeholder_img = base64.b64decode(
'iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAABF0lEQVRIS8XWPw9EMBQA8Eok' \
'JBKrMFqMBt//GzAYLTZ/VomExPDu6uLiaPteqVynBn0/75W2Vp7nEIYhe6p1XcespmmAd7Is' \
'M+4URcGiKPogvMMvmIS2eN9MOMKbKWgf54SYgI4vKkTuQKJKSJErkKzUSkQHUs0lilAg7GMh' \
'ISoIA/hYMiKCKIA2soeowCWEMkfHtUmrXLcyGYYBfN9HF8djiaglWzNZlgVs21YisoAUaEXG' \
'cQTP86QIFgi7vyLzPIPjOEIEC7ANQv/4aZrAdd0TUtc1i+MYnSsMWjPp+x6CIPgJVlUVS5KE' \
'DKig/+wnVzM4pnzaGeHd+ENlWbI0TbVLJBtw2uMfP63wc9d2kDCWxi5Q27bsBerSJ9afJbeL' \
'AAAAAElFTkSuQmCC'
)
def fetch_favicon(url: str) -> bytes:
"""Fetches a favicon using DuckDuckGo's favicon retriever
Args:
url: The url to fetch the favicon from
Returns:
bytes - the favicon bytes, or a placeholder image if one
was not returned
"""
response = get(f'{ddg_favicon_site}/{urlparse(url).netloc}.ico')
if response.status_code == 200 and len(response.content) > 0:
tmp_mem = io.BytesIO()
tmp_mem.write(response.content)
tmp_mem.seek(0)
return tmp_mem.read()
return placeholder_img
def gen_file_hash(path: str, static_file: str) -> str:
file_contents = open(os.path.join(path, static_file), 'rb').read()
file_hash = hashlib.md5(file_contents).hexdigest()[:8]
filename_split = os.path.splitext(static_file)
return f'{filename_split[0]}.{file_hash}{filename_split[-1]}'
def read_config_bool(var: str, default: bool=False) -> bool:
val = os.getenv(var, '1' if default else '0')
# user can specify one of the following values as 'true' inputs (all
# variants with upper case letters will also work):
# ('true', 't', '1', 'yes', 'y')
return val.lower() in ('true', 't', '1', 'yes', 'y')
def get_client_ip(r: Request) -> str:
if r.environ.get('HTTP_X_FORWARDED_FOR') is None:
return r.environ['REMOTE_ADDR']
return r.environ['HTTP_X_FORWARDED_FOR']
def get_request_url(url: str) -> str:
if os.getenv('HTTPS_ONLY', False):
return url.replace('http://', 'https://', 1)
return url
def get_proxy_host_url(r: Request, default: str, root=False) -> str:
scheme = r.headers.get('X-Forwarded-Proto', 'https')
http_host = r.headers.get('X-Forwarded-Host')
full_path = r.full_path if not root else ''
if full_path.startswith('/'):
full_path = f'/{full_path}'
if http_host:
prefix = os.environ.get('WHOOGLE_URL_PREFIX', '')
if prefix:
prefix = f'/{re.sub("[^0-9a-zA-Z]+", "", prefix)}'
return f'{scheme}://{http_host}{prefix}{full_path}'
return default
def check_for_update(version_url: str, current: str) -> int:
# Check for the latest version of Whoogle
has_update = ''
with contextlib.suppress(exceptions.ConnectionError, AttributeError):
update = bsoup(get(version_url).text, 'html.parser')
latest = update.select_one('[class="Link--primary"]').string[1:]
current = int(''.join(filter(str.isdigit, current)))
latest = int(''.join(filter(str.isdigit, latest)))
has_update = '' if current >= latest else latest
return has_update
def get_abs_url(url, page_url):
# Creates a valid absolute URL using a partial or relative URL
urls = {
"//": f"https:{url}",
"/": f"{urlparse(page_url).netloc}{url}",
"./": f"{page_url}{url[2:]}"
}
for start in urls:
if url.startswith(start):
return urls[start]
return url
def list_to_dict(lst: list) -> dict:
if len(lst) < 2:
return {}
return {lst[i].replace(' ', ''): lst[i+1].replace(' ', '')
for i in range(0, len(lst), 2)}
def encrypt_string(key: bytes, string: str) -> str:
cipher_suite = Fernet(key)
return cipher_suite.encrypt(string.encode()).decode()
def decrypt_string(key: bytes, string: str) -> str:
cipher_suite = Fernet(g.session_key)
return cipher_suite.decrypt(string.encode()).decode()

466
app/utils/results.py Executable file
View file

@ -0,0 +1,466 @@
from app.models.config import Config
from app.models.endpoint import Endpoint
from app.utils.misc import list_to_dict
from bs4 import BeautifulSoup, NavigableString
import copy
from flask import current_app
import html
import os
import urllib.parse as urlparse
from urllib.parse import parse_qs
import re
import warnings
SKIP_ARGS = ['ref_src', 'utm']
SKIP_PREFIX = ['//www.', '//mobile.', '//m.']
GOOG_STATIC = 'www.gstatic.com'
G_M_LOGO_URL = 'https://www.gstatic.com/m/images/icons/googleg.gif'
GOOG_IMG = '/images/branding/searchlogo/1x/googlelogo'
LOGO_URL = GOOG_IMG + '_desk'
BLANK_B64 = ('data:image/png;base64,'
'iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAD0lEQVR42mNkw'
'AIYh7IgAAVVAAuInjI5AAAAAElFTkSuQmCC')
# Ad keywords
BLACKLIST = [
'ad', 'ads', 'anuncio', 'annuncio', 'annonce', 'Anzeige', '广告', '廣告',
'Reklama', 'Реклама', 'Anunț', '광고', 'annons', 'Annonse', 'Iklan',
'広告', 'Augl.', 'Mainos', 'Advertentie', 'إعلان', 'Գովազդ', 'विज्ञापन',
'Reklam', 'آگهی', 'Reklāma', 'Reklaam', 'Διαφήμιση', 'מודעה', 'Hirdetés', 'Szponzorált',
'Anúncio', 'Quảng cáo','โฆษณา', 'sponsored', 'patrocinado', 'gesponsert', 'Sponzorováno', '스폰서', 'Gesponsord'
]
SITE_ALTS = {
'twitter.com': os.getenv('WHOOGLE_ALT_TW', 'farside.link/nitter'),
'youtube.com': os.getenv('WHOOGLE_ALT_YT', 'farside.link/invidious'),
'reddit.com': os.getenv('WHOOGLE_ALT_RD', 'farside.link/libreddit'),
**dict.fromkeys([
'medium.com',
'levelup.gitconnected.com'
], os.getenv('WHOOGLE_ALT_MD', 'farside.link/scribe')),
'imgur.com': os.getenv('WHOOGLE_ALT_IMG', 'farside.link/rimgo'),
'wikipedia.org': os.getenv('WHOOGLE_ALT_WIKI', 'farside.link/wikiless'),
'imdb.com': os.getenv('WHOOGLE_ALT_IMDB', 'farside.link/libremdb'),
'quora.com': os.getenv('WHOOGLE_ALT_QUORA', 'farside.link/quetre'),
'stackoverflow.com': os.getenv('WHOOGLE_ALT_SO', 'farside.link/anonymousoverflow')
}
# Include custom site redirects from WHOOGLE_REDIRECTS
SITE_ALTS.update(list_to_dict(re.split(',|:', os.getenv('WHOOGLE_REDIRECTS', ''))))
def contains_cjko(s: str) -> bool:
"""This function check whether or not a string contains Chinese, Japanese,
or Korean characters. It employs regex and uses the u escape sequence to
match any character in a set of Unicode ranges.
Args:
s (str): string to be checked
Returns:
bool: True if the input s contains the characters and False otherwise
"""
unicode_ranges = ('\u4e00-\u9fff' # Chinese characters
'\u3040-\u309f' # Japanese hiragana
'\u30a0-\u30ff' # Japanese katakana
'\u4e00-\u9faf' # Japanese kanji
'\uac00-\ud7af' # Korean hangul syllables
'\u1100-\u11ff' # Korean hangul jamo
)
return bool(re.search(fr'[{unicode_ranges}]', s))
def bold_search_terms(response: str, query: str) -> BeautifulSoup:
"""Wraps all search terms in bold tags (<b>). If any terms are wrapped
in quotes, only that exact phrase will be made bold.
Args:
response: The initial response body for the query
query: The original search query
Returns:
BeautifulSoup: modified soup object with bold items
"""
response = BeautifulSoup(response, 'html.parser')
def replace_any_case(element: NavigableString, target_word: str) -> None:
# Replace all instances of the word, but maintaining the same case in
# the replacement
if len(element) == len(target_word):
return
# Ensure target word is escaped for regex
target_word = re.escape(target_word)
# Check if the word contains Chinese, Japanese, or Korean characters
if contains_cjko(target_word):
reg_pattern = fr'((?![{{}}<>-]){target_word}(?![{{}}<>-]))'
else:
reg_pattern = fr'\b((?![{{}}<>-]){target_word}(?![{{}}<>-]))\b'
if re.match(r'.*[@_!#$%^&*()<>?/\|}{~:].*', target_word) or (
element.parent and element.parent.name == 'style'):
return
element.replace_with(BeautifulSoup(
re.sub(reg_pattern,
r'<b>\1</b>',
element,
flags=re.I), 'html.parser')
)
# Split all words out of query, grouping the ones wrapped in quotes
for word in re.split(r'\s+(?=[^"]*(?:"[^"]*"[^"]*)*$)', query):
word = re.sub(r'[@_!#$%^&*()<>?/\|}{~:]+', '', word)
target = response.find_all(
text=re.compile(r'' + re.escape(word), re.I))
for nav_str in target:
replace_any_case(nav_str, word)
return response
def has_ad_content(element: str) -> bool:
"""Inspects an HTML element for ad related content
Args:
element: The HTML element to inspect
Returns:
bool: True/False for the element containing an ad
"""
element_str = ''.join(filter(str.isalpha, element))
return (element_str.upper() in (value.upper() for value in BLACKLIST)
or '' in element)
def get_first_link(soup: BeautifulSoup) -> str:
"""Retrieves the first result link from the query response
Args:
soup: The BeautifulSoup response body
Returns:
str: A str link to the first result
"""
first_link = ''
orig_details = []
# Temporarily remove details so we don't grab those links
for details in soup.find_all('details'):
temp_details = soup.new_tag('removed_details')
orig_details.append(details.replace_with(temp_details))
# Replace hrefs with only the intended destination (no "utm" type tags)
for a in soup.find_all('a', href=True):
# Return the first search result URL
if a['href'].startswith('http://') or a['href'].startswith('https://'):
first_link = a['href']
break
# Add the details back
for orig_detail, details in zip(orig_details, soup.find_all('removed_details')):
details.replace_with(orig_detail)
return first_link
def get_site_alt(link: str, site_alts: dict = SITE_ALTS) -> str:
"""Returns an alternative to a particular site, if one is configured
Args:
link: A string result URL to check against the site_alts map
site_alts: A map of site alternatives to replace with. defaults to SITE_ALTS
Returns:
str: An updated (or ignored) result link
"""
# Need to replace full hostname with alternative to encapsulate
# subdomains as well
parsed_link = urlparse.urlparse(link)
# Extract subdomain separately from the domain+tld. The subdomain
# is used for wikiless translations.
split_host = parsed_link.netloc.split('.')
subdomain = split_host[0] if len(split_host) > 2 else ''
hostname = '.'.join(split_host[-2:])
# The full scheme + hostname is used when comparing against the list of
# available alternative services, due to how Medium links are constructed.
# (i.e. for medium.com: "https://something.medium.com" should match,
# "https://medium.com/..." should match, but "philomedium.com" should not)
hostcomp = f'{parsed_link.scheme}://{hostname}'
for site_key in site_alts.keys():
site_alt = f'{parsed_link.scheme}://{site_key}'
if not hostname or site_alt not in hostcomp or not site_alts[site_key]:
continue
# Wikipedia -> Wikiless replacements require the subdomain (if it's
# a 2-char language code) to be passed as a URL param to Wikiless
# in order to preserve the language setting.
params = ''
if 'wikipedia' in hostname and len(subdomain) == 2:
hostname = f'{subdomain}.{hostname}'
params = f'?lang={subdomain}'
elif 'medium' in hostname and len(subdomain) > 0:
hostname = f'{subdomain}.{hostname}'
parsed_alt = urlparse.urlparse(site_alts[site_key])
link = link.replace(hostname, site_alts[site_key]) + params
# If a scheme is specified in the alternative, this results in a
# replaced link that looks like "https://http://altservice.tld".
# In this case, we can remove the original scheme from the result
# and use the one specified for the alt.
if parsed_alt.scheme:
link = '//'.join(link.split('//')[1:])
for prefix in SKIP_PREFIX:
if parsed_alt.scheme:
# If a scheme is specified, remove everything before the
# first occurence of it
link = f'{parsed_alt.scheme}{link.split(parsed_alt.scheme, 1)[-1]}'
else:
# Otherwise, replace the first occurrence of the prefix
link = link.replace(prefix, '//', 1)
break
return link
def filter_link_args(link: str) -> str:
"""Filters out unnecessary URL args from a result link
Args:
link: The string result link to check for extraneous URL params
Returns:
str: An updated (or ignored) result link
"""
parsed_link = urlparse.urlparse(link)
link_args = parse_qs(parsed_link.query)
safe_args = {}
if len(link_args) == 0 and len(parsed_link) > 0:
return link
for arg in link_args.keys():
if arg in SKIP_ARGS:
continue
safe_args[arg] = link_args[arg]
# Remove original link query and replace with filtered args
link = link.replace(parsed_link.query, '')
if len(safe_args) > 0:
link = link + urlparse.urlencode(safe_args, doseq=True)
else:
link = link.replace('?', '')
return link
def append_nojs(result: BeautifulSoup) -> None:
"""Appends a no-Javascript alternative for a search result
Args:
result: The search result to append a no-JS link to
Returns:
None
"""
nojs_link = BeautifulSoup(features='html.parser').new_tag('a')
nojs_link['href'] = f'{Endpoint.window}?nojs=1&location=' + result['href']
nojs_link.string = ' NoJS Link'
result.append(nojs_link)
def append_anon_view(result: BeautifulSoup, config: Config) -> None:
"""Appends an 'anonymous view' for a search result, where all site
contents are viewed through Whoogle as a proxy.
Args:
result: The search result to append an anon view link to
nojs: Remove Javascript from Anonymous View
Returns:
None
"""
av_link = BeautifulSoup(features='html.parser').new_tag('a')
nojs = 'nojs=1' if config.nojs else 'nojs=0'
location = f'location={result["href"]}'
av_link['href'] = f'{Endpoint.window}?{nojs}&{location}'
translation = current_app.config['TRANSLATIONS'][
config.get_localization_lang()
]
av_link.string = f'{translation["anon-view"]}'
av_link['class'] = 'anon-view'
result.append(av_link)
def check_currency(response: str) -> dict:
"""Check whether the results have currency conversion
Args:
response: Search query Result
Returns:
dict: Consists of currency names and values
"""
soup = BeautifulSoup(response, 'html.parser')
currency_link = soup.find('a', {'href': 'https://g.co/gfd'})
if currency_link:
while 'class' not in currency_link.attrs or \
'ZINbbc' not in currency_link.attrs['class']:
if currency_link.parent:
currency_link = currency_link.parent
else:
return {}
currency_link = currency_link.find_all(class_='BNeawe')
currency1 = currency_link[0].text
currency2 = currency_link[1].text
currency1 = currency1.rstrip('=').split(' ', 1)
currency2 = currency2.split(' ', 1)
# Handle differences in currency formatting
# i.e. "5.000" vs "5,000"
if currency2[0][-3] == ',':
currency1[0] = currency1[0].replace('.', '')
currency1[0] = currency1[0].replace(',', '.')
currency2[0] = currency2[0].replace('.', '')
currency2[0] = currency2[0].replace(',', '.')
else:
currency1[0] = currency1[0].replace(',', '')
currency2[0] = currency2[0].replace(',', '')
currency1_value = float(re.sub(r'[^\d\.]', '', currency1[0]))
currency1_label = currency1[1]
currency2_value = float(re.sub(r'[^\d\.]', '', currency2[0]))
currency2_label = currency2[1]
return {'currencyValue1': currency1_value,
'currencyLabel1': currency1_label,
'currencyValue2': currency2_value,
'currencyLabel2': currency2_label
}
return {}
def add_currency_card(soup: BeautifulSoup,
conversion_details: dict) -> BeautifulSoup:
"""Adds the currency conversion boxes
to response of the search query
Args:
soup: Parsed search result
conversion_details: Dictionary of currency
related information
Returns:
BeautifulSoup
"""
# Element before which the code will be changed
# (This is the 'disclaimer' link)
element1 = soup.find('a', {'href': 'https://g.co/gfd'})
while 'class' not in element1.attrs or \
'nXE3Ob' not in element1.attrs['class']:
element1 = element1.parent
# Creating the conversion factor
conversion_factor = (conversion_details['currencyValue1'] /
conversion_details['currencyValue2'])
# Creating a new div for the input boxes
conversion_box = soup.new_tag('div')
conversion_box['class'] = 'conversion_box'
# Currency to be converted from
input_box1 = soup.new_tag('input')
input_box1['id'] = 'cb1'
input_box1['type'] = 'number'
input_box1['class'] = 'cb'
input_box1['value'] = conversion_details['currencyValue1']
input_box1['oninput'] = f'convert(1, 2, {1 / conversion_factor})'
label_box1 = soup.new_tag('label')
label_box1['for'] = 'cb1'
label_box1['class'] = 'cb_label'
label_box1.append(conversion_details['currencyLabel1'])
br = soup.new_tag('br')
# Currency to be converted to
input_box2 = soup.new_tag('input')
input_box2['id'] = 'cb2'
input_box2['type'] = 'number'
input_box2['class'] = 'cb'
input_box2['value'] = conversion_details['currencyValue2']
input_box2['oninput'] = f'convert(2, 1, {conversion_factor})'
label_box2 = soup.new_tag('label')
label_box2['for'] = 'cb2'
label_box2['class'] = 'cb_label'
label_box2.append(conversion_details['currencyLabel2'])
conversion_box.append(input_box1)
conversion_box.append(label_box1)
conversion_box.append(br)
conversion_box.append(input_box2)
conversion_box.append(label_box2)
element1.insert_before(conversion_box)
return soup
def get_tabs_content(tabs: dict,
full_query: str,
search_type: str,
preferences: str,
translation: dict) -> dict:
"""Takes the default tabs content and updates it according to the query.
Args:
tabs: The default content for the tabs
full_query: The original search query
search_type: The current search_type
translation: The translation to get the names of the tabs
Returns:
dict: contains the name, the href and if the tab is selected or not
"""
map_query = full_query
if '-site:' in full_query:
block_idx = full_query.index('-site:')
map_query = map_query[:block_idx]
tabs = copy.deepcopy(tabs)
for tab_id, tab_content in tabs.items():
# update name to desired language
if tab_id in translation:
tab_content['name'] = translation[tab_id]
# update href with query
query = full_query.replace(f'&tbm={search_type}', '')
if tab_content['tbm'] is not None:
query = f"{query}&tbm={tab_content['tbm']}"
if preferences:
query = f"{query}&preferences={preferences}"
tab_content['href'] = tab_content['href'].format(
query=query,
map_query=map_query)
# update if selected tab (default all tab is selected)
if tab_content['tbm'] == search_type:
tabs['all']['selected'] = False
tab_content['selected'] = True
return tabs

194
app/utils/search.py Executable file
View file

@ -0,0 +1,194 @@
import os
import re
from typing import Any
from app.filter import Filter
from app.request import gen_query
from app.utils.misc import get_proxy_host_url
from app.utils.results import get_first_link
from bs4 import BeautifulSoup as bsoup
from cryptography.fernet import Fernet, InvalidToken
from flask import g
TOR_BANNER = '<hr><h1 style="text-align: center">You are using Tor</h1><hr>'
CAPTCHA = 'div class="g-recaptcha"'
def needs_https(url: str) -> bool:
"""Checks if the current instance needs to be upgraded to HTTPS
Note that all Heroku instances are available by default over HTTPS, but
do not automatically set up a redirect when visited over HTTP.
Args:
url: The instance url
Returns:
bool: True/False representing the need to upgrade
"""
https_only = bool(os.getenv('HTTPS_ONLY', 0))
is_heroku = url.endswith('.herokuapp.com')
is_http = url.startswith('http://')
return (is_heroku and is_http) or (https_only and is_http)
def has_captcha(results: str) -> bool:
"""Checks to see if the search results are blocked by a captcha
Args:
results: The search page html as a string
Returns:
bool: True/False indicating if a captcha element was found
"""
return CAPTCHA in results
class Search:
"""Search query preprocessor - used before submitting the query or
redirecting to another site
Attributes:
request: the incoming flask request
config: the current user config settings
session_key: the flask user fernet key
"""
def __init__(self, request, config, session_key, cookies_disabled=False):
method = request.method
self.request = request
self.request_params = request.args if method == 'GET' else request.form
self.user_agent = request.headers.get('User-Agent')
self.feeling_lucky = False
self.config = config
self.session_key = session_key
self.query = ''
self.widget = ''
self.view_image = True
self.cookies_disabled = cookies_disabled
self.search_type = self.request_params.get(
'tbm') if 'tbm' in self.request_params else ''
def __getitem__(self, name) -> Any:
return getattr(self, name)
def __setitem__(self, name, value) -> None:
return setattr(self, name, value)
def __delitem__(self, name) -> None:
return delattr(self, name)
def __contains__(self, name) -> bool:
return hasattr(self, name)
def new_search_query(self) -> str:
"""Parses a plaintext query into a valid string for submission
Also decrypts the query string, if encrypted (in the case of
paginated results).
Returns:
str: A valid query string
"""
q = self.request_params.get('q')
if q is None or len(q) == 0:
return ''
else:
# Attempt to decrypt if this is an internal link
try:
q = Fernet(self.session_key).decrypt(q.encode()).decode()
except InvalidToken:
pass
# Strip '!' for "feeling lucky" queries
if match := re.search(r"(^|\s)!($|\s)", q):
self.feeling_lucky = True
start, end = match.span()
self.query = " ".join([seg for seg in [q[:start], q[end:]] if seg])
else:
self.feeling_lucky = False
self.query = q
# Check for possible widgets
self.widget = "ip" if re.search("([^a-z0-9]|^)my *[^a-z0-9] *(ip|internet protocol)" +
"($|( *[^a-z0-9] *(((addres|address|adres|" +
"adress)|a)? *$)))", self.query.lower()) else self.widget
self.widget = 'calculator' if re.search(
r"\bcalculator\b|\bcalc\b|\bcalclator\b|\bmath\b",
self.query.lower()) else self.widget
return self.query
def generate_response(self) -> str:
"""Generates a response for the user's query
Returns:
str: A string response to the search query, in the form of a URL
or string representation of HTML content.
"""
mobile = 'Android' in self.user_agent or 'iPhone' in self.user_agent
# reconstruct url if X-Forwarded-Host header present
root_url = get_proxy_host_url(
self.request,
self.request.url_root,
root=True)
content_filter = Filter(self.session_key,
root_url=root_url,
mobile=mobile,
config=self.config,
query=self.query)
full_query = gen_query(self.query,
self.request_params,
self.config)
self.full_query = full_query
# force mobile search when view image is true and
# the request is not already made by a mobile
view_image = ('tbm=isch' in full_query
# and self.config.view_image
#and not g.user_request.mobile
)
get_body = g.user_request.send(query=full_query,
force_mobile=view_image,
user_agent=self.user_agent)
# Produce cleanable html soup from response
get_body_safed = get_body.text.replace("&lt;","andlt;").replace("&gt;","andgt;")
html_soup = bsoup(get_body_safed, 'html.parser')
# Replace current soup if view_image is active
if view_image:
html_soup = content_filter.view_image(html_soup)
# Indicate whether or not a Tor connection is active
if g.user_request.tor_valid:
html_soup.insert(0, bsoup(TOR_BANNER, 'html.parser'))
formatted_results = content_filter.clean(html_soup)
if self.feeling_lucky:
if lucky_link := get_first_link(formatted_results):
return lucky_link
# Fall through to regular search if unable to find link
self.feeling_lucky = False
# Append user config to all search links, if available
param_str = ''.join('&{}={}'.format(k, v)
for k, v in
self.request_params.to_dict(flat=True).items()
if self.config.is_safe_key(k))
for link in formatted_results.find_all('a', href=True):
link['rel'] = "nofollow noopener noreferrer"
if 'search?' not in link['href'] or link['href'].index(
'search?') > 1:
continue
link['href'] += param_str
return str(formatted_results)

39
app/utils/session.py Executable file
View file

@ -0,0 +1,39 @@
from cryptography.fernet import Fernet
from flask import current_app as app
REQUIRED_SESSION_VALUES = ['uuid', 'config', 'key', 'auth']
def generate_key() -> bytes:
"""Generates a key for encrypting searches and element URLs
Args:
cookies_disabled: Flag for whether or not cookies are disabled by the
user. If so, the user can only use the default key
generated on app init for queries.
Returns:
str: A unique Fernet key
"""
# Generate/regenerate unique key per user
return Fernet.generate_key()
def valid_user_session(session: dict) -> bool:
"""Validates the current user session
Args:
session: The current Flask user session
Returns:
bool: True/False indicating that all required session values are
available
"""
# Generate secret key for user if unavailable
for value in REQUIRED_SESSION_VALUES:
if value not in session:
return False
return True

71
app/utils/widgets.py Executable file
View file

@ -0,0 +1,71 @@
from pathlib import Path
from bs4 import BeautifulSoup
# root
BASE_DIR = Path(__file__).parent.parent.parent
def add_ip_card(html_soup: BeautifulSoup, ip: str) -> BeautifulSoup:
"""Adds the client's IP address to the search results
if query contains keywords
Args:
html_soup: The parsed search result containing the keywords
ip: ip address of the client
Returns:
BeautifulSoup
"""
main_div = html_soup.select_one('#main')
if main_div:
# HTML IP card tag
ip_tag = html_soup.new_tag('div')
ip_tag['class'] = 'ZINbbc xpd O9g5cc uUPGi'
# For IP Address html tag
ip_address = html_soup.new_tag('div')
ip_address['class'] = 'kCrYT ip-address-div'
ip_address.string = ip
# Text below the IP address
ip_text = html_soup.new_tag('div')
ip_text.string = 'Your public IP address'
ip_text['class'] = 'kCrYT ip-text-div'
# Adding all the above html tags to the IP card
ip_tag.append(ip_address)
ip_tag.append(ip_text)
# Insert the element at the top of the result list
main_div.insert_before(ip_tag)
return html_soup
def add_calculator_card(html_soup: BeautifulSoup) -> BeautifulSoup:
"""Adds the a calculator widget to the search results
if query contains keywords
Args:
html_soup: The parsed search result containing the keywords
Returns:
BeautifulSoup
"""
main_div = html_soup.select_one('#main')
if main_div:
# absolute path
widget_file = open(BASE_DIR / 'app/static/widgets/calculator.html', encoding="utf8")
widget_tag = html_soup.new_tag('div')
widget_tag['class'] = 'ZINbbc xpd O9g5cc uUPGi'
widget_tag['id'] = 'calculator-wrapper'
calculator_text = html_soup.new_tag('div')
calculator_text['class'] = 'kCrYT ip-address-div'
calculator_text.string = 'Calculator'
calculator_widget = html_soup.new_tag('div')
calculator_widget.append(BeautifulSoup(widget_file, 'html.parser'))
calculator_widget['class'] = 'kCrYT ip-text-div'
widget_tag.append(calculator_text)
widget_tag.append(calculator_widget)
main_div.insert_before(widget_tag)
widget_file.close()
return html_soup

7
app/version.py Executable file
View file

@ -0,0 +1,7 @@
import os
optional_dev_tag = ''
if os.getenv('DEV_BUILD'):
optional_dev_tag = '.dev' + os.getenv('DEV_BUILD')
__version__ = '0.9.1' + optional_dev_tag

View file

@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

23
charts/whoogle/Chart.yaml Normal file
View file

@ -0,0 +1,23 @@
apiVersion: v2
name: whoogle
description: A self hosted search engine on Kubernetes
type: application
version: 0.1.0
appVersion: 0.9.1
icon: https://github.com/benbusby/whoogle-search/raw/main/app/static/img/favicon/favicon-96x96.png
sources:
- https://github.com/benbusby/whoogle-search
- https://gitlab.com/benbusby/whoogle-search
- https://gogs.benbusby.com/benbusby/whoogle-search
keywords:
- whoogle
- degoogle
- search
- google
- search-engine
- privacy
- tor
- python

View file

@ -0,0 +1,22 @@
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
{{- range .paths }}
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
{{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "whoogle.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "whoogle.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "whoogle.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "whoogle.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}

View file

@ -0,0 +1,62 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "whoogle.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "whoogle.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "whoogle.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "whoogle.labels" -}}
helm.sh/chart: {{ include "whoogle.chart" . }}
{{ include "whoogle.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "whoogle.selectorLabels" -}}
app.kubernetes.io/name: {{ include "whoogle.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "whoogle.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "whoogle.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

Some files were not shown because too many files have changed in this diff Show more