├── MANIFEST.in ├── ovisbot ├── __init__.py ├── locale │ ├── cy │ │ └── LC_MESSAGES │ │ │ ├── ovisbot.mo │ │ │ └── ovisbot.po │ └── ovisbot.pot ├── utils │ ├── logging.py │ └── progressbar.py ├── __main__.py ├── locale.py ├── todo-fixes.md ├── exceptions.py ├── error_handling.py ├── events.py ├── helpers.py ├── extensions │ ├── poll │ │ └── poll.py │ ├── stats │ │ └── stats.py │ ├── utils │ │ └── utils.py │ ├── ctftime │ │ ├── ctftime.py │ │ └── ctftime_helpers.py │ ├── cryptohack │ │ └── cryptohack.py │ └── hackthebox │ │ └── hackthebox.py ├── commands │ ├── base.py │ ├── rank.py │ └── manage.py ├── cli.py ├── bot.py ├── db_models.py ├── config.py └── cog_manager.py ├── docs ├── _build │ ├── html │ │ ├── _static │ │ │ ├── custom.css │ │ │ ├── file.png │ │ │ ├── plus.png │ │ │ ├── minus.png │ │ │ ├── documentation_options.js │ │ │ ├── pygments.css │ │ │ ├── doctools.js │ │ │ ├── underscore.js │ │ │ ├── language_data.js │ │ │ ├── alabaster.css │ │ │ └── basic.css │ │ ├── objects.inv │ │ ├── .buildinfo │ │ ├── _sources │ │ │ └── index.rst.txt │ │ ├── searchindex.js │ │ ├── genindex.html │ │ ├── search.html │ │ ├── py-modindex.html │ │ └── index.html │ └── doctrees │ │ ├── index.doctree │ │ └── environment.pickle ├── index.rst ├── Makefile ├── make.bat └── conf.py ├── .bumpversion.cfg ├── docker-compose.prod.yml ├── Dockerfile ├── Makefile ├── .env.example ├── .pre-commit-config.yaml ├── docker-compose.reloading.yml ├── .gitignore ├── docker-compose.yml ├── logging.ini ├── .github └── workflows │ ├── pypi_publish.yml │ ├── push_image_deploy.yml │ └── codeql-analysis.yml ├── CONTRIBUTE.md ├── Pipfile ├── setup.py ├── setup.cfg └── README.md /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include ovisbot/locale * -------------------------------------------------------------------------------- /ovisbot/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "2.0.0" 2 | -------------------------------------------------------------------------------- /docs/_build/html/_static/custom.css: -------------------------------------------------------------------------------- 1 | /* This file intentionally left blank. */ 2 | -------------------------------------------------------------------------------- /docs/_build/html/objects.inv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cybermouflons/ovisbot/HEAD/docs/_build/html/objects.inv -------------------------------------------------------------------------------- /docs/_build/html/_static/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cybermouflons/ovisbot/HEAD/docs/_build/html/_static/file.png -------------------------------------------------------------------------------- /docs/_build/html/_static/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cybermouflons/ovisbot/HEAD/docs/_build/html/_static/plus.png -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 2.0.0 3 | commit = True 4 | tag = True 5 | tag_name = v{new_version} 6 | -------------------------------------------------------------------------------- /docs/_build/doctrees/index.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cybermouflons/ovisbot/HEAD/docs/_build/doctrees/index.doctree -------------------------------------------------------------------------------- /docs/_build/html/_static/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cybermouflons/ovisbot/HEAD/docs/_build/html/_static/minus.png -------------------------------------------------------------------------------- /docker-compose.prod.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | bot: 5 | image: ghcr.io/cybermouflons/ovisbot:latest 6 | volumes: [] -------------------------------------------------------------------------------- /docs/_build/doctrees/environment.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cybermouflons/ovisbot/HEAD/docs/_build/doctrees/environment.pickle -------------------------------------------------------------------------------- /ovisbot/locale/cy/LC_MESSAGES/ovisbot.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cybermouflons/ovisbot/HEAD/ovisbot/locale/cy/LC_MESSAGES/ovisbot.mo -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9 2 | 3 | COPY . /ovisbot 4 | 5 | WORKDIR /ovisbot 6 | 7 | RUN pip install pipenv 8 | RUN pipenv install -e . 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | setupenv: 3 | @python tools/setupenv.py 4 | 5 | docker-run: 6 | @docker-compose up 7 | 8 | # Python Code Style 9 | reformat: 10 | python -m black `git ls-files "*.py"` 11 | -------------------------------------------------------------------------------- /ovisbot/utils/logging.py: -------------------------------------------------------------------------------- 1 | import logging.config 2 | import os 3 | 4 | file_path = os.path.dirname(os.path.abspath(__file__)) 5 | 6 | logging.config.fileConfig( 7 | os.path.join(file_path, "..", "..", "logging.ini"), disable_existing_loggers=False 8 | ) 9 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | OVISBOT_DISCORD_TOKEN= 2 | OVISBOT_WOLFRAM_ALPHA_APP_ID= 3 | OVISBOT_HTB_CREDS_EMAIL=htb@randomdomain.com 4 | OVISBOT_HTB_CREDS_PASS=myhtbpassword 5 | OVISBOT_HTB_TEAM_ID=1000 6 | OVISBOT_CTFTIME_TEAM_ID=999 7 | OVISBOT_ADMIN_ROLE=moderator -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v2.0.0 6 | hooks: 7 | - id: flake8 -------------------------------------------------------------------------------- /docs/_build/html/.buildinfo: -------------------------------------------------------------------------------- 1 | # Sphinx build info version 1 2 | # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. 3 | config: e7585df31311fa4669f9e4eb033fb11f 4 | tags: 645f666f9bcd5a90fca523b33c5a78b7 5 | -------------------------------------------------------------------------------- /docker-compose.reloading.yml: -------------------------------------------------------------------------------- 1 | services: 2 | live-reloader: 3 | image: apogiatzis/livereloading 4 | container_name: livereloader 5 | privileged: true 6 | environment: 7 | - RELOAD_CONTAINER=ovisbot 8 | volumes: 9 | - "/var/run/docker.sock:/var/run/docker.sock" 10 | - ./ovisbot:/ovisbot -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.env 3 | .vscode 4 | .DS_Store 5 | *.egg-info 6 | 7 | # Distribution / packaging 8 | .Python 9 | build/ 10 | develop-eggs/ 11 | dist/ 12 | downloads/ 13 | eggs/ 14 | .eggs/ 15 | lib/ 16 | lib64/ 17 | parts/ 18 | sdist/ 19 | var/ 20 | wheels/ 21 | pip-wheel-metadata/ 22 | share/python-wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | ## Logs 29 | ovisbot.log -------------------------------------------------------------------------------- /docs/_build/html/_static/documentation_options.js: -------------------------------------------------------------------------------- 1 | var DOCUMENTATION_OPTIONS = { 2 | URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), 3 | VERSION: '1.0.0', 4 | LANGUAGE: 'None', 5 | COLLAPSE_INDEX: false, 6 | BUILDER: 'html', 7 | FILE_SUFFIX: '.html', 8 | HAS_SOURCE: true, 9 | SOURCELINK_SUFFIX: '.txt', 10 | NAVIGATION_WITH_KEYS: false 11 | }; -------------------------------------------------------------------------------- /ovisbot/__main__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import ovisbot.utils.logging 3 | 4 | from colorama import init as colorama_init 5 | 6 | from ovisbot.locale import setup_locale 7 | from ovisbot.bot import OvisBot 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | def launch(): 13 | colorama_init(autoreset=True) 14 | setup_locale() 15 | 16 | bot = OvisBot() 17 | bot.launch() 18 | 19 | 20 | if __name__ == "__main__": 21 | launch() 22 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | bot: 5 | build: . 6 | container_name: 'ovisbot' 7 | restart: always 8 | volumes: 9 | - ./ovisbot:/ovisbot/ovisbot 10 | working_dir: /ovisbot 11 | command: pipenv run ovisbot run 12 | links: 13 | - mongo 14 | mongo: 15 | image: mongo:6 16 | restart: always 17 | volumes: 18 | - db-data:/data/db 19 | volumes: 20 | db-data: 21 | driver: local 22 | -------------------------------------------------------------------------------- /logging.ini: -------------------------------------------------------------------------------- 1 | [loggers] 2 | keys=root 3 | 4 | [handlers] 5 | keys=consoleHandler, fileHandler 6 | 7 | [formatters] 8 | keys=simpleFormatter 9 | 10 | [logger_root] 11 | level=INFO 12 | handlers=fileHandler, consoleHandler 13 | 14 | [handler_consoleHandler] 15 | class=StreamHandler 16 | level=INFO 17 | formatter=simpleFormatter 18 | args=(sys.stdout,) 19 | 20 | [handler_fileHandler] 21 | class=FileHandler 22 | level=DEBUG 23 | formatter=simpleFormatter 24 | args=("ovisbot.log",) 25 | 26 | [formatter_simpleFormatter] 27 | format=%(asctime)s %(name)s - %(levelname)s:%(message)s -------------------------------------------------------------------------------- /ovisbot/locale.py: -------------------------------------------------------------------------------- 1 | import gettext 2 | import logging 3 | import os 4 | import pytz 5 | import time 6 | 7 | logger = logging.getLogger(__name__) 8 | tz = pytz.timezone("Europe/Athens") 9 | _ = None 10 | 11 | 12 | def setup_locale(): 13 | global _ 14 | os.environ["TZ"] = "Europe/Athens" 15 | time.tzset() 16 | localedir = os.path.join(os.path.abspath(os.path.dirname(__file__)), "locale") 17 | translate = gettext.translation( 18 | "ovisbot", localedir, languages=["cy"], fallback=True 19 | ) 20 | translate.install() 21 | _ = translate.gettext 22 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. OvisBot documentation master file, created by 2 | sphinx-quickstart on Fri Mar 27 14:05:07 2020. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to OvisBot's documentation! 7 | =================================== 8 | 9 | .. automodule:: ovisbot.core 10 | :members: 11 | 12 | .. toctree:: 13 | :maxdepth: 2 14 | :caption: Contents: 15 | 16 | 17 | 18 | Indices and tables 19 | ================== 20 | 21 | * :ref:`genindex` 22 | * :ref:`modindex` 23 | * :ref:`search` 24 | -------------------------------------------------------------------------------- /docs/_build/html/_sources/index.rst.txt: -------------------------------------------------------------------------------- 1 | .. OvisBot documentation master file, created by 2 | sphinx-quickstart on Fri Mar 27 14:05:07 2020. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to OvisBot's documentation! 7 | =================================== 8 | 9 | .. automodule:: ovisbot.core 10 | :members: 11 | 12 | .. toctree:: 13 | :maxdepth: 2 14 | :caption: Contents: 15 | 16 | 17 | 18 | Indices and tables 19 | ================== 20 | 21 | * :ref:`genindex` 22 | * :ref:`modindex` 23 | * :ref:`search` 24 | -------------------------------------------------------------------------------- /ovisbot/todo-fixes.md: -------------------------------------------------------------------------------- 1 | --- 2 | Title: Bug Fixes / Code Suggestions 3 | Author: ishtar 4 | --- 5 | 6 | + Add comments 7 | 8 | + Define constants for error messages: 9 | ``` 10 | ctf.py(271, 17) 11 | ctf.py(501, 17) 12 | ctf.py(536, 17) 13 | ctf.py(599, 17) 14 | ctf.py(685, 17) 15 | ctf.py(770, 17) 16 | ctf.py(797, 17) 17 | ``` 18 | 19 | + Type hinting for static analysis 20 | ```py 21 | def add(x:int, y:int) -> int: 22 | return x + y 23 | ``` 24 | 25 | + Some code raises exceptions and some others not. I suggest to follow the exceptions model. 26 | ``` 27 | https://github.com/cybermouflons/ovisbot/blob/09c475c12a5b00789226e0d7e4b1a0d31263c82c/ovisbot/extensions/ctf/ctf.py#L646 28 | ``` -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /.github/workflows/pypi_publish.yml: -------------------------------------------------------------------------------- 1 | name: PyPI Publish 2 | 3 | on: 4 | release: 5 | types: # This configuration does not affect the page_build event above 6 | - created 7 | 8 | jobs: 9 | publish: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: '3.7' 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install pipenv 23 | pipenv install --dev 24 | - name: Build and publish 25 | env: 26 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 27 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 28 | run: | 29 | pipenv run build 30 | pipenv run publish -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/_build/html/searchindex.js: -------------------------------------------------------------------------------- 1 | Search.setIndex({docnames:["index"],envversion:{"sphinx.domains.c":1,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":1,"sphinx.domains.index":1,"sphinx.domains.javascript":1,"sphinx.domains.math":2,"sphinx.domains.python":1,"sphinx.domains.rst":1,"sphinx.domains.std":1,sphinx:56},filenames:["index.rst"],objects:{ovisbot:{core:[0,0,0,"-"]}},objnames:{"0":["py","module","Python module"]},objtypes:{"0":"py:module"},terms:{"function":[],"import":[],"int":[],"return":[],"true":[],The:0,about:[],about_m:[],accord:[],annot:[],attribut:[],bool:[],docstr:[],exampl:0,fals:[],first:[],function_with_types_in_docstr:[],includ:[],index:0,modul:0,most:[],name:[],need:[],otherwis:[],page:0,param1:[],param2:[],param:[],paramet:[],pep:[],person:[],project:0,search:0,second:[],str:[],string:[],success:[],support:[],thei:[],thing:[],type:[],valu:[],your_nam:[]},titles:["Welcome to OvisBot\u2019s documentation!"],titleterms:{core:0,document:0,indic:0,ovisbot:0,tabl:0,welcom:0}}) -------------------------------------------------------------------------------- /ovisbot/utils/progressbar.py: -------------------------------------------------------------------------------- 1 | # Adopted from https://github.com/Changaco/unicode-progress-bars 2 | 3 | import sys 4 | import math 5 | 6 | bar_styles = [ 7 | "▁▂▃▄▅▆▇█", 8 | "⣀⣄⣤⣦⣶⣷⣿", 9 | "⣀⣄⣆⣇⣧⣷⣿", 10 | "○◔◐◕⬤", 11 | "□◱◧▣■", 12 | "□◱▨▩■", 13 | "□◱▥▦■", 14 | "░▒▓█", 15 | "░█", 16 | "⬜⬛", 17 | "▱▰", 18 | "▭◼", 19 | "▯▮", 20 | "◯⬤", 21 | "⚪⚫", 22 | ] 23 | 24 | 25 | def draw_bar(value, max_value=100, barsize=10, style=3, label=""): 26 | percentage = 0 if max_value == 0 else value / max_value 27 | progress = round(barsize * percentage) 28 | empty_progress = barsize - progress 29 | style_symbols = bar_styles[style % len(bar_styles)] 30 | full_symbol = style_symbols[-1] 31 | empty_symbol = style_symbols[0] 32 | progress_text = [full_symbol] * progress 33 | empty_text = [empty_symbol] * empty_progress 34 | percentage_text = "{0}%".format(round(percentage * 100)) 35 | bar = label + " [" + " ".join(progress_text + empty_text) + "] " + percentage_text 36 | return bar 37 | -------------------------------------------------------------------------------- /CONTRIBUTE.md: -------------------------------------------------------------------------------- 1 | # Setting up a development environment 2 | 3 | 1. Create a new [application with Discord](https://discordapp.com/developers/applications/) 4 | 2. Select "Bot" from the settings side bar on the left 5 | 3. Name your bot 🤖 6 | 4. Copy the Token provided by Discord for your bot (this is sensitive and should be kept secret) 7 | 5. Create a file named `.env` at the top level of the project, this file must include all the required environment variables 8 | - `DISCORD_BOT_TOKEN` is the token copied at step 4 9 | - You can see `.env.example` for the expected environment variables. 10 | - If any of the required ones are missing, you'll run into this error "Couldn't launch bot. Not configured properly" 11 | 6. Invite the bot to your Discord channel ([Instructions](https://github.com/jagrosh/MusicBot/wiki/Adding-Your-Bot-To-Your-Server)) 12 | 7. From the top level of the project run `docker-compose up` (or `docker-compose up --build` to also re-build images) 13 | 8. Once the bot is ready you'll see its status as Online in your channel. That means it's up and ready to start receiving commands 🎉 14 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | bump2version = "*" 8 | setuptools = "*" 9 | wheel = "*" 10 | twine = "*" 11 | secretstorage = {markers = "sys_platform == 'linux'"} 12 | keyring = "==19.1.0" 13 | black = "==19.10b0" 14 | pipenv-setup = "*" 15 | pytest = "*" 16 | ovisbot = {editable = true,path = "."} 17 | flake8 = "*" 18 | sphinx = "*" 19 | 20 | [packages] 21 | discord-py = "2.3.2" 22 | colorama = "*" 23 | pytz = "*" 24 | python-dotenv = "*" 25 | pymodm = "*" 26 | texttable = "*" 27 | requests = "*" 28 | colorthief = "*" 29 | beautifulsoup4 = "*" 30 | parse = "*" 31 | pycryptodome = "*" 32 | gitpython = "*" 33 | pynacl = "*" 34 | python-dateutil = "*" 35 | click = "*" 36 | 37 | [requires] 38 | python_version = "3.9" 39 | 40 | [scripts] 41 | build = "python setup.py sdist bdist_wheel" 42 | publish = "twine upload dist/*" 43 | reformat = "bash -c \"python -m black $(git ls-files '*.py')\"" 44 | setupenv = "python tools/setupenv.py" 45 | setup-sync = "pipenv-setup sync" 46 | docker = "docker-compose up" 47 | docker-prod = "docker-compose -f docker-compose.yml -f docker-compose.prod.yml up" 48 | -------------------------------------------------------------------------------- /ovisbot/exceptions.py: -------------------------------------------------------------------------------- 1 | class ChallengeDoesNotExistException(Exception): 2 | pass 3 | 4 | 5 | class ChallengeExistsException(Exception): 6 | pass 7 | 8 | 9 | class ChallengeInvalidCategory(Exception): 10 | pass 11 | 12 | class ChallengeInvalidDifficulty(Exception): 13 | pass 14 | 15 | class ChallengeAlreadySolvedException(Exception): 16 | def __init__(self, solved_by, *args, **kwargs): 17 | self.solved_by = solved_by 18 | 19 | 20 | class ChallengeNotSolvedException(Exception): 21 | pass 22 | 23 | 24 | class UserAlreadyInChallengeChannelException(Exception): 25 | pass 26 | 27 | 28 | class FewParametersException(Exception): 29 | pass 30 | 31 | 32 | class NotInChallengeChannelException(Exception): 33 | pass 34 | 35 | 36 | class CTFAlreadyExistsException(Exception): 37 | pass 38 | 39 | 40 | class CTFSharedCredentialsNotSet(Exception): 41 | pass 42 | 43 | 44 | class CTFAlreadyFinishedException(Exception): 45 | pass 46 | 47 | 48 | class CtfimeNameDoesNotMatch(Exception): 49 | pass 50 | 51 | 52 | class DateMisconfiguredException(Exception): 53 | pass 54 | 55 | 56 | class MissingStartDateException(Exception): 57 | pass 58 | 59 | 60 | class MissingEndDateException(Exception): 61 | pass 62 | 63 | 64 | class CryptoHackApiException(Exception): 65 | pass 66 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import setup 4 | 5 | kwargs = { 6 | "include_package_data": True, 7 | } 8 | 9 | if os.getenv("READTHEDOCS", False): 10 | setup( 11 | install_requires=[ 12 | "aiohttp==3.6.2", 13 | "async-timeout==3.0.1", 14 | "attrs==20.1.0", 15 | "beautifulsoup4==4.9.1", 16 | "certifi==2020.6.20", 17 | "cffi==1.14.2", 18 | "chardet==3.0.4", 19 | "colorama==0.4.3", 20 | "colorthief==0.2.1", 21 | "discord-py==1.4.1", 22 | "gitdb==4.0.5", 23 | "gitpython==3.1.7", 24 | "idna==2.10", 25 | "multidict==4.7.6", 26 | "parse==1.17.0", 27 | "pillow==7.2.0", 28 | "pycparser==2.20", 29 | "pycryptodome==3.9.8", 30 | "pymodm==0.4.3", 31 | "pymongo==3.11.0", 32 | "pynacl==1.4.0", 33 | "python-dateutil==2.8.1", 34 | "python-dotenv==0.14.0", 35 | "pytz==2020.1", 36 | "requests==2.24.0", 37 | "six==1.15.0", 38 | "smmap==3.0.4", 39 | "soupsieve==2.0.1", 40 | "texttable==1.6.2", 41 | "typing-extensions==3.7.4.3; python_version < '3.8'", 42 | "urllib3==1.25.10", 43 | "yarl==1.5.1", 44 | ], 45 | **kwargs, 46 | python_requires=">=3.7", 47 | ) 48 | else: 49 | setup(**kwargs) 50 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = ovisbot 3 | version = attr: ovisbot.__version__ 4 | description = A modular Discord bot for CTF teams 5 | license = GPL-3.0 6 | long_description = file: README.md 7 | long_description_content_type = text/markdown; charset=UTF-8; variant=GFM 8 | author = CYberMouflons 9 | author_email = info@cybermouflons.com 10 | url = https://github.com/cybermouflons/ovisbot 11 | classifiers = 12 | # List at https://pypi.org/pypi?%3Aaction=list_classifiers 13 | Development Status :: 5 - Production/Stable 14 | Framework :: AsyncIO 15 | Framework :: Pytest 16 | Intended Audience :: Developers 17 | Intended Audience :: End Users/Desktop 18 | License :: OSI Approved :: GNU General Public License v3 (GPLv3) 19 | Natural Language :: English 20 | Operating System :: OS Independent 21 | Programming Language :: Python :: 3.7 22 | Topic :: Communications :: Chat 23 | Topic :: Documentation :: Sphinx 24 | 25 | [options] 26 | packages = ovisbot 27 | python_requires = >=3.7.0 28 | 29 | [options.entry_points] 30 | console_scripts = 31 | ovisbot=ovisbot.cli:cli 32 | 33 | [options.packages.find] 34 | include = 35 | ovisbot 36 | ovisbot.* 37 | 38 | [extract_messages] 39 | input_dirs = ovisbot 40 | output_file = ovisbot/locale/ovisbot.pot 41 | 42 | [flake8] 43 | exclude = 44 | .git, 45 | __pycache__, 46 | .pytest_cache, 47 | venv 48 | ignore = E203, E266, E501, W503, F403, F401, W605 49 | # Put Error/Style codes here e.g. H301 50 | select = B,C,E,F,W,T4,B9 51 | max-line-length = 89 52 | max-complexity = 18 53 | 54 | -------------------------------------------------------------------------------- /ovisbot/locale/ovisbot.pot: -------------------------------------------------------------------------------- 1 | # Translations template for OvisBot. 2 | # Copyright (C) 2020 ORGANIZATION 3 | # This file is distributed under the same license as the OvisBot project. 4 | # FIRST AUTHOR , 2020. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: OvisBot 1.0.0\n" 10 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 11 | "POT-Creation-Date: 2020-03-30 04:53+0100\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Generated-By: Babel 2.8.0\n" 19 | 20 | #: ovisbot/core.py:81 21 | msgid "Permission denied." 22 | msgstr "" 23 | 24 | #: ovisbot/core.py:83 ovisbot/core.py:85 25 | msgid "Command not found" 26 | msgstr "" 27 | 28 | #: ovisbot/core.py:87 29 | msgid "Something went wrong..." 30 | msgstr "" 31 | 32 | #: ovisbot/core.py:94 33 | msgid "What?!" 34 | msgstr "" 35 | 36 | #: ovisbot/core.py:112 37 | msgid "" 38 | "Hello {0}! Welcome to the server! Send {1}help to see a list of available" 39 | " commands" 40 | msgstr "" 41 | 42 | #: ovisbot/core.py:122 43 | msgid "Welcome {0}! Take your time to briefly introduce yourself" 44 | msgstr "" 45 | 46 | #: ovisbot/core.py:142 47 | msgid "" 48 | "Page {0} does not exist!\n" 49 | "Available pages: {1}" 50 | msgstr "" 51 | 52 | #: ovisbot/core.py:150 53 | msgid "Ask for help in direct message!" 54 | msgstr "" 55 | 56 | #: ovisbot/core.py:167 57 | msgid "CTF list is empty!" 58 | msgstr "" 59 | 60 | #: ovisbot/core.py:178 61 | msgid "Frappe on it's way...!" 62 | msgstr "" 63 | 64 | #: ovisbot/core.py:205 65 | msgid "DISCORD_BOT_TOKEN variable has not been set!" 66 | msgstr "" 67 | 68 | #: ovisbot/db_models.py:65 69 | msgid "" 70 | "No challenges found. Try adding one with `!ctf addchallenge " 71 | "`" 72 | msgstr "" 73 | 74 | -------------------------------------------------------------------------------- /ovisbot/error_handling.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | import ovisbot.locale as i18n 4 | 5 | from discord.ext.commands.errors import ( 6 | CommandNotFound, 7 | ExpectedClosingQuoteError, 8 | MissingPermissions, 9 | MissingRole, 10 | NoPrivateMessage, 11 | ) 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | def hook_error_handlers(bot): 17 | """ 18 | General error handlers for global / command errors 19 | """ 20 | 21 | @bot.event 22 | async def on_error(event, *args, **kwargs): 23 | logger.info("ON_ERROR") 24 | logger.info(sys.exc_info()) 25 | for arg in args: 26 | if isinstance(arg, Exception): 27 | raise arg 28 | 29 | @bot.event 30 | async def on_command_error(ctx, error): 31 | logger.info("ON_ERROR_COMMAND") 32 | if ctx.cog is not None: 33 | # Errors coming from cogs 34 | logger.info("Received cog exception: {0}".format(error)) 35 | raise error.original 36 | 37 | if isinstance(error, MissingPermissions): 38 | # Handle missing permissions 39 | await ctx.channel.send(i18n._("Permission denied.")) 40 | elif isinstance(error, NoPrivateMessage): 41 | await ctx.channel.send( 42 | i18n._("This command cannot be used in private messages.") 43 | ) 44 | elif isinstance(error, MissingRole): 45 | await ctx.channel.send( 46 | i18n._("You don't have the required role to run this") 47 | ) 48 | elif isinstance(error, CommandNotFound): 49 | await ctx.channel.send(i18n._("Command not found")) 50 | elif isinstance(error, ExpectedClosingQuoteError): 51 | await ctx.channel.send(i18n._("Missing a quote?")) 52 | else: 53 | # TODO: Send this only if the exception was not handled already... How to check this?? 54 | # await ctx.channel.send(i18n._("Something went wrong...")) 55 | if hasattr(error, "original"): 56 | raise error.original 57 | else: 58 | raise error 59 | -------------------------------------------------------------------------------- /ovisbot/events.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import discord 3 | import sys 4 | import ovisbot.locale as i18n 5 | 6 | from discord.ext.commands.errors import ( 7 | MissingPermissions, 8 | CommandNotFound, 9 | ExpectedClosingQuoteError, 10 | ) 11 | 12 | from ovisbot import __version__ 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | def hook_events(bot): 18 | @bot.event 19 | async def on_ready(): 20 | logger.info("discordpy: {0}".format(discord.__version__)) 21 | logger.info("<" + bot.user.name + " Online>") 22 | logger.info("OvisBot v{0}".format(__version__)) 23 | await bot.change_presence( 24 | activity=discord.Game(name="with your mind! Use !help") 25 | ) 26 | 27 | @bot.event 28 | async def on_message(message): 29 | if bot.user in message.mentions: 30 | await message.channel.send(i18n._("What?!")) 31 | await bot.process_commands(message) 32 | 33 | @bot.event 34 | async def on_message_edit(before, after): 35 | threshold = bot.config.COMMAND_CORRECTION_WINDOW 36 | if ( 37 | after.content != before.content 38 | and after.edited_at.timestamp() - after.created_at.timestamp() <= threshold 39 | ): 40 | await bot.process_commands(after) 41 | 42 | @bot.event 43 | async def on_member_join(member): 44 | await member.send( 45 | i18n._( 46 | "Hello {0}! Welcome to the server! Send {1}help to see a list of available commands".format( 47 | member.name, bot.command_prefix 48 | ) 49 | ) 50 | ) 51 | announcements = discord.utils.get( 52 | member.guild.text_channels, name="announcements" 53 | ) 54 | if announcements is not None: 55 | await announcements.send( 56 | ( 57 | i18n._( 58 | "Welcome {0}! Take a moment to briefly introduce yourbot".format( 59 | member.name 60 | ) 61 | ) 62 | ) 63 | ) 64 | -------------------------------------------------------------------------------- /.github/workflows/push_image_deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build and push image 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build-and-push: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout Repository 14 | uses: actions/checkout@v3 15 | 16 | - name: Login to GitHub Container Registry 17 | uses: docker/login-action@v3 18 | with: 19 | registry: ghcr.io 20 | username: ${{ github.actor }} 21 | password: ${{ secrets.GITHUB_TOKEN }} 22 | 23 | - name: Set up Docker Buildx 24 | uses: docker/setup-buildx-action@v2 25 | 26 | - name: Build and Push Docker Image 27 | uses: docker/build-push-action@v4 28 | with: 29 | context: . 30 | push: true 31 | tags: ghcr.io/cybermouflons/ovisbot:latest 32 | 33 | deploy: 34 | needs: build-and-push 35 | runs-on: ubuntu-latest 36 | environment: PROD 37 | steps: 38 | - name: Checkout Repository 39 | uses: actions/checkout@v3 40 | 41 | - name: Write Secret to .env File 42 | env: 43 | ENV_FILE: ${{ secrets.ENV_FILE }} 44 | run: | 45 | echo "$ENV_FILE" > .env 46 | echo ".env file created with the contents of ENV_FILE secret" 47 | 48 | - name: Deploy to Server 49 | uses: mai-space/action-sshpass-rsync@v1 50 | with: 51 | host: ${{ secrets.SSH_HOST }} 52 | user: ${{ secrets.SSH_USER }} 53 | port: 22 54 | pass: ${{ secrets.SSH_PASSWORD }} 55 | local: "." 56 | args: "-avz --exclude '.git'" 57 | remote: "~/ovisbot" 58 | runAfterDeployment: | 59 | cd ~/ovisbot 60 | if [ -f docker-compose.yml ]; then 61 | docker-compose -f docker-compose.yml -f docker-compose.prod.yml down || true 62 | docker-compose -f docker-compose.yml -f docker-compose.prod.yml pull 63 | docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d 64 | else 65 | echo "docker-compose.yml not found in the repository!" 66 | exit 1 67 | fi 68 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | 16 | sys.path.insert(0, os.path.abspath("../ovisbot")) 17 | 18 | 19 | # -- Project information ----------------------------------------------------- 20 | 21 | project = "OvisBot" 22 | copyright = "2020, CYberMouflons" 23 | author = "CYberMouflons" 24 | 25 | # The full version, including alpha/beta/rc tags 26 | release = "1.0.0" 27 | 28 | 29 | # -- General configuration --------------------------------------------------- 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = ["sphinx.ext.autodoc", "sphinx.ext.coverage", "sphinx.ext.napoleon"] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ["_templates"] 38 | 39 | # List of patterns, relative to source directory, that match files and 40 | # directories to ignore when looking for source files. 41 | # This pattern also affects html_static_path and html_extra_path. 42 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 43 | 44 | 45 | # -- Options for HTML output ------------------------------------------------- 46 | 47 | # The theme to use for HTML and HTML Help pages. See the documentation for 48 | # a list of builtin themes. 49 | # 50 | html_theme = "alabaster" 51 | 52 | # Add any paths that contain custom static files (such as style sheets) here, 53 | # relative to this directory. They are copied after the builtin static files, 54 | # so a file named "default.css" will overwrite the builtin "default.css". 55 | html_static_path = ["_static"] 56 | -------------------------------------------------------------------------------- /ovisbot/locale/cy/LC_MESSAGES/ovisbot.po: -------------------------------------------------------------------------------- 1 | # Translations template for OvisBot. 2 | # Copyright (C) 2020 ORGANIZATION 3 | # This file is distributed under the same license as the OvisBot project. 4 | # FIRST AUTHOR , 2020. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: OvisBot 1.0.0\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2020-03-26 23:43+0000\n" 11 | "PO-Revision-Date: 2020-03-26 23:56+0000\n" 12 | "Language-Team: \n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=UTF-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Generated-By: Babel 2.8.0\n" 17 | "X-Generator: Poedit 2.3\n" 18 | "Last-Translator: \n" 19 | "Language: cyp\n" 20 | 21 | #: ovisbot/core.py:70 22 | msgid "Permission denied." 23 | msgstr "" 24 | "Ops! You don’t have sufficient permissions to do that. Τζίλα το πάρακατω…" 25 | 26 | #: ovisbot/core.py:73 27 | msgid "Command not found" 28 | msgstr "Έφυε σου λλίο… Command not found" 29 | 30 | #: ovisbot/core.py:75 31 | msgid "Something went wrong..." 32 | msgstr "Ουπς. Κάτι επήε λάθος." 33 | 34 | #: ovisbot/core.py:82 35 | msgid "What?!" 36 | msgstr "Άφησ’ με! Μεν μου μάσσιεσαι…" 37 | 38 | #: ovisbot/core.py:99 39 | msgid "" 40 | "Hello {0}! Welcome to the server! Send {1}help to see a list of available " 41 | "commands" 42 | msgstr "" 43 | "Γειά σου {0}! Εγώ είμαι ο Ζόλος τζαι καμνω τα ούλλα. Στείλε !help να " 44 | "πάρεις μιαν ιδέα." 45 | 46 | #: ovisbot/core.py:105 47 | msgid "Welcome {0}! Take your time to briefly introduce yourself" 48 | msgstr "Καλωσόρισες {member.name}! Ατε πε μας 2 λοούθκια για σένα!!" 49 | 50 | #: ovisbot/core.py:122 51 | msgid "" 52 | "Page {0} does not exist!\n" 53 | "Available pages: {1}" 54 | msgstr "Μπούκκα ρε Τσιούη! Εν υπάρχει ετσί page({0})\\nAvailable pages: {1" 55 | 56 | #: ovisbot/core.py:130 57 | msgid "Ask for help in direct message!" 58 | msgstr "Κύριε Βειτερ!! Στείλε DM γιατί έπρησες μας τα!" 59 | 60 | #: ovisbot/core.py:147 61 | msgid "CTF list is empty!" 62 | msgstr "Μα σάννα τζιαι εν θωρώ κανένα CTF ρε παρέα μου." 63 | 64 | #: ovisbot/core.py:159 65 | msgid "Frappe on it's way...!" 66 | msgstr "Έφτασεεεεν … Ρούφα τζαι έρκετε!" 67 | 68 | #: ovisbot/core.py:186 69 | msgid "DISCORD_BOT_TOKEN variable has not been set!" 70 | msgstr "DISCORD_BOT_TOKEN variable has not been set!" 71 | 72 | #: ovisbot/db_models.py:62 73 | msgid "" 74 | "No challenges found. Try adding one with `!ctf addchallenge " 75 | "`" 76 | msgstr "" 77 | "No challenges found. Try adding one with `!ctf addchallenge " 78 | "`" 79 | -------------------------------------------------------------------------------- /ovisbot/helpers.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import requests 3 | import os 4 | import urllib.parse 5 | 6 | from discord.ext.commands.core import GroupMixin 7 | from texttable import Texttable 8 | 9 | 10 | def chunkify(text, limit): 11 | chunks = [] 12 | while len(text) > limit: 13 | idx = text.index("\n", limit) 14 | chunks.append(text[:idx]) 15 | text = text[idx:] 16 | chunks.append(text) 17 | return chunks 18 | 19 | 20 | def escape_md(text): 21 | return text.replace("_", "\_").replace("*", "\*").replace(">>>", "\>>>") 22 | 23 | 24 | def create_corimd_notebook(): 25 | base_url = "https://notes.status.im/" 26 | create_new_note_url = base_url + "new" 27 | res = requests.get(create_new_note_url) 28 | return res.url 29 | 30 | 31 | def wolfram_simple_query(query, app_id): 32 | base_url = "https://api.wolframalpha.com/v2/result?i={0}&appid={1}" 33 | query_url = base_url.format(urllib.parse.quote(query), app_id) 34 | return requests.get(query_url).text 35 | 36 | 37 | def td_format(td_object): 38 | ## Straight copy paste from here: https://stackoverflow.com/questions/538666/format-timedelta-to-string 39 | seconds = int(td_object.total_seconds()) 40 | periods = [ 41 | ("year", 60 * 60 * 24 * 365), 42 | ("month", 60 * 60 * 24 * 30), 43 | ("day", 60 * 60 * 24), 44 | ("hour", 60 * 60), 45 | ("minute", 60), 46 | ("second", 1), 47 | ] 48 | 49 | strings = [] 50 | for period_name, period_seconds in periods: 51 | if seconds > period_seconds: 52 | period_value, seconds = divmod(seconds, period_seconds) 53 | has_s = "s" if period_value > 1 else "" 54 | strings.append("%s %s%s" % (period_value, period_name, has_s)) 55 | 56 | return ", ".join(strings) 57 | 58 | 59 | def get_props(obj): 60 | """Returns properties of the class""" 61 | return inspect.getmembers(obj, lambda a: not (inspect.isroutine(a))) 62 | 63 | 64 | def draw_options_table(options): 65 | table = Texttable() 66 | table.set_deco(Texttable.VLINES | Texttable.HEADER | Texttable.HLINES) 67 | table.set_cols_dtype(["a", "a"]) # automatic 68 | table.set_cols_align(["l", "l"]) 69 | table.add_rows( 70 | [ 71 | ["name", "value"], 72 | *[[name, val] for name, val in options], 73 | ] 74 | ) 75 | return table.draw() 76 | 77 | 78 | async def success(message): 79 | await message.add_reaction("✅") 80 | 81 | 82 | async def failed(message): 83 | await message.add_reaction("❌") 84 | -------------------------------------------------------------------------------- /ovisbot/extensions/poll/poll.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import discord 3 | 4 | from discord.ext.commands import errors as discorderr 5 | from discord.ext import commands 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class Poll(commands.Cog): 11 | def __init__(self, bot): 12 | self.bot = bot 13 | 14 | @commands.group() 15 | async def poll(self, ctx): 16 | """ 17 | Collection of commands to create polls 18 | """ 19 | if ctx.invoked_subcommand is None: 20 | await ctx.send("Invalid command passed. Use `!help {0}`".format(__name__)) 21 | 22 | @poll.command() 23 | async def binary(self, ctx, question): 24 | """ 25 | Creates a binary poll. 26 | """ 27 | poll_q = question 28 | 29 | embed = discord.Embed(title=poll_q, description="", color=int("00fbf5", 16)) 30 | embed.add_field( 31 | name="Choices:", value=":thumbsup:\tYES\n\n:thumbsdown:\tNO\n\n" 32 | ) 33 | msg = await ctx.channel.send(embed=embed) 34 | await msg.add_reaction("👍") 35 | await msg.add_reaction("👎") 36 | 37 | @poll.command() 38 | async def multichoice(self, ctx, question, *options): 39 | """ 40 | Creates a multichoice poll 41 | """ 42 | poll_q = question 43 | options = options 44 | option_symbols = ["🇦", "🇧", "🇨", "🇩", "🇪", "🇫", "🇬", "🇭"] 45 | 46 | if len(options) > len(option_symbols): 47 | await ctx.send( 48 | "This commands supports only {0} options".format(len(option_symbols)) 49 | ) 50 | 51 | embed = discord.Embed(title=poll_q, description="", color=int("00fbf5", 16)) 52 | options_str = "\n\n".join( 53 | "{0} \t{1}".format(s, o) for s, o in zip(option_symbols, options) 54 | ) 55 | 56 | embed.add_field(name="Choices:", value=options_str + "\n\n") 57 | msg = await ctx.channel.send(embed=embed) 58 | for sym in option_symbols[: len(options)]: 59 | await msg.add_reaction(sym) 60 | 61 | @commands.Cog.listener() 62 | async def on_command_error(self, ctx, error): 63 | if isinstance(error, commands.errors.MissingRequiredArgument): 64 | self.bot.help_command.context = ctx 65 | await self.bot.help_command.command_callback(ctx, command=str(ctx.command)) 66 | 67 | if isinstance(error, discorderr.ExpectedClosingQuoteError): 68 | await ctx.channel.send("Expected closing quote.") 69 | 70 | 71 | async def setup(bot): 72 | await bot.add_cog(Poll(bot)) 73 | -------------------------------------------------------------------------------- /ovisbot/extensions/stats/stats.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import struct 3 | from ovisbot.extensions.ctf.ctf import CHALLENGE_CATEGORIES 4 | from ovisbot.db_models import CTF, Challenge 5 | from ovisbot.utils.progressbar import draw_bar 6 | from discord.ext import commands 7 | 8 | logging.basicConfig(level=logging.INFO) 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class Stats(commands.Cog): 13 | def __init__(self, bot): 14 | self.bot = bot 15 | 16 | @commands.group() 17 | async def stats(self, ctx): 18 | """ 19 | Collection of commands for player statistics 20 | """ 21 | if ctx.invoked_subcommand is None: 22 | await ctx.send("Invalid command passed. Use `!help`.") 23 | 24 | @stats.command() 25 | async def me(self, ctx, *params): 26 | """ 27 | Displays your CTF statistics. Use --style to change text format. 28 | """ 29 | style = 5 30 | for p in params: 31 | try: 32 | arg = "--style" 33 | if arg in p: 34 | style = int(p[p.index(arg) + len(arg) + 1 :]) 35 | except ValueError: 36 | await ctx.send("Ούλλο μαλακίες είσαι....") 37 | return 38 | 39 | author = ctx.message.author.name 40 | ctfs = CTF.objects.aggregate( 41 | {"$match": {"challenges.solved_by": {"$eq": author}}}, 42 | {"$unwind": "$challenges"}, 43 | {"$match": {"challenges.solved_by": {"$eq": author}}}, 44 | ) 45 | 46 | categories_solved = {k: 0 for k in CHALLENGE_CATEGORIES} 47 | for ctf in ctfs: 48 | # only look at the first category tag - in case of multiple tags 49 | tag = ctf["challenges"]["tags"][0] 50 | if tag.lower() not in categories_solved: 51 | continue 52 | categories_solved[tag] += 1 53 | 54 | total = sum(categories_solved.values()) 55 | mx = max(categories_solved.values()) 56 | 57 | to_ret = "\n".join( 58 | [ 59 | f"{draw_bar(categories_solved[k], mx, style=style)} {k.upper()} x{categories_solved[k]}" 60 | for k in CHALLENGE_CATEGORIES 61 | ] 62 | ) 63 | to_ret = "Total {0} Challenge(s) Solved!\n\n".format(total) + to_ret 64 | 65 | preambles = [ 66 | "👶 Είσαι νινί ακόμα.", # 0-24 solved 67 | "👍 Κουτσά στραβά, κάτι καμνεις.", # 15-49 solved 68 | "🐐👑 Μα εσού είσαι αρχιτράουλλος!", # 50+ solved 69 | ] 70 | p_choice = preambles[min(int(total / 25), len(preambles) - 1)] 71 | await ctx.send(f"{p_choice}\n```CSS\n{to_ret}```") 72 | 73 | 74 | async def setup(bot): 75 | await bot.add_cog(Stats(bot)) 76 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | schedule: 15 | - cron: '0 8 * * 1' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Override automatic language detection by changing the below list 26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 27 | language: ['python'] 28 | # Learn more... 29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v2 34 | with: 35 | # We must fetch at least the immediate parents so that if this is 36 | # a pull request then we can checkout the head. 37 | fetch-depth: 2 38 | 39 | # If this run was triggered by a pull request event, then checkout 40 | # the head of the pull request instead of the merge commit. 41 | - run: git checkout HEAD^2 42 | if: ${{ github.event_name == 'pull_request' }} 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /ovisbot/commands/base.py: -------------------------------------------------------------------------------- 1 | import discord 2 | import logging 3 | import requests 4 | import ovisbot.locale as i18n 5 | 6 | from ovisbot.helpers import chunkify, wolfram_simple_query 7 | from ovisbot.db_models import CTF, Challenge, BotConfig 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class BaseCommandsMixin: 13 | def load_commands(self): 14 | """Hooks commands with bot subclass""" 15 | bot = self 16 | 17 | @bot.command() 18 | async def ping(ctx): 19 | """ 20 | Ping.... Pong.... 21 | """ 22 | await ctx.channel.send("pong") 23 | 24 | @bot.command() 25 | async def frappe(ctx): 26 | """ 27 | Orders a cold frappe! 28 | """ 29 | await ctx.channel.send(i18n._("Frappe on it's way...!")) 30 | 31 | @bot.command() 32 | async def wolfram(ctx, query): 33 | """ 34 | Ask wolfram anything you want 35 | """ 36 | await ctx.channel.send( 37 | wolfram_simple_query(query, bot.config.WOLFRAM_ALPHA_APP_ID) 38 | ) 39 | 40 | @bot.command() 41 | async def chucknorris(ctx): 42 | """ 43 | Tells a chunk norris joke 44 | """ 45 | joke_url = "http://api.icndb.com/jokes/random" 46 | response = requests.get(joke_url) 47 | data = response.json() 48 | await ctx.channel.send(data["value"]["joke"]) 49 | 50 | @bot.command() 51 | async def contribute(ctx): 52 | """ 53 | Shows contribute information 54 | """ 55 | await ctx.channel.send("https://github.com/cybermouflons/ovisbot") 56 | 57 | @bot.command() 58 | async def status(ctx): 59 | """ 60 | Shows any ongoing, scheduled, finished CTFs in the server. 61 | """ 62 | status_response = "" 63 | ctfs = sorted([c for c in ctx.guild.categories], key=lambda x: x.created_at) 64 | for ctf in ctfs: 65 | try: 66 | ctf_doc = CTF.objects.get({"name": ctf.name}) 67 | except CTF.DoesNotExist: 68 | continue 69 | # await ctx.channel.send(f"{ctf.name} was not in the DB") 70 | ctfrole = discord.utils.get(ctx.guild.roles, name="Team-" + ctf.name) 71 | status_response += ctf_doc.status(len(ctfrole.members)) 72 | 73 | if len(status_response) == 0: 74 | status_response = i18n._("CTF list is empty!") 75 | await ctx.channel.send(status_response) 76 | return 77 | 78 | for chunk in chunkify(status_response, 1900): 79 | emb = discord.Embed(description=chunk, colour=4387968) 80 | await ctx.channel.send(embed=emb) 81 | -------------------------------------------------------------------------------- /ovisbot/cli.py: -------------------------------------------------------------------------------- 1 | import os 2 | import click 3 | import logging 4 | 5 | from itertools import chain 6 | from pymodm import connect 7 | from dotenv import load_dotenv 8 | 9 | from ovisbot import __version__ 10 | from ovisbot.__main__ import launch 11 | from ovisbot.helpers import draw_options_table 12 | from pymongo.errors import ServerSelectionTimeoutError 13 | 14 | load_dotenv(dotenv_path=os.path.join(os.getcwd(), ".env")) 15 | 16 | from ovisbot.config import ConfigurableProperty, bot_config # noqa: E402 17 | 18 | 19 | @click.group() 20 | @click.option("--env", default="prod", help="Environment to use") 21 | @click.option("--verbose", is_flag=True, default=False, help="Show verbose information") 22 | @click.pass_context 23 | def cli(ctx, env, verbose): 24 | # if not verbose: 25 | # logging.config.dictConfig(config={'version': 1, 'level': logging.NOTSET}) 26 | ctx.ensure_object(dict) 27 | ctx.obj["env"] = env 28 | 29 | 30 | @cli.command() 31 | @click.pass_context 32 | def config(ctx): 33 | """Shows current bot config (Requires DB connection).""" 34 | try: 35 | config_cls = bot_config[ctx.obj.get("env")] 36 | connect(config_cls.DB_URL) 37 | config = config_cls() 38 | except ServerSelectionTimeoutError: 39 | logging.error( 40 | "Database timeout error! Make use an instance of mongodb is running and your OVISBOT_DB_URL env variable is valid! Terminating... " 41 | ) 42 | exit(1) 43 | 44 | click.echo( 45 | draw_options_table( 46 | chain( 47 | config._get_configurable_props_from_cls(), 48 | config._get_static_props_from_cls(), 49 | ) 50 | ) 51 | ) 52 | 53 | 54 | @cli.command() 55 | @click.pass_context 56 | def setupenv(ctx): 57 | """Setup environment variables to launch bot""" 58 | config_cls = bot_config[ctx.obj.get("env")] 59 | env = {} 60 | for param in dir(config_cls): 61 | if param.isupper(): 62 | default = getattr(config_cls, param) 63 | val = ( 64 | input( 65 | "Please enter value for OVISBOT_{0} [default: {1}]:".format( 66 | param, default 67 | ) 68 | ) 69 | or default 70 | ) 71 | env[param] = val 72 | 73 | ROOT_DIR = os.path.abspath(os.curdir) 74 | 75 | with open(os.path.join(ROOT_DIR, ".env"), "w") as f: 76 | for k, v in env.items(): 77 | if not ( 78 | v is None or (isinstance(v, ConfigurableProperty) and v.value is None) 79 | ): 80 | f.write("OVISBOT_{0}={1}\n".format(k, v)) 81 | 82 | 83 | @cli.command() 84 | def run(): 85 | """Launch the bot (Require DB connection)""" 86 | launch() 87 | 88 | 89 | @cli.command() 90 | def version(): 91 | """Displays bot version""" 92 | click.echo(f"OvisBot - v{__version__}") 93 | -------------------------------------------------------------------------------- /ovisbot/bot.py: -------------------------------------------------------------------------------- 1 | import os 2 | import discord 3 | import sys 4 | import logging 5 | import ovisbot.locale as i18n 6 | 7 | from discord.ext.commands import Bot 8 | from dotenv import load_dotenv 9 | from pathlib import Path 10 | from os import environ 11 | from typing import NoReturn 12 | from pymodm import connect 13 | from pymongo.errors import ServerSelectionTimeoutError 14 | from colorama import Fore 15 | 16 | from ovisbot import __version__ 17 | from ovisbot import commands 18 | from ovisbot.cog_manager import CogManager 19 | from ovisbot.events import hook_events 20 | from ovisbot.error_handling import hook_error_handlers 21 | from ovisbot.commands.base import BaseCommandsMixin 22 | from ovisbot.commands.rank import RankCommandsMixin 23 | from ovisbot.commands.manage import ManageCommandsMixin 24 | from discord.ext.commands.errors import ExtensionNotFound 25 | 26 | logger = logging.getLogger(__name__) 27 | 28 | 29 | class OvisBot(Bot, BaseCommandsMixin, RankCommandsMixin, ManageCommandsMixin): 30 | def __init__(self, *args, **kwargs): 31 | # Load Config 32 | env_path = os.path.join( 33 | Path(sys.modules[__name__].__file__).resolve( 34 | ).parent.parent, ".env" 35 | ) 36 | load_dotenv(dotenv_path=env_path, verbose=True) 37 | env = environ.get("OVISBOT_ENV", "dev") 38 | from ovisbot.config import bot_config 39 | 40 | try: 41 | self.config_cls = bot_config[env] 42 | self.init_db() 43 | self.config = self.config_cls() 44 | except ServerSelectionTimeoutError: 45 | logging.error( 46 | "Database timeout error! Make use an instance of mongodb is running and your OVISBOT_DB_URL env variable is valid! Terminating... " 47 | ) 48 | exit(1) 49 | 50 | intents = discord.Intents.all() 51 | super().__init__(*args, command_prefix=self.config.COMMAND_PREFIX, 52 | intents=intents, **kwargs) 53 | 54 | # Perform necessary tasks 55 | Path(self.config.THIRD_PARTY_COGS_INSTALL_DIR).mkdir( 56 | parents=True, exist_ok=True 57 | ) 58 | 59 | # Hook commands 60 | BaseCommandsMixin.load_commands(self) 61 | RankCommandsMixin.load_commands(self) 62 | ManageCommandsMixin.load_commands(self) 63 | 64 | # Error Handling & Events 65 | hook_error_handlers(self) 66 | hook_events(self) 67 | 68 | async def setup_hook(self): 69 | # Load Cogs 70 | try: 71 | self.cog_manager = CogManager(self) 72 | await self.cog_manager.load_cogs() 73 | except ExtensionNotFound: 74 | pass 75 | 76 | def launch(self) -> None: 77 | logger.info("Launching bot...") 78 | 79 | if self.config.DISCORD_BOT_TOKEN is None: 80 | raise ValueError( 81 | i18n._("DISCORD_BOT_TOKEN variable has not been set!")) 82 | self.run(self.config.DISCORD_BOT_TOKEN) 83 | 84 | def init_db(self) -> NoReturn: 85 | """Initializes db connection""" 86 | connect(self.config_cls.DB_URL) 87 | -------------------------------------------------------------------------------- /docs/_build/html/genindex.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Index — OvisBot 1.0.0 documentation 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
30 | 31 | 32 |
33 | 34 | 35 |

Index

36 | 37 |
38 | O 39 | 40 |
41 |

O

42 | 43 | 47 |
48 | 49 | 50 | 51 |
52 | 53 |
54 |
55 | 95 |
96 |
97 | 105 | 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /docs/_build/html/search.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Search — OvisBot 1.0.0 documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
32 |
33 |
34 | 35 | 36 |
37 | 38 |

Search

39 |
40 | 41 |

42 | Please activate JavaScript to enable the search 43 | functionality. 44 |

45 |
46 |

47 | From here you can search these documents. Enter your search 48 | words into the box below and click "search". Note that the search 49 | function will automatically search for all of the words. Pages 50 | containing fewer words won't appear in the result list. 51 |

52 |
53 | 54 | 55 | 56 |
57 | 58 |
59 | 60 |
61 | 62 |
63 | 64 |
65 |
66 | 96 |
97 |
98 | 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /docs/_build/html/py-modindex.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Python Module Index — OvisBot 1.0.0 documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |
31 |
32 | 33 | 34 |
35 | 36 | 37 |

Python Module Index

38 | 39 |
40 | o 41 |
42 | 43 | 44 | 45 | 47 | 48 | 50 | 53 | 54 | 55 | 58 |
 
46 | o
51 | ovisbot 52 |
    56 | ovisbot.core 57 |
59 | 60 | 61 |
62 | 63 |
64 |
65 | 105 |
106 |
107 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /docs/_build/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Welcome to OvisBot’s documentation! — OvisBot 1.0.0 documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 |
29 | 30 | 31 |
32 | 33 |
34 |

Welcome to OvisBot’s documentation!

35 |
36 |

core.py

37 |

The core module of my example project

38 |
39 |
40 |
41 |
42 |
43 |

Indices and tables

44 | 49 |
50 | 51 | 52 |
53 | 54 |
55 |
56 | 96 |
97 |
98 | 109 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /ovisbot/extensions/utils/utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from ovisbot.helpers import chunkify 3 | import struct 4 | import string 5 | import crypt 6 | 7 | from Crypto.Util.number import long_to_bytes, bytes_to_long 8 | from discord.ext import commands 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | def rotn_helper(offset, text): 13 | shifted = string.ascii_lowercase[offset:] + string.ascii_lowercase[:offset] +\ 14 | string.ascii_uppercase[offset:] + string.ascii_uppercase[:offset] 15 | shifted_tab = str.maketrans(string.ascii_letters, shifted) 16 | return text.translate(shifted_tab) 17 | 18 | class Utils(commands.Cog): 19 | def __init__(self, bot): 20 | self.bot = bot 21 | 22 | @commands.group() 23 | async def utils(self, ctx): 24 | """ 25 | Utility commands for various trivial tasks 26 | """ 27 | if ctx.invoked_subcommand is None: 28 | await ctx.send("Invalid command passed. Use !help.") 29 | 30 | @utils.command(aliases=["stol"]) 31 | async def str2long(self, ctx, params): 32 | """ 33 | Converts string to long 34 | """ 35 | await ctx.send("`{0}`".format(bytes_to_long(params.encode("utf-8")))) 36 | 37 | @utils.command(aliases=["ltos"]) 38 | async def long2str(self, ctx, params): 39 | """ 40 | Converts long to string 41 | """ 42 | await ctx.send("`{0}`".format(long_to_bytes(params))) 43 | 44 | @utils.command() 45 | async def str2hex(self, ctx, *params): 46 | """ 47 | Converts string to hex 48 | """ 49 | joined_params = " ".join(params) 50 | await ctx.send( 51 | "`{0}`".format(joined_params.encode("latin-1").hex()) 52 | ) 53 | 54 | @utils.command() 55 | async def hex2str(self, ctx, param): 56 | """ 57 | Converts hex to string 58 | """ 59 | await ctx.send("`{0}`".format(bytearray.fromhex(param).decode())) 60 | 61 | @utils.command() 62 | async def rotn(self, ctx, shift, *params): 63 | ''' 64 | Returns the ROT-n encoding of a message. 65 | ''' 66 | msg = ' '.join(params) 67 | out = 'Original message:\n' + msg 68 | if shift == "*": 69 | for s in range(1, 14): 70 | out += f'\n=[ ROT({s}) ]=\n' 71 | out += rotn_helper(s, msg) 72 | else: 73 | shift = int(shift) 74 | shifted_str = rotn_helper(shift, msg) 75 | 76 | out += f'\n=[ ROT({shift}) ]=\n' 77 | out += 'Encoded message:\n' + shifted_str 78 | 79 | for chunk in chunkify(out, 1700): 80 | await ctx.send("".join(["```", chunk, "```"])) 81 | 82 | @utils.command() 83 | async def genshadow(self, ctx, cleartext, method = None): 84 | ''' 85 | genshadow, generates a UNIX password hash and a corresponding /etc/shadow entry 86 | and is intended for usage in boot2root environments 87 | 88 | Available hash types: 89 | + MD5 90 | + Blowfish 91 | + SHA-256 92 | + SHA-512 93 | ''' 94 | __methods = { 95 | "1": crypt.METHOD_MD5, 96 | "MD5": crypt.METHOD_MD5, 97 | 98 | "2": crypt.METHOD_BLOWFISH, 99 | "BLOWFISH": crypt.METHOD_BLOWFISH, 100 | 101 | "5": crypt.METHOD_SHA256, 102 | "SHA256": crypt.METHOD_SHA256, 103 | 104 | "6": crypt.METHOD_SHA512, 105 | "SHA512": crypt.METHOD_SHA512 106 | } 107 | if method and not method.isnumeric(): 108 | method = method.upper() 109 | method = __methods.get(method, None) 110 | 111 | unix_passwd = crypt.crypt(cleartext, method) 112 | shadow = f"root:{unix_passwd}:0:0:99999:7::" 113 | await ctx.send(f"{cleartext}:\n" +\ 114 | f"=> {unix_passwd}\n" +\ 115 | f"=> {shadow}") 116 | 117 | async def setup(bot): 118 | await bot.add_cog(Utils(bot)) 119 | -------------------------------------------------------------------------------- /ovisbot/commands/rank.py: -------------------------------------------------------------------------------- 1 | import discord 2 | import logging 3 | import re 4 | import requests 5 | import ovisbot.locale as i18n 6 | 7 | from datetime import datetime 8 | from discord.ext.commands import CommandError 9 | 10 | from ovisbot.helpers import chunkify 11 | from ovisbot.db_models import CTF, Challenge, BotConfig 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | class NotConfiguredException(CommandError): 17 | pass 18 | 19 | 20 | class RankCommandsMixin: 21 | def load_commands(self): 22 | """Hooks commands with bot subclass""" 23 | bot = self 24 | 25 | @bot.group() 26 | async def rank(ctx): 27 | """ 28 | Collection of team ranking commands 29 | """ 30 | if ctx.invoked_subcommand is None: 31 | subcomms = [sub_command for sub_command in ctx.command.all_commands] 32 | await ctx.send( 33 | "Ranking is not tracked at the moment.\nAvailable rankings are:\n```{0}```".format( 34 | " ".join(subcomms) 35 | ) 36 | ) 37 | 38 | @rank.command(name="htb") 39 | async def rank_htb(ctx): 40 | """ 41 | Displays Hack The Box team ranking 42 | """ 43 | if self.config.HTB_TEAM_ID is None: 44 | raise NotConfiguredException("Variable HTB_TEAM_ID is not configured") 45 | 46 | headers = { 47 | "Host": "www.hackthebox.eu", 48 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0", 49 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 50 | "Accept-Language": "en-US,en;q=0.5", 51 | } 52 | url = "https://www.hackthebox.eu/teams/profile/{0}".format( 53 | self.config.HTB_TEAM_ID 54 | ) 55 | r = requests.get(url, headers=headers) 56 | result = re.search( 57 | ' (.*)
', r.text 58 | ) 59 | 60 | status_response = i18n._("Position: " + result.group(1)) 61 | 62 | embed = discord.Embed( 63 | title="Hack The Box Team Ranking", 64 | colour=discord.Colour(0x7ED321), 65 | url="https://www.hackthebox.eu", 66 | description=status_response, 67 | timestamp=datetime.now(), 68 | ) 69 | 70 | embed.set_thumbnail( 71 | url="https://forum.hackthebox.eu/uploads/RJZMUY81IQLQ.png" 72 | ) 73 | embed.set_footer( 74 | text="CYberMouflons", 75 | icon_url="https://i.ibb.co/yW2mYjq/cybermouflons.png", 76 | ) 77 | await ctx.channel.send(embed=embed) 78 | 79 | @rank.command(name="ctftime") 80 | async def rank_ctftime(ctx): 81 | """ 82 | Displays team ranking on Ctftime. 83 | """ 84 | if self.config.CTFTIME_TEAM_ID is None: 85 | raise NotConfiguredException( 86 | "Variable CTFTIME_TEAM_ID is not configured" 87 | ) 88 | 89 | url = "https://ctftime.org/api/v1/teams/{0}/".format( 90 | self.config.CTFTIME_TEAM_ID 91 | ) 92 | headers = { 93 | "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:61.0) Gecko/20100101 Firefox/61.0", 94 | } 95 | r = requests.get(url, headers=headers) 96 | data = r.json() 97 | status_response = i18n._( 98 | "Position: " 99 | + str(data["rating"][0][str(datetime.now().year)]["rating_place"]) 100 | ) 101 | 102 | embed = discord.Embed( 103 | title="CTFTime Ranking", 104 | colour=discord.Colour(0xFF0035), 105 | url="https://ctftime.org/", 106 | description=status_response, 107 | timestamp=datetime.now(), 108 | ) 109 | 110 | embed.set_thumbnail( 111 | url="https://pbs.twimg.com/profile_images/2189766987/ctftime-logo-avatar_400x400.png" 112 | ) 113 | embed.set_footer( 114 | text="CYberMouflons", 115 | icon_url="https://i.ibb.co/yW2mYjq/cybermouflons.png", 116 | ) 117 | 118 | await ctx.channel.send(embed=embed) 119 | 120 | @rank_ctftime.error 121 | async def ranking_error(ctx, error): 122 | if isinstance(error, NotConfiguredException): 123 | await ctx.channel.send( 124 | i18n._("Configuration missing: {0}".format(error.args[0])) 125 | ) 126 | -------------------------------------------------------------------------------- /docs/_build/html/_static/pygments.css: -------------------------------------------------------------------------------- 1 | .highlight .hll { background-color: #ffffcc } 2 | .highlight { background: #f8f8f8; } 3 | .highlight .c { color: #8f5902; font-style: italic } /* Comment */ 4 | .highlight .err { color: #a40000; border: 1px solid #ef2929 } /* Error */ 5 | .highlight .g { color: #000000 } /* Generic */ 6 | .highlight .k { color: #004461; font-weight: bold } /* Keyword */ 7 | .highlight .l { color: #000000 } /* Literal */ 8 | .highlight .n { color: #000000 } /* Name */ 9 | .highlight .o { color: #582800 } /* Operator */ 10 | .highlight .x { color: #000000 } /* Other */ 11 | .highlight .p { color: #000000; font-weight: bold } /* Punctuation */ 12 | .highlight .ch { color: #8f5902; font-style: italic } /* Comment.Hashbang */ 13 | .highlight .cm { color: #8f5902; font-style: italic } /* Comment.Multiline */ 14 | .highlight .cp { color: #8f5902 } /* Comment.Preproc */ 15 | .highlight .cpf { color: #8f5902; font-style: italic } /* Comment.PreprocFile */ 16 | .highlight .c1 { color: #8f5902; font-style: italic } /* Comment.Single */ 17 | .highlight .cs { color: #8f5902; font-style: italic } /* Comment.Special */ 18 | .highlight .gd { color: #a40000 } /* Generic.Deleted */ 19 | .highlight .ge { color: #000000; font-style: italic } /* Generic.Emph */ 20 | .highlight .gr { color: #ef2929 } /* Generic.Error */ 21 | .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 22 | .highlight .gi { color: #00A000 } /* Generic.Inserted */ 23 | .highlight .go { color: #888888 } /* Generic.Output */ 24 | .highlight .gp { color: #745334 } /* Generic.Prompt */ 25 | .highlight .gs { color: #000000; font-weight: bold } /* Generic.Strong */ 26 | .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 27 | .highlight .gt { color: #a40000; font-weight: bold } /* Generic.Traceback */ 28 | .highlight .kc { color: #004461; font-weight: bold } /* Keyword.Constant */ 29 | .highlight .kd { color: #004461; font-weight: bold } /* Keyword.Declaration */ 30 | .highlight .kn { color: #004461; font-weight: bold } /* Keyword.Namespace */ 31 | .highlight .kp { color: #004461; font-weight: bold } /* Keyword.Pseudo */ 32 | .highlight .kr { color: #004461; font-weight: bold } /* Keyword.Reserved */ 33 | .highlight .kt { color: #004461; font-weight: bold } /* Keyword.Type */ 34 | .highlight .ld { color: #000000 } /* Literal.Date */ 35 | .highlight .m { color: #990000 } /* Literal.Number */ 36 | .highlight .s { color: #4e9a06 } /* Literal.String */ 37 | .highlight .na { color: #c4a000 } /* Name.Attribute */ 38 | .highlight .nb { color: #004461 } /* Name.Builtin */ 39 | .highlight .nc { color: #000000 } /* Name.Class */ 40 | .highlight .no { color: #000000 } /* Name.Constant */ 41 | .highlight .nd { color: #888888 } /* Name.Decorator */ 42 | .highlight .ni { color: #ce5c00 } /* Name.Entity */ 43 | .highlight .ne { color: #cc0000; font-weight: bold } /* Name.Exception */ 44 | .highlight .nf { color: #000000 } /* Name.Function */ 45 | .highlight .nl { color: #f57900 } /* Name.Label */ 46 | .highlight .nn { color: #000000 } /* Name.Namespace */ 47 | .highlight .nx { color: #000000 } /* Name.Other */ 48 | .highlight .py { color: #000000 } /* Name.Property */ 49 | .highlight .nt { color: #004461; font-weight: bold } /* Name.Tag */ 50 | .highlight .nv { color: #000000 } /* Name.Variable */ 51 | .highlight .ow { color: #004461; font-weight: bold } /* Operator.Word */ 52 | .highlight .w { color: #f8f8f8; text-decoration: underline } /* Text.Whitespace */ 53 | .highlight .mb { color: #990000 } /* Literal.Number.Bin */ 54 | .highlight .mf { color: #990000 } /* Literal.Number.Float */ 55 | .highlight .mh { color: #990000 } /* Literal.Number.Hex */ 56 | .highlight .mi { color: #990000 } /* Literal.Number.Integer */ 57 | .highlight .mo { color: #990000 } /* Literal.Number.Oct */ 58 | .highlight .sa { color: #4e9a06 } /* Literal.String.Affix */ 59 | .highlight .sb { color: #4e9a06 } /* Literal.String.Backtick */ 60 | .highlight .sc { color: #4e9a06 } /* Literal.String.Char */ 61 | .highlight .dl { color: #4e9a06 } /* Literal.String.Delimiter */ 62 | .highlight .sd { color: #8f5902; font-style: italic } /* Literal.String.Doc */ 63 | .highlight .s2 { color: #4e9a06 } /* Literal.String.Double */ 64 | .highlight .se { color: #4e9a06 } /* Literal.String.Escape */ 65 | .highlight .sh { color: #4e9a06 } /* Literal.String.Heredoc */ 66 | .highlight .si { color: #4e9a06 } /* Literal.String.Interpol */ 67 | .highlight .sx { color: #4e9a06 } /* Literal.String.Other */ 68 | .highlight .sr { color: #4e9a06 } /* Literal.String.Regex */ 69 | .highlight .s1 { color: #4e9a06 } /* Literal.String.Single */ 70 | .highlight .ss { color: #4e9a06 } /* Literal.String.Symbol */ 71 | .highlight .bp { color: #3465a4 } /* Name.Builtin.Pseudo */ 72 | .highlight .fm { color: #000000 } /* Name.Function.Magic */ 73 | .highlight .vc { color: #000000 } /* Name.Variable.Class */ 74 | .highlight .vg { color: #000000 } /* Name.Variable.Global */ 75 | .highlight .vi { color: #000000 } /* Name.Variable.Instance */ 76 | .highlight .vm { color: #000000 } /* Name.Variable.Magic */ 77 | .highlight .il { color: #990000 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | drawing 3 |

4 | 5 | 6 |

7 | OvisBot 8 |

9 | 10 |

Open source Discord bot for CTF teams

11 | 12 |
13 | 14 |

15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | Code Style: Black 27 | 28 |

29 | 30 |

31 | Overview 32 | • 33 | Installation 34 | • 35 | Documentation 36 | • 37 | Contribution 38 | • 39 | License 40 |

41 | 42 | # Overview 43 | 44 | OvisBot is a modular, feature-extensive Discord bot for managing CTF teams through discord. It facilitates collaboration and organisation by providing well defined commands to create/delete/update discord category/channels in order to structure CTF problems and provide more efficient team commmunication. In addition the bot provides basic utility functions to assist the solving process of CTF challenges (encoding schemes, etc.. ). Finally, promotes competitiveness amongst team members by providing a aut-synchronised leaderboard to common cybersecurity training platforms such as CryptoHack and Hack The Box, 45 | 46 | Note that the majority of the features are provided by isolated plugins and thus they can be enabled/disabled on demand. 47 | 48 | This is a self-hosted bot, therefore it requires to be hosted on a private server in order to be used. Further instructions to do so are provided below. It also required a running instance of MongoDB on the server but still, the docker-based installation instructions take care of that. 49 | 50 | # Installation 51 | 52 | There are couple ways to install the bot but generally the installing using docker-compose is the most convenient way to do it. Nevertheless, don't hesitate to use any other methods that suits you. 53 | 54 | ## Installing using pip 55 | 56 | To install using pip run the following command 57 | ``` 58 | pip install ovisbot 59 | ``` 60 | The above will install `ovisbot` in your python environment and will introduce the `ovisbot` cli. The cli provides commands to launch and interact with ovisbot. 61 | 62 | At runtime, the bot requires a running MongoDB server. An easy way to run a local mongodb server is using docker. You skip this step if you already have one running 63 | ``` 64 | docker run -d -p 27017-27019:27017-27019 --name mongodb mongo 65 | ``` 66 | 67 | Since OvisBot requires some predifined configuration before launch, it is necessary the you set your environment variables accordingly. Alternatively you can create a `.env` file that defined the required variables. Refer to [.env.example](.env.example) for an example. 68 | 69 | OvisBot cli provides the `setupenv` command which assists the creation of a .env file. Therefore to contrinue run and fill in the variables. 70 | ``` 71 | ovisbot setupenv 72 | ``` 73 | At the end of the process a new `.env` file will be create in your current directory. 74 | 75 | Finally to launch the bot, run: 76 | ``` 77 | ovisbot run 78 | ``` 79 | 80 | ## Installing using docker 81 | 82 | Installation using docker takes care of running mongo db automatically without requiring any extra steps. To achieve this, `docker-compose` is utilised therefore make sure that you have `docker` and `docker-compose` installed on your system. 83 | 84 | Firstly clone this repository: 85 | ``` 86 | git clone https://github.com/cybermouflons/ovisbot ovisbot && cd ovisbot 87 | ``` 88 | 89 | For the next step make sure that you have your environment variables configured properly and run: 90 | ``` 91 | docker-compose -f docker-compose.yml -f docker-compose.prod.yml up 92 | ``` 93 | 94 | ## Versioning 95 | 96 | We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/cybermouflons/ovisbot/tags). 97 | 98 | # Contribution 99 | 100 | Have a feature request? Make a GitHub issue and feel free to contribute. To get started with contributing refer to [CONTRIBUTE.md](./CONTRIBUTE.md). 101 | 102 | ### Current Contributors: 103 | 104 | - [apogiatzis](https://github.com/apogiatzis) 105 | - [kgeorgiou](https://github.com/kgeorgiou) 106 | - [condiom](https://github.com/condiom) 107 | - [npitsillos](https://github.com/npitsillos) 108 | - [Sikkis](https://github.com/Sikkis) 109 | - [ishtar](https://github.com/xmpf) 110 | - [cfalas](https://github.com/cfalas) 111 | 112 | # License 113 | 114 | Released under the GNU GPL v3 license. 115 | -------------------------------------------------------------------------------- /ovisbot/extensions/ctftime/ctftime.py: -------------------------------------------------------------------------------- 1 | import discord 2 | import io 3 | import logging 4 | import re 5 | import requests 6 | 7 | import ctftime_helpers as ctfh 8 | 9 | from colorthief import ColorThief 10 | from discord.ext import commands 11 | from urllib.request import urlopen 12 | from ovisbot.exceptions import * 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | class Ctf(commands.Cog): 18 | def __init__(self, bot): 19 | self.bot = bot 20 | 21 | @commands.group() 22 | async def ctftime(self, ctx): 23 | """ 24 | Collection of commands to interact with Ctftime 25 | """ 26 | self.guild = ctx.guild 27 | self.gid = ctx.guild.id 28 | 29 | if ctx.invoked_subcommand is None: 30 | await ctx.send("Invalid command passed. Use !help.") 31 | 32 | @ctftime.command() 33 | async def upcoming(self, ctx): 34 | """ 35 | Displays the next 3 upcoming CTFs listed in Ctftime 36 | """ 37 | default_image = "https://pbs.twimg.com/profile_images/2189766987/ctftime-logo-avatar_400x400.png" 38 | upcoming_url = "https://ctftime.org/api/v1/events/" 39 | headers = { 40 | "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:61.0) Gecko/20100101 Firefox/61.0", 41 | } 42 | 43 | def rgb2hex(r, g, b): 44 | tohex = "#{:02x}{:02x}{:02x}".format(r, g, b) 45 | return tohex 46 | 47 | limit = "3" 48 | response = requests.get(upcoming_url, headers=headers, params=limit) 49 | data = response.json() 50 | 51 | for num in range(0, int(limit)): 52 | ctf_title = data[num]["title"] 53 | (ctf_start, ctf_end) = ( 54 | data[num]["start"].replace("T", " ").split("+", 1)[0] + " UTC", 55 | data[num]["finish"].replace("T", " ").split("+", 1)[0] + " UTC", 56 | ) 57 | (ctf_start, ctf_end) = ( 58 | re.sub(":00 ", " ", ctf_start), 59 | re.sub(":00 ", " ", ctf_end), 60 | ) 61 | dur_dict = data[num]["duration"] 62 | (ctf_hours, ctf_days) = (str(dur_dict["hours"]), str(dur_dict["days"])) 63 | ctf_link = data[num]["url"] 64 | ctf_image = data[num]["logo"] 65 | ctf_format = data[num]["format"] 66 | ctf_place = data[num]["onsite"] 67 | 68 | if ctf_place is False: 69 | ctf_place = "Online" 70 | else: 71 | ctf_place = "Onsite" 72 | 73 | fd = urlopen(default_image) 74 | f = io.BytesIO(fd.read()) 75 | color_thief = ColorThief(f) 76 | rgb_color = color_thief.get_color(quality=49) 77 | hexed = str(rgb2hex(rgb_color[0], rgb_color[1], rgb_color[2])).replace( 78 | "#", "" 79 | ) 80 | f_color = int(hexed, 16) 81 | embed = discord.Embed(title=ctf_title, description=ctf_link, color=f_color) 82 | 83 | if ctf_image != "": 84 | embed.set_thumbnail(url=ctf_image) 85 | else: 86 | embed.set_thumbnail(url=default_image) 87 | 88 | embed.add_field( 89 | name="Duration", 90 | value=((ctf_days + " days, ") + ctf_hours) + " hours", 91 | inline=True, 92 | ) 93 | embed.add_field( 94 | name="Format", value=(ctf_place + " ") + ctf_format, inline=True 95 | ) 96 | embed.add_field( 97 | name="─" * 23, value=(ctf_start + " -> ") + ctf_end, inline=True 98 | ) 99 | await ctx.channel.send(embed=embed) 100 | 101 | @ctftime.command(aliases=["w"]) 102 | async def writeups(self, ctx, name): 103 | """ 104 | Returns the submitted writeups for a given CTF 105 | !ctftime writeups 106 | !ctftime w 107 | """ 108 | event = ctfh.Event(e_name=name) 109 | try: 110 | ctf_name, ctf_writeups = event.find_event_writeups() 111 | except ValueError: 112 | await ctx.channel.send("Could not find such event") 113 | return 114 | 115 | for writeup in ctf_writeups: 116 | info = writeup.__dict__ 117 | 118 | embed = discord.Embed( 119 | title=info["name"], 120 | url="https://ctftime.org" + info["url"], 121 | description=ctf_name, 122 | color=0x8CFF00, 123 | ) 124 | embed.add_field(name="Points", value=info["points"], inline=True) 125 | embed.add_field( 126 | name="Total Writeups", value=info["no_writeups"], inline=True 127 | ) 128 | embed.add_field(name="Tags", value=info["tags"], inline=False) 129 | 130 | await ctx.channel.send(embed=embed) 131 | 132 | @writeups.error 133 | async def writeups_error(self, ctx, error): 134 | if isinstance(error.original, ValueError): 135 | await ctx.channel.send( 136 | "Έλεος μάθε να μετράς τστστσ. For this command you have to provide an int number" 137 | ) 138 | else: 139 | await ctx.channel.send( 140 | "Κατι εν εδούλεψε... Θέλεις ξανα δοκιμασε, θέλεις μεν δοκιμάσεις! Στα @@ μου." 141 | ) 142 | 143 | 144 | async def setup(bot): 145 | await bot.add_cog(Ctf(bot)) 146 | -------------------------------------------------------------------------------- /ovisbot/extensions/ctftime/ctftime_helpers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import re 4 | import sys 5 | import json 6 | import requests 7 | import os 8 | 9 | 10 | from bs4 import BeautifulSoup 11 | from dataclasses import dataclass 12 | 13 | # Type Hints 14 | from typing import List, Sequence, Dict 15 | 16 | # write machinery to find `eventID` according to the CTF 17 | URL = 'https://ctftime.org/event/{EVENT_ID}/tasks/' 18 | HEADERS = { 19 | 'User-Agent':'Mozilla/5.0' 20 | } 21 | 22 | 23 | # ----- [ Class Event ] ----- # BEGIN # 24 | class Event(): 25 | ''' 26 | Event class 27 | name : str 28 | id : int 29 | ctf_id : int 30 | ctftime_url : str 31 | e_url : str 32 | e_title : str 33 | ''' 34 | 35 | s = requests.Session() 36 | 37 | def __init__(self, 38 | e_name : str = '', 39 | e_id : int = 0, 40 | e_ctf_id : int = 0, 41 | e_ctftime_url : str = '', 42 | e_url : str = '', 43 | e_title : str = '' 44 | ): 45 | self.e_name = e_name 46 | 47 | ''' @raises ValueError ''' 48 | self.e_id = int( e_id ) 49 | self.e_ctf_id = int( e_ctf_id ) 50 | 51 | self.e_ctftime_url = e_ctftime_url 52 | self.e_url = e_url 53 | self.e_title = e_title 54 | self.writeups = self.find_event_writeups 55 | 56 | 57 | def __dict__(self) -> Dict: 58 | e_dict = dict ({ 59 | "name" : self.e_name, 60 | "id" : str(self.e_id), 61 | "ctf_id" : str(self.e_ctf_id), 62 | "ctftime_url" : str(self.e_ctftime_url), 63 | "url" : str(self.e_url), 64 | "title" : str(self.e_title) 65 | }) 66 | return e_dict 67 | 68 | def find_event_by_name(self): 69 | ''' 70 | Given @name finds @event_id and fills needed fields to be used by @find_event_by_id 71 | Raises @ValueError 72 | ''' 73 | 74 | url = 'https://ctftime.org/event/list/past' 75 | r = (self.s).get(url=url, headers=HEADERS) 76 | soup = BeautifulSoup(r.text, 'html.parser') 77 | 78 | # find Event by name 79 | found = False 80 | try: 81 | all_tr = soup.body.table.find_all('tr')[1::] 82 | except AttributeError: 83 | print("Error: Unable to get list") 84 | 85 | for i in all_tr: 86 | name = (i.td).text 87 | e_name = self.e_name.split('.')[0] 88 | if e_name.upper() in name.upper(): 89 | found = True 90 | href = (i.a).get('href') 91 | start_ix, end_ix = re.search(r'\d+', href).span() 92 | self.e_id = int( href[start_ix:end_ix] ) 93 | self.e_name = name 94 | break 95 | if not found: 96 | raise ValueError('Event not found') 97 | 98 | def find_event_by_id(self): 99 | ''' 100 | Using the @event_id finds and collects available writeups into a list 101 | ''' 102 | 103 | url = f'https://ctftime.org/event/{ str(self.e_id) }/tasks/' 104 | r = (self.s).get(url=url, headers=HEADERS) 105 | soup = BeautifulSoup(r.text, 'html.parser') 106 | 107 | writeups = [] 108 | all_tr = soup.find_all('tr')[1:] 109 | for i in all_tr: 110 | url = (i.a).get("href") 111 | name = (i.a).get_text() 112 | tags = list(map(lambda x: x.get_text(), i.find_all('span'))) 113 | all_td = i.find_all('td') 114 | points = all_td[1].get_text() 115 | no_writeups = all_td[-2].get_text() 116 | 117 | writeup = Writeup(name=name, 118 | points=points, 119 | tags=tags, 120 | no_writeups=no_writeups, 121 | url=url) 122 | writeups.append( writeup ) 123 | 124 | self.writeups = writeups 125 | return self.e_name, self.writeups 126 | 127 | def find_event_writeups(self): 128 | self.find_event_by_name() 129 | return self.find_event_by_id() 130 | 131 | @dataclass 132 | class Writeup(): 133 | ''' 134 | Writeup Class 135 | name : str 136 | points : int 137 | tags : list 138 | no_writeups : int 139 | url : str 140 | ''' 141 | name : str = '' 142 | points : int = 0 143 | tags : Sequence[str] = None 144 | no_writeups : int = 0 145 | url : str = '' 146 | 147 | def __str__(self) -> str: 148 | ''' 149 | String representation of class Writeup 150 | ''' 151 | return f'Name: {self.name} ({self.points} pts)\n' +\ 152 | f'Tags: {self.tags}\n' +\ 153 | f'#Writeups: {self.no_writeups}\n' +\ 154 | f'Writeup URL: https://ctftime.org{self.url}\n' 155 | 156 | if __name__ == '__main__': 157 | 158 | if len(sys.argv) == 2: 159 | e_name = str( sys.argv[1] ) 160 | event = Event ( 161 | e_name = e_name 162 | ) 163 | try: 164 | ctf_name, writeups = event.find_event_writeups() 165 | except ValueError as e: 166 | print( 'Could not find such event' ) 167 | sys.exit( 1 ) 168 | 169 | n = len(ctf_name) 170 | print('=' * n, ctf_name, '=' * n, sep='\n') 171 | print( '\n'.join( map( str, writeups ))) 172 | sys.exit(0) 173 | else: 174 | print(f'Usage: {sys.argv[0]} ') 175 | sys.exit(1) 176 | -------------------------------------------------------------------------------- /ovisbot/db_models.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import ovisbot.locale as i18n 4 | 5 | from ovisbot.helpers import escape_md 6 | from pymodm import MongoModel, EmbeddedMongoModel, fields, connect 7 | from ovisbot.utils.progressbar import draw_bar 8 | from texttable import Texttable 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | class CogDetails(MongoModel): 14 | name = fields.CharField(required=True) 15 | local_path = fields.CharField(required=True) 16 | enabled = fields.BooleanField(default=True) 17 | loaded = fields.BooleanField(default=False) 18 | url = fields.CharField(required=False) 19 | description = fields.CharField(required=False) 20 | open_source = fields.BooleanField(default=True) 21 | 22 | def tolist(self): 23 | return [ 24 | "✅" if self.enabled else "❌", 25 | "✅" if self.loaded else "❌", 26 | self.name, 27 | self.url if self.url else "BUILTIN", 28 | "YES" if self.open_source else "NO", 29 | ] 30 | 31 | 32 | class BotConfig(MongoModel): 33 | REMINDERS_CHANNEL = fields.IntegerField(blank=True) 34 | IS_MAINTENANCE = fields.BooleanField() 35 | CTFTIME_TEAM_ID = fields.CharField() 36 | HTB_TEAM_ID = fields.CharField() 37 | ADMIN_ROLE = fields.CharField() 38 | EXTENSIONS = fields.EmbeddedDocumentListField(CogDetails, default=[]) 39 | 40 | 41 | class SSHKey(MongoModel): 42 | name = fields.CharField(required=True) 43 | owner_id = fields.CharField(required=True) 44 | owner_name = fields.CharField(required=True) 45 | private_key = fields.CharField(required=True) 46 | public_key = fields.CharField(required=True) 47 | 48 | def table_row_serialize(self): 49 | return [self.name, self.owner_name, self.public_key] 50 | 51 | @classmethod 52 | def table_serialize(cls): 53 | table = Texttable() 54 | table.set_deco(Texttable.HEADER) 55 | table.set_cols_dtype(["a", "a", "a"]) # automatic 56 | table.set_cols_align(["c", "c", "l"]) 57 | table.add_rows( 58 | [ 59 | ["name", "owner", "public_key_url"], 60 | *[key.table_row_serialize() for key in cls.objects.all()], 61 | ] 62 | ) 63 | return table.draw() 64 | 65 | 66 | class HTBUserMapping(MongoModel): 67 | discord_user_id = fields.IntegerField(required=True) 68 | htb_user = fields.CharField(required=True) 69 | htb_user_id = fields.IntegerField(required=True) 70 | 71 | 72 | class CryptoHackUserMapping(MongoModel): 73 | discord_user_id = fields.IntegerField(required=True) 74 | cryptohack_user = fields.CharField(required=True) 75 | 76 | 77 | class Challenge(EmbeddedMongoModel): 78 | name = fields.CharField(required=True) 79 | created_at = fields.DateTimeField(required=True) 80 | tags = fields.ListField(fields.CharField(), default=[]) 81 | attempted_by = fields.ListField(fields.CharField(), default=[]) 82 | solved_at = fields.DateTimeField(blank=True) 83 | solved_by = fields.ListField(fields.CharField(), default=[], blank=True) 84 | notebook_url = fields.CharField(default="", blank=True) 85 | flag = fields.CharField() 86 | 87 | 88 | class CTF(MongoModel): 89 | name = fields.CharField(required=True) 90 | description = fields.CharField() 91 | created_at = fields.DateTimeField(required=True) 92 | finished_at = fields.DateTimeField() 93 | start_date = fields.DateTimeField() 94 | end_date = fields.DateTimeField() 95 | url = fields.URLField() 96 | username = fields.CharField() 97 | password = fields.CharField() 98 | challenges = fields.EmbeddedDocumentListField(Challenge, default=[], blank=True) 99 | pending_reminders = fields.ListField(blank=True, default=[]) 100 | 101 | def status(self, members_joined_count): 102 | 103 | description_str = self.description + "\n" if self.description else "" 104 | 105 | solved_count = len( 106 | list(filter(lambda x: x.solved_at is not None, self.challenges)) 107 | ) 108 | total_count = len(self.challenges) 109 | status = ( 110 | f":triangular_flag_on_post: **{self.name}** ({members_joined_count} Members joined)\n{description_str}" 111 | + f"```CSS\n{draw_bar(solved_count, total_count, style=5)}\n" 112 | + f" {solved_count} Solved / {total_count} Total" 113 | ) 114 | if self.start_date: 115 | fmt_str = "%d/%m %H:\u200b%M" 116 | start_date_str = self.start_date.strftime(fmt_str) 117 | end_date_str = self.end_date.strftime(fmt_str) if self.end_date else "?" 118 | status += f"\n {start_date_str} - {end_date_str}\n" 119 | status += "```" 120 | return status 121 | 122 | def credentials(self): 123 | response = f":busts_in_silhouette: **Username**: {self.username}\n:key: **Password**: {self.password}" 124 | if self.url is not None: 125 | response += f"\n\nLogin Here: {self.url}" 126 | return response 127 | 128 | def challenge_summary(self): 129 | if not self.challenges: 130 | return i18n._( 131 | "No challenges found. Try adding one with `!ctf addchallenge `" 132 | ) 133 | 134 | solved_response, unsolved_response = "", "" 135 | 136 | for challenge in self.challenges: 137 | challenge_details = f'**{escape_md(challenge.name[len(self.name)+1:])}** [{", ".join(challenge.tags)}]' 138 | if challenge.solved_at: 139 | solved_response += f':white_check_mark: {challenge_details} Solved by: [{", ".join(challenge.solved_by)}]\n' 140 | else: 141 | unsolved_response += f':thinking: {challenge_details} Attempted by: [{escape_md(", ".join(challenge.attempted_by))}]\n' 142 | 143 | return ( 144 | f"\\>>> Solved\n{solved_response}" + f"\\>>> Unsolved\n{unsolved_response}" 145 | ) 146 | 147 | class Meta: 148 | collection_name = "ctf" 149 | ignore_unknown_fields = True 150 | -------------------------------------------------------------------------------- /ovisbot/extensions/cryptohack/cryptohack.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import json 3 | import requests 4 | import discord 5 | import dataclasses 6 | import datetime 7 | 8 | from discord.ext import commands 9 | 10 | from ovisbot.exceptions import CryptoHackApiException 11 | from ovisbot.helpers import success, escape_md 12 | from ovisbot.db_models import CryptoHackUserMapping 13 | 14 | logging.basicConfig(level=logging.INFO) 15 | logger = logging.getLogger(__name__) 16 | 17 | 18 | @dataclasses.dataclass 19 | class Score: 20 | username: str 21 | global_rank: int 22 | points: int 23 | total_points: int 24 | challs_solved: int 25 | total_challs: int 26 | num_users: int 27 | 28 | @classmethod 29 | def parse(cls, raw): 30 | # username:global_rank:points:total_points:challs_solved:total_challs:num_users 31 | spl = raw.split(":") 32 | assert len(spl) == 7 33 | username = spl.pop(0) 34 | return cls(*([username] + list(map(int, spl)))) 35 | 36 | 37 | class CryptoHack(commands.Cog): 38 | DISCORD_TOKEN_API_URL = "https://cryptohack.org/discord_token/{0}/" 39 | USER_SCORE_API_URL = "https://cryptohack.org/wechall/userscore/" 40 | USER_URL = "https://cryptohack.org/user/{0}/" 41 | 42 | def __init__(self, bot): 43 | self.bot = bot 44 | 45 | def _get_cryptohack_score(self, cryptohack_user): 46 | """ 47 | Returns score of given user. 48 | """ 49 | req = requests.get( 50 | CryptoHack.USER_SCORE_API_URL, params={"username": cryptohack_user} 51 | ) 52 | res = req.text 53 | 54 | if "failed" in res: 55 | raise CryptoHackApiException 56 | 57 | return Score.parse(res) 58 | 59 | @commands.group() 60 | async def cryptohack(self, ctx): 61 | """ 62 | Collection of CryptoHack commands 63 | """ 64 | if ctx.invoked_subcommand is None: 65 | await ctx.send("Invalid command passed. Use `!help`.") 66 | 67 | @cryptohack.command() 68 | async def connect(self, ctx, token=None): 69 | """ 70 | Link your CryptoHack account 71 | """ 72 | if token is None: 73 | msg = ( 74 | "Εν μου έδωκες token! Πάεννε στο https://cryptohack.org/user/ " 75 | "τζαι πιαστο token που εν κάτω κάτω! Μια ζωη να σας παρατζέλλω δαμέσα!" 76 | ) 77 | await ctx.send(msg) 78 | else: 79 | token = token.replace("/", "").replace(".", "").replace("%", "") 80 | req = requests.get(CryptoHack.DISCORD_TOKEN_API_URL.format(token)) 81 | res = json.loads(req.content) 82 | 83 | if "error" in res: 84 | raise CryptoHackApiException(res["error"]) 85 | 86 | cryptohack_user = res["user"] 87 | CryptoHackUserMapping( 88 | cryptohack_user=cryptohack_user, discord_user_id=ctx.author.id 89 | ).save() 90 | await ctx.send(f"Linked CryptoHack as {cryptohack_user}!") 91 | await success(ctx.message) 92 | 93 | @cryptohack.command() 94 | async def disconnect(self, ctx): 95 | """ 96 | Disconecct your CryptoHack account 97 | """ 98 | cryptohack_mapping = CryptoHackUserMapping.objects.get( 99 | {"discord_user_id": ctx.author.id} 100 | ) 101 | cryptohack_mapping.delete() 102 | await success(ctx.message) 103 | 104 | @cryptohack.command() 105 | async def stats(self, ctx, user=None): 106 | """ 107 | Show your CryptoHack stats or any given user's 108 | """ 109 | if user is None: 110 | user_id = ctx.author.id 111 | else: 112 | user_id = next((m.id for m in ctx.message.mentions), None) 113 | if user_id is None: 114 | return 115 | 116 | cryptohack_mapping = CryptoHackUserMapping.objects.get( 117 | {"discord_user_id": user_id} 118 | ) 119 | score = self._get_cryptohack_score(cryptohack_mapping.cryptohack_user) 120 | 121 | await ctx.send( 122 | embed=discord.Embed( 123 | title=score.username, 124 | url=CryptoHack.USER_URL.format(score.username), 125 | color=0xFEB32B, 126 | ) 127 | .add_field( 128 | name="Rank", 129 | value=f"{score.global_rank} / {score.num_users}", 130 | inline=False, 131 | ) 132 | .add_field( 133 | name="Score", 134 | value=f"{score.points} / {score.total_points}", 135 | inline=False, 136 | ) 137 | .add_field( 138 | name="Solves", 139 | value=f"{score.challs_solved} / {score.total_challs}", 140 | inline=False, 141 | ) 142 | ) 143 | 144 | @cryptohack.command() 145 | async def scoreboard(self, ctx): 146 | """ 147 | Displays internal CryptoHack scoreboard. 148 | """ 149 | limit = 10 150 | mappings = CryptoHackUserMapping.objects.all() 151 | scores = [ 152 | ( 153 | escape_md(self.bot.get_user(mapping.discord_user_id).name), 154 | self._get_cryptohack_score(mapping.cryptohack_user), 155 | ) 156 | for mapping in mappings 157 | ][:limit] 158 | 159 | scores = sorted(scores, key=lambda s: s[1].points, reverse=True) 160 | scoreboard = "\n".join( 161 | "{0}. **{1}**\t{2}".format(idx + 1, s[0], s[1].points) 162 | for idx, s in enumerate(scores) 163 | ) 164 | embed = discord.Embed( 165 | title="CryptoHack Scoreboard", 166 | colour=discord.Colour(0xFEB32B), 167 | url="https://cryptohack.org/", 168 | description=scoreboard, 169 | timestamp=datetime.datetime.now(), 170 | ) 171 | 172 | embed.set_thumbnail(url="https://cryptohack.org/static/img/main.png") 173 | embed.set_footer( 174 | text="CYberMouflons", 175 | icon_url="https://i.ibb.co/yW2mYjq/cybermouflons.png", 176 | ) 177 | 178 | await ctx.send(embed=embed) 179 | 180 | @scoreboard.error 181 | @disconnect.error 182 | @connect.error 183 | @stats.error 184 | async def generic_error_handler(self, ctx, error): 185 | if isinstance(error.original, CryptoHackUserMapping.DoesNotExist): 186 | await ctx.channel.send( 187 | "Ρε λεβεντη... εν βρίσκω συνδεδεμένο CryptoHack account! (`!cryptohack connect `)" 188 | ) 189 | elif isinstance(error.original, CryptoHackApiException): 190 | await ctx.channel.send("Ούπς... κατι επήε λάθος ρε τσιάκκο!") 191 | 192 | 193 | async def setup(bot): 194 | await bot.add_cog(CryptoHack(bot)) 195 | -------------------------------------------------------------------------------- /ovisbot/config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import inspect 3 | import sys 4 | 5 | from colorama import Fore 6 | from itertools import chain 7 | from typing import Optional, List, Any, NoReturn 8 | from os import environ 9 | from pymodm.errors import ValidationError 10 | 11 | from ovisbot.db_models import BotConfig 12 | from ovisbot.helpers import get_props 13 | from texttable import Texttable 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | 18 | class ConfigurableProperty: 19 | """Defines a property that can be updated dyanmically 20 | through the bot. For that purpose, the last value of the property 21 | is also kept in the database 22 | """ 23 | 24 | def __init__(self, value: str): 25 | self._value = value 26 | 27 | @property 28 | def value(self): 29 | return self._value 30 | 31 | def __str__(self): 32 | return str(self._value) 33 | 34 | 35 | class AbstractConfig: 36 | """Singleton abstract config class""" 37 | 38 | __instance__ = None 39 | 40 | def __new__(cls): 41 | if AbstractConfig.__instance__ is None: 42 | logger.info( 43 | Fore.YELLOW + "[+]" + Fore.RESET + "Creating config instance..." 44 | ) 45 | AbstractConfig.__instance__ = object.__new__(cls) 46 | AbstractConfig.__instance__._copy_from_class() 47 | AbstractConfig.__instance__._load_props_from_db() 48 | return AbstractConfig.__instance__ 49 | 50 | def _copy_from_class(self) -> NoReturn: 51 | """Copies all attributes from class to instance""" 52 | dynamic_props = self._get_configurable_props_from_cls() 53 | static_props = self._get_static_props_from_cls() 54 | for prop, value in chain(dynamic_props, static_props): 55 | setattr(self, prop, value) 56 | 57 | def _get_or_create_config_from_db(self) -> BotConfig: 58 | """Loads the BotConfig from DB or creates a new one with the values 59 | provided in the subclasses of this class 60 | 61 | NOTE: Assumes that there is only one config. 62 | """ 63 | try: 64 | return BotConfig.objects.all().first() 65 | except BotConfig.DoesNotExist: 66 | config = BotConfig( 67 | **{k: v for k, v in self._get_configurable_props_from_cls()} 68 | ) 69 | try: 70 | config.save() 71 | except ValidationError as err: 72 | logger.error("Couldn't launch bot. Not configured properly") 73 | logger.error(err) 74 | sys.exit(1) 75 | return config 76 | 77 | def _load_props_from_db(self) -> NoReturn: 78 | """Loads properties from config store in the DB. i.e. the configurable options""" 79 | dynamic_props = self._get_configurable_props_from_cls() 80 | config_in_db = self._get_or_create_config_from_db() 81 | self.config_in_db = config_in_db 82 | for prop, value in dynamic_props: 83 | saved_value = getattr(config_in_db, prop, None) 84 | if saved_value is not None and value != saved_value: 85 | setattr(self, prop, saved_value) 86 | 87 | def _get_configurable_props_from_cls(self) -> List[Any]: 88 | """Returns configurable properties of class""" 89 | return map( 90 | lambda a: (a[0], a[1].value), 91 | filter( 92 | lambda a: isinstance( 93 | getattr(self.__class__, a[0]), ConfigurableProperty 94 | ), 95 | get_props(self.__class__), 96 | ), 97 | ) 98 | 99 | def _get_static_props_from_cls(self) -> List[Any]: 100 | """Returns non configurable (static) properties of class""" 101 | return filter( 102 | lambda a: not ( 103 | isinstance(getattr(self.__class__, a[0]), ConfigurableProperty) 104 | or (a[0].startswith("__") and a[0].endswith("__")) 105 | ), 106 | get_props(self.__class__), 107 | ) 108 | 109 | def options_table(self) -> str: 110 | """Returns an ASCII table with configurable options""" 111 | table = Texttable() 112 | table.set_deco(Texttable.VLINES | Texttable.HEADER | Texttable.HLINES) 113 | table.set_cols_dtype(["a", "a"]) # automatic 114 | table.set_cols_align(["l", "l"]) 115 | table.add_rows( 116 | [ 117 | ["name", "value"], 118 | *[ 119 | [name, getattr(self, name)] 120 | for name, val in self._get_configurable_props_from_cls() 121 | ], 122 | ] 123 | ) 124 | return table.draw() 125 | 126 | def save(self) -> NoReturn: 127 | """Saves current instance config to database""" 128 | for prop in self._get_configurable_props_from_cls(): 129 | prop_name = prop[0] 130 | config_in_db_prop_names = list( 131 | map(lambda p: p[0], get_props(self.config_in_db)) 132 | ) 133 | if prop_name in config_in_db_prop_names: 134 | setattr(self.config_in_db, prop_name, getattr(self, prop_name)) 135 | else: 136 | logger.warning( 137 | Fore.YELLOW 138 | + "Attempted to save configurable config variable ({0}) which is not included in the DB model...".format( 139 | prop_name 140 | ) 141 | ) 142 | self.config_in_db.save() 143 | self._load_props_from_db() # To ensure consistency after db validations 144 | 145 | 146 | class Config(AbstractConfig): 147 | """Parent configuration class.""" 148 | 149 | DB_URL = environ.get("OVISBOT_DB_URL", "mongodb://mongo/ovisdb") 150 | COMMAND_PREFIX = environ.get("OVISBOT_COMMAND_PREFIX", "!") 151 | DISCORD_BOT_TOKEN = environ.get("OVISBOT_DISCORD_TOKEN") 152 | 153 | THIRD_PARTY_COGS_INSTALL_DIR = environ.get( 154 | "OVISBOT_THIRD_PARTY_COGS_INSTALL_DIR", "/usr/local/share/ovisbot/cogs" 155 | ) 156 | EXTENSIONS = environ.get("OVISBOT_EXTENSIONS", []) 157 | 158 | COMMAND_CORRECTION_WINDOW = environ.get( 159 | "OVISBOT_COMMAND_CORRECTION_WINDOW", 30 # time in seconds 160 | ) 161 | GIT_REPO = environ.get( 162 | "OVISBOT_GIT_REPO", "https://github.com/cybermouflons/ovisbot" 163 | ) 164 | 165 | WOLFRAM_ALPHA_APP_ID = environ.get("OVISBOT_WOLFRAM_ALPHA_APP_ID") 166 | HTB_CREDS_EMAIL = environ.get("OVISBOT_HTB_CREDS_EMAIL") 167 | HTB_CREDS_PASS = environ.get("OVISBOT_HTB_CREDS_PASS") 168 | 169 | ADMIN_ROLE = ConfigurableProperty(environ.get("OVISBOT_ADMIN_ROLE")) 170 | CTFTIME_TEAM_ID = ConfigurableProperty(environ.get("OVISBOT_CTFTIME_TEAM_ID")) 171 | HTB_TEAM_ID = ConfigurableProperty(environ.get("OVISBOT_HTB_TEAM_ID")) 172 | REMINDERS_CHANNEL = ConfigurableProperty(environ.get("OVISBOT_REMINDERS_CHANNEL")) 173 | IS_MAINTENANCE = ConfigurableProperty(environ.get("OVISBOT_IS_MAINTENANCE", False)) 174 | 175 | 176 | class TestingConfig(Config): 177 | """Configurations for Testing""" 178 | 179 | pass 180 | 181 | 182 | class DevelopmentConfig(Config): 183 | """Configurations for Development.""" 184 | 185 | pass 186 | 187 | 188 | class QaConfig(Config): 189 | """Configurations for QA.""" 190 | 191 | pass 192 | 193 | 194 | class StagingConfig(Config): 195 | """Configurations for Staging.""" 196 | 197 | pass 198 | 199 | 200 | class ProductionConfig(Config): 201 | """Configurations for Production.""" 202 | 203 | pass 204 | 205 | 206 | bot_config = { 207 | "test": TestingConfig, 208 | "dev": DevelopmentConfig, 209 | "qa": QaConfig, 210 | "staging": StagingConfig, 211 | "prod": ProductionConfig, 212 | } 213 | 214 | 215 | def get_config(): 216 | return BotConfig.objects.all().first() 217 | -------------------------------------------------------------------------------- /ovisbot/cog_manager.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import logging 4 | import json 5 | import shutil 6 | import requests 7 | 8 | from colorama import Fore 9 | from git import Repo, Git 10 | from pathlib import Path 11 | from texttable import Texttable 12 | from typing import Tuple, List, NoReturn 13 | 14 | from ovisbot.db_models import CogDetails 15 | from discord.ext.commands.errors import ExtensionNotLoaded 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | 20 | class CogAlreadyInstalledException(Exception): 21 | pass 22 | 23 | 24 | class CogSpecificationMissingException(Exception): 25 | pass 26 | 27 | 28 | class CogManager(object): 29 | """This class is responsible for loading arbitrary cogs (extensions)""" 30 | 31 | def __init__(self, bot): 32 | self._bot = bot 33 | 34 | @property 35 | def cogs(self): 36 | return CogDetails.objects.all() 37 | 38 | def _builitin_cogs(self): 39 | """Returns a list of CogDetails objects for the builtin cogs""" 40 | builtin_cogs_dir = os.path.join( 41 | Path(__file__).resolve().parent, "extensions") 42 | builtin_cogs = self._create_cogs_from_path(builtin_cogs_dir) 43 | 44 | # Load builtin cogs from DB or create cog if does not exist 45 | cogs = [] 46 | for cog in builtin_cogs: 47 | try: 48 | saved_cog = CogDetails.objects.get( 49 | {"name": cog.name, "url": None}) 50 | cogs.append(saved_cog) 51 | except CogDetails.DoesNotExist: 52 | cogs.append(cog) 53 | 54 | return cogs 55 | 56 | def _third_party_cogs(self): 57 | """Returns a list of CogDetails objects for the third party cogs 58 | as specified in the config""" 59 | return list(filter(lambda c: c.url is not None, CogDetails.objects.all())) 60 | 61 | def _create_cogs_from_path(self, path): 62 | """Creates CogDetails objects by traversing a given path""" 63 | return [ 64 | CogDetails(name=diritem, local_path=os.path.join(path, diritem)) 65 | for diritem in os.listdir(path) 66 | if os.path.isdir(os.path.join(path, diritem)) 67 | and not diritem.startswith("__") 68 | and not diritem.endswith("__") 69 | ] 70 | 71 | async def _load_cog_from_object(self, cog: CogDetails) -> CogDetails: 72 | """Loads a cog based ona CogDetails object""" 73 | try: 74 | sys.path.insert(1, cog.local_path) 75 | await self._bot.load_extension(cog.name) 76 | logger.info( 77 | Fore.GREEN 78 | + "[Success]" 79 | + Fore.RESET 80 | + " Extension: {0} from {1}".format(cog.name, cog.local_path) 81 | ) 82 | cog.enabled = True 83 | cog.loaded = True 84 | except Exception as error: 85 | logger.info(type(error)) 86 | cog.enabled = False 87 | cog.loaded = False 88 | logger.info( 89 | Fore.RED 90 | + "[Failed]" 91 | + Fore.RESET 92 | + " Extension: {0} from {1}".format(cog.name, cog.local_path) 93 | ) 94 | logger.error( 95 | "Cog `{0}` failed to load. Error: {1}".format(cog.name, error)) 96 | cog.save() # Hacky workaround... 97 | raise error 98 | return cog 99 | 100 | def cog_table(self): 101 | """Returns an ASCII table with details for installed cogs""" 102 | table = Texttable() 103 | table.set_deco(Texttable.HEADER) 104 | table.set_cols_dtype(["a", "a", "a", "a", "a"]) # automatic 105 | table.set_cols_align(["c", "c", "l", "l", "c"]) 106 | table.add_rows( 107 | [ 108 | ["enabled", "loaded", "name", "url", "open_source"], 109 | *[cog.tolist() for cog in self.cogs], 110 | ] 111 | ) 112 | return table.draw() 113 | 114 | def is_cog_installed(self, name): 115 | """Returns true if the third party extension is already installed""" 116 | try: 117 | CogDetails.objects.get({"name": name}) 118 | except CogDetails.DoesNotExist: 119 | return False 120 | return True 121 | 122 | def parse_cog_spec(self, path): 123 | """ 124 | Parses cog specification file from a path. 125 | Used to read metadata for third party cogs 126 | """ 127 | data = None 128 | try: 129 | with open(os.path.join(path, "extension.json")) as json_file: 130 | data = json.load(json_file) 131 | except FileNotFoundError: 132 | raise CogSpecificationMissingException 133 | return data 134 | 135 | async def load_cogs(self) -> List[CogDetails]: 136 | """ 137 | Loads builtin and installed cogs 138 | 139 | Returns: 140 | List[CogDetails]: A list of all the cogs attempted to load. 141 | """ 142 | logger.info(Fore.YELLOW + "[+]" + Fore.RESET + " Loading Cogs...") 143 | 144 | builtin_cogs = self._builitin_cogs() 145 | third_party_cogs = self._third_party_cogs() 146 | all_cogs = builtin_cogs + third_party_cogs 147 | logger.info(f'All cogs: {all_cogs}') 148 | for cog in all_cogs: 149 | if cog.enabled: 150 | cog = await self._load_cog_from_object(cog) 151 | cog.save() 152 | 153 | return self.cogs 154 | 155 | async def reset(self): 156 | """Deletes all third party installed cogs and resets builtin cogs""" 157 | for cog in self.cogs: 158 | await self.remove(cog.name) 159 | self.load_cogs() 160 | 161 | async def remove(self, cog_name): 162 | """Deletes an instlled cog""" 163 | cog = CogDetails.objects.get({"name": cog_name}) 164 | if cog.enabled: 165 | try: 166 | await self._bot.unload_extension(cog.name) 167 | except ExtensionNotLoaded: 168 | logger.error( 169 | "Attempted to unload extension, without loading... Investigate this..." 170 | ) 171 | cog.delete() 172 | if cog.local_path in sys.path: 173 | sys.path.remove(cog.local_path) 174 | 175 | if cog.url is not None and cog.local_path: 176 | shutil.rmtree(cog.local_path, ignore_errors=True) 177 | 178 | async def disable_cog(self, name) -> NoReturn: 179 | """Disables the specified cog""" 180 | cog = CogDetails.objects.get({"name": name}) 181 | await self._bot.unload_extension(cog.name) 182 | cog.enabled = False 183 | cog.save() 184 | 185 | async def enable_cog(self, name) -> NoReturn: 186 | """Enables the specified cog""" 187 | cog = CogDetails.objects.get({"name": name}) 188 | await self._load_cog_from_object(cog) 189 | cog.save() 190 | 191 | async def reload_cog(self, name) -> NoReturn: 192 | """Reloads an installed cog""" 193 | cog = CogDetails.objects.get({"name": name}) 194 | if cog.enabled: 195 | await self._bot.unload_extension(cog.name) 196 | await self._bot.load_extension(cog.name) 197 | 198 | async def install_cog_by_path(self, path) -> CogDetails: 199 | """Installs a Cog the given path. (Mainly for dev purposes)""" 200 | cog_spec = self.parse_cog_spec(path) 201 | name = cog_spec["name"] 202 | 203 | if self.is_cog_installed(name): 204 | raise CogAlreadyInstalledException(name) 205 | 206 | cog = CogDetails( 207 | name=name, 208 | local_path=path, 209 | url=path, 210 | open_source=False, 211 | ) 212 | await self._load_cog_from_object(cog) 213 | cog.save() 214 | 215 | return cog 216 | 217 | async def install_cog_by_git_url(self, url, sshkey=None) -> CogDetails: 218 | """Installs a Cog from a git repository""" 219 | url = url.lower().strip() 220 | 221 | path = os.path.join( 222 | self._bot.config.THIRD_PARTY_COGS_INSTALL_DIR, 223 | url.split("/")[-1] + "_" + os.urandom(6).hex(), 224 | ) 225 | 226 | if sshkey: 227 | logger.info(Fore.CYAN + "[+] Using SSH key to clone extension...") 228 | key_file_id = os.urandom(16).hex() 229 | git_ssh_identity_file = os.path.join( 230 | "/tmp", "{0}.key".format(key_file_id)) 231 | logger.info( 232 | Fore.CYAN + "[+] Privkey url: {0}".format(sshkey.private_key)) 233 | 234 | r = requests.get(sshkey.private_key) 235 | with open(git_ssh_identity_file, "wb") as outfile: 236 | outfile.write(r.content) 237 | os.chmod(git_ssh_identity_file, 0o400) 238 | 239 | git_ssh_cmd = ( 240 | 'ssh -i %s -o "StrictHostKeyChecking no"' % git_ssh_identity_file 241 | ) 242 | 243 | Repo.clone_from( 244 | url, path, branch="master", env={"GIT_SSH_COMMAND": git_ssh_cmd} 245 | ) 246 | 247 | os.remove(git_ssh_identity_file) 248 | else: 249 | Repo.clone_from(url, path, branch="master") 250 | 251 | cog_spec = self.parse_cog_spec(path) 252 | name = cog_spec["name"] 253 | 254 | if self.is_cog_installed(name): 255 | shutil.rmtree(path, ignore_errors=True) 256 | raise CogAlreadyInstalledException(name) 257 | 258 | cog = CogDetails( 259 | name=name, 260 | local_path=path, 261 | url=url, 262 | open_source=False if sshkey else True, 263 | ) 264 | await self._load_cog_from_object(cog) 265 | cog.save() 266 | 267 | return cog 268 | 269 | def install(self, url, sshkey=None) -> CogDetails: 270 | """Attempts to install a cog based on a path/url. If path not found locally it 271 | fallsback to git""" 272 | if os.path.exists(url): 273 | logger.info( 274 | Fore.YELLOW + 275 | "[INFO] Extension found locally at {0}".format(url) 276 | ) 277 | self.install_cog_by_path(url) 278 | else: 279 | logger.info( 280 | Fore.YELLOW 281 | + "[INFO] Trying to install extension from git: {0}".format(url) 282 | ) 283 | self.install_cog_by_git_url(url, sshkey) 284 | -------------------------------------------------------------------------------- /docs/_build/html/_static/doctools.js: -------------------------------------------------------------------------------- 1 | /* 2 | * doctools.js 3 | * ~~~~~~~~~~~ 4 | * 5 | * Sphinx JavaScript utilities for all documentation. 6 | * 7 | * :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | /** 13 | * select a different prefix for underscore 14 | */ 15 | $u = _.noConflict(); 16 | 17 | /** 18 | * make the code below compatible with browsers without 19 | * an installed firebug like debugger 20 | if (!window.console || !console.firebug) { 21 | var names = ["log", "debug", "info", "warn", "error", "assert", "dir", 22 | "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", 23 | "profile", "profileEnd"]; 24 | window.console = {}; 25 | for (var i = 0; i < names.length; ++i) 26 | window.console[names[i]] = function() {}; 27 | } 28 | */ 29 | 30 | /** 31 | * small helper function to urldecode strings 32 | */ 33 | jQuery.urldecode = function(x) { 34 | return decodeURIComponent(x).replace(/\+/g, ' '); 35 | }; 36 | 37 | /** 38 | * small helper function to urlencode strings 39 | */ 40 | jQuery.urlencode = encodeURIComponent; 41 | 42 | /** 43 | * This function returns the parsed url parameters of the 44 | * current request. Multiple values per key are supported, 45 | * it will always return arrays of strings for the value parts. 46 | */ 47 | jQuery.getQueryParameters = function(s) { 48 | if (typeof s === 'undefined') 49 | s = document.location.search; 50 | var parts = s.substr(s.indexOf('?') + 1).split('&'); 51 | var result = {}; 52 | for (var i = 0; i < parts.length; i++) { 53 | var tmp = parts[i].split('=', 2); 54 | var key = jQuery.urldecode(tmp[0]); 55 | var value = jQuery.urldecode(tmp[1]); 56 | if (key in result) 57 | result[key].push(value); 58 | else 59 | result[key] = [value]; 60 | } 61 | return result; 62 | }; 63 | 64 | /** 65 | * highlight a given string on a jquery object by wrapping it in 66 | * span elements with the given class name. 67 | */ 68 | jQuery.fn.highlightText = function(text, className) { 69 | function highlight(node, addItems) { 70 | if (node.nodeType === 3) { 71 | var val = node.nodeValue; 72 | var pos = val.toLowerCase().indexOf(text); 73 | if (pos >= 0 && 74 | !jQuery(node.parentNode).hasClass(className) && 75 | !jQuery(node.parentNode).hasClass("nohighlight")) { 76 | var span; 77 | var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); 78 | if (isInSVG) { 79 | span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); 80 | } else { 81 | span = document.createElement("span"); 82 | span.className = className; 83 | } 84 | span.appendChild(document.createTextNode(val.substr(pos, text.length))); 85 | node.parentNode.insertBefore(span, node.parentNode.insertBefore( 86 | document.createTextNode(val.substr(pos + text.length)), 87 | node.nextSibling)); 88 | node.nodeValue = val.substr(0, pos); 89 | if (isInSVG) { 90 | var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); 91 | var bbox = node.parentElement.getBBox(); 92 | rect.x.baseVal.value = bbox.x; 93 | rect.y.baseVal.value = bbox.y; 94 | rect.width.baseVal.value = bbox.width; 95 | rect.height.baseVal.value = bbox.height; 96 | rect.setAttribute('class', className); 97 | addItems.push({ 98 | "parent": node.parentNode, 99 | "target": rect}); 100 | } 101 | } 102 | } 103 | else if (!jQuery(node).is("button, select, textarea")) { 104 | jQuery.each(node.childNodes, function() { 105 | highlight(this, addItems); 106 | }); 107 | } 108 | } 109 | var addItems = []; 110 | var result = this.each(function() { 111 | highlight(this, addItems); 112 | }); 113 | for (var i = 0; i < addItems.length; ++i) { 114 | jQuery(addItems[i].parent).before(addItems[i].target); 115 | } 116 | return result; 117 | }; 118 | 119 | /* 120 | * backward compatibility for jQuery.browser 121 | * This will be supported until firefox bug is fixed. 122 | */ 123 | if (!jQuery.browser) { 124 | jQuery.uaMatch = function(ua) { 125 | ua = ua.toLowerCase(); 126 | 127 | var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || 128 | /(webkit)[ \/]([\w.]+)/.exec(ua) || 129 | /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || 130 | /(msie) ([\w.]+)/.exec(ua) || 131 | ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || 132 | []; 133 | 134 | return { 135 | browser: match[ 1 ] || "", 136 | version: match[ 2 ] || "0" 137 | }; 138 | }; 139 | jQuery.browser = {}; 140 | jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; 141 | } 142 | 143 | /** 144 | * Small JavaScript module for the documentation. 145 | */ 146 | var Documentation = { 147 | 148 | init : function() { 149 | this.fixFirefoxAnchorBug(); 150 | this.highlightSearchWords(); 151 | this.initIndexTable(); 152 | if (DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) { 153 | this.initOnKeyListeners(); 154 | } 155 | }, 156 | 157 | /** 158 | * i18n support 159 | */ 160 | TRANSLATIONS : {}, 161 | PLURAL_EXPR : function(n) { return n === 1 ? 0 : 1; }, 162 | LOCALE : 'unknown', 163 | 164 | // gettext and ngettext don't access this so that the functions 165 | // can safely bound to a different name (_ = Documentation.gettext) 166 | gettext : function(string) { 167 | var translated = Documentation.TRANSLATIONS[string]; 168 | if (typeof translated === 'undefined') 169 | return string; 170 | return (typeof translated === 'string') ? translated : translated[0]; 171 | }, 172 | 173 | ngettext : function(singular, plural, n) { 174 | var translated = Documentation.TRANSLATIONS[singular]; 175 | if (typeof translated === 'undefined') 176 | return (n == 1) ? singular : plural; 177 | return translated[Documentation.PLURALEXPR(n)]; 178 | }, 179 | 180 | addTranslations : function(catalog) { 181 | for (var key in catalog.messages) 182 | this.TRANSLATIONS[key] = catalog.messages[key]; 183 | this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')'); 184 | this.LOCALE = catalog.locale; 185 | }, 186 | 187 | /** 188 | * add context elements like header anchor links 189 | */ 190 | addContextElements : function() { 191 | $('div[id] > :header:first').each(function() { 192 | $('\u00B6'). 193 | attr('href', '#' + this.id). 194 | attr('title', _('Permalink to this headline')). 195 | appendTo(this); 196 | }); 197 | $('dt[id]').each(function() { 198 | $('\u00B6'). 199 | attr('href', '#' + this.id). 200 | attr('title', _('Permalink to this definition')). 201 | appendTo(this); 202 | }); 203 | }, 204 | 205 | /** 206 | * workaround a firefox stupidity 207 | * see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075 208 | */ 209 | fixFirefoxAnchorBug : function() { 210 | if (document.location.hash && $.browser.mozilla) 211 | window.setTimeout(function() { 212 | document.location.href += ''; 213 | }, 10); 214 | }, 215 | 216 | /** 217 | * highlight the search words provided in the url in the text 218 | */ 219 | highlightSearchWords : function() { 220 | var params = $.getQueryParameters(); 221 | var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : []; 222 | if (terms.length) { 223 | var body = $('div.body'); 224 | if (!body.length) { 225 | body = $('body'); 226 | } 227 | window.setTimeout(function() { 228 | $.each(terms, function() { 229 | body.highlightText(this.toLowerCase(), 'highlighted'); 230 | }); 231 | }, 10); 232 | $('') 234 | .appendTo($('#searchbox')); 235 | } 236 | }, 237 | 238 | /** 239 | * init the domain index toggle buttons 240 | */ 241 | initIndexTable : function() { 242 | var togglers = $('img.toggler').click(function() { 243 | var src = $(this).attr('src'); 244 | var idnum = $(this).attr('id').substr(7); 245 | $('tr.cg-' + idnum).toggle(); 246 | if (src.substr(-9) === 'minus.png') 247 | $(this).attr('src', src.substr(0, src.length-9) + 'plus.png'); 248 | else 249 | $(this).attr('src', src.substr(0, src.length-8) + 'minus.png'); 250 | }).css('display', ''); 251 | if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) { 252 | togglers.click(); 253 | } 254 | }, 255 | 256 | /** 257 | * helper function to hide the search marks again 258 | */ 259 | hideSearchWords : function() { 260 | $('#searchbox .highlight-link').fadeOut(300); 261 | $('span.highlighted').removeClass('highlighted'); 262 | }, 263 | 264 | /** 265 | * make the url absolute 266 | */ 267 | makeURL : function(relativeURL) { 268 | return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL; 269 | }, 270 | 271 | /** 272 | * get the current relative url 273 | */ 274 | getCurrentURL : function() { 275 | var path = document.location.pathname; 276 | var parts = path.split(/\//); 277 | $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() { 278 | if (this === '..') 279 | parts.pop(); 280 | }); 281 | var url = parts.join('/'); 282 | return path.substring(url.lastIndexOf('/') + 1, path.length - 1); 283 | }, 284 | 285 | initOnKeyListeners: function() { 286 | $(document).keydown(function(event) { 287 | var activeElementType = document.activeElement.tagName; 288 | // don't navigate when in search box or textarea 289 | if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT' 290 | && !event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey) { 291 | switch (event.keyCode) { 292 | case 37: // left 293 | var prevHref = $('link[rel="prev"]').prop('href'); 294 | if (prevHref) { 295 | window.location.href = prevHref; 296 | return false; 297 | } 298 | case 39: // right 299 | var nextHref = $('link[rel="next"]').prop('href'); 300 | if (nextHref) { 301 | window.location.href = nextHref; 302 | return false; 303 | } 304 | } 305 | } 306 | }); 307 | } 308 | }; 309 | 310 | // quick alias for translations 311 | _ = Documentation.gettext; 312 | 313 | $(document).ready(function() { 314 | Documentation.init(); 315 | }); 316 | -------------------------------------------------------------------------------- /docs/_build/html/_static/underscore.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.3.1 2 | // (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. 3 | // Underscore is freely distributable under the MIT license. 4 | // Portions of Underscore are inspired or borrowed from Prototype, 5 | // Oliver Steele's Functional, and John Resig's Micro-Templating. 6 | // For all details and documentation: 7 | // http://documentcloud.github.com/underscore 8 | (function(){function q(a,c,d){if(a===c)return a!==0||1/a==1/c;if(a==null||c==null)return a===c;if(a._chain)a=a._wrapped;if(c._chain)c=c._wrapped;if(a.isEqual&&b.isFunction(a.isEqual))return a.isEqual(c);if(c.isEqual&&b.isFunction(c.isEqual))return c.isEqual(a);var e=l.call(a);if(e!=l.call(c))return false;switch(e){case "[object String]":return a==String(c);case "[object Number]":return a!=+a?c!=+c:a==0?1/a==1/c:a==+c;case "[object Date]":case "[object Boolean]":return+a==+c;case "[object RegExp]":return a.source== 9 | c.source&&a.global==c.global&&a.multiline==c.multiline&&a.ignoreCase==c.ignoreCase}if(typeof a!="object"||typeof c!="object")return false;for(var f=d.length;f--;)if(d[f]==a)return true;d.push(a);var f=0,g=true;if(e=="[object Array]"){if(f=a.length,g=f==c.length)for(;f--;)if(!(g=f in a==f in c&&q(a[f],c[f],d)))break}else{if("constructor"in a!="constructor"in c||a.constructor!=c.constructor)return false;for(var h in a)if(b.has(a,h)&&(f++,!(g=b.has(c,h)&&q(a[h],c[h],d))))break;if(g){for(h in c)if(b.has(c, 10 | h)&&!f--)break;g=!f}}d.pop();return g}var r=this,G=r._,n={},k=Array.prototype,o=Object.prototype,i=k.slice,H=k.unshift,l=o.toString,I=o.hasOwnProperty,w=k.forEach,x=k.map,y=k.reduce,z=k.reduceRight,A=k.filter,B=k.every,C=k.some,p=k.indexOf,D=k.lastIndexOf,o=Array.isArray,J=Object.keys,s=Function.prototype.bind,b=function(a){return new m(a)};if(typeof exports!=="undefined"){if(typeof module!=="undefined"&&module.exports)exports=module.exports=b;exports._=b}else r._=b;b.VERSION="1.3.1";var j=b.each= 11 | b.forEach=function(a,c,d){if(a!=null)if(w&&a.forEach===w)a.forEach(c,d);else if(a.length===+a.length)for(var e=0,f=a.length;e2;a== 12 | null&&(a=[]);if(y&&a.reduce===y)return e&&(c=b.bind(c,e)),f?a.reduce(c,d):a.reduce(c);j(a,function(a,b,i){f?d=c.call(e,d,a,b,i):(d=a,f=true)});if(!f)throw new TypeError("Reduce of empty array with no initial value");return d};b.reduceRight=b.foldr=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(z&&a.reduceRight===z)return e&&(c=b.bind(c,e)),f?a.reduceRight(c,d):a.reduceRight(c);var g=b.toArray(a).reverse();e&&!f&&(c=b.bind(c,e));return f?b.reduce(g,c,d,e):b.reduce(g,c)};b.find=b.detect= 13 | function(a,c,b){var e;E(a,function(a,g,h){if(c.call(b,a,g,h))return e=a,true});return e};b.filter=b.select=function(a,c,b){var e=[];if(a==null)return e;if(A&&a.filter===A)return a.filter(c,b);j(a,function(a,g,h){c.call(b,a,g,h)&&(e[e.length]=a)});return e};b.reject=function(a,c,b){var e=[];if(a==null)return e;j(a,function(a,g,h){c.call(b,a,g,h)||(e[e.length]=a)});return e};b.every=b.all=function(a,c,b){var e=true;if(a==null)return e;if(B&&a.every===B)return a.every(c,b);j(a,function(a,g,h){if(!(e= 14 | e&&c.call(b,a,g,h)))return n});return e};var E=b.some=b.any=function(a,c,d){c||(c=b.identity);var e=false;if(a==null)return e;if(C&&a.some===C)return a.some(c,d);j(a,function(a,b,h){if(e||(e=c.call(d,a,b,h)))return n});return!!e};b.include=b.contains=function(a,c){var b=false;if(a==null)return b;return p&&a.indexOf===p?a.indexOf(c)!=-1:b=E(a,function(a){return a===c})};b.invoke=function(a,c){var d=i.call(arguments,2);return b.map(a,function(a){return(b.isFunction(c)?c||a:a[c]).apply(a,d)})};b.pluck= 15 | function(a,c){return b.map(a,function(a){return a[c]})};b.max=function(a,c,d){if(!c&&b.isArray(a))return Math.max.apply(Math,a);if(!c&&b.isEmpty(a))return-Infinity;var e={computed:-Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b>=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,c,d){if(!c&&b.isArray(a))return Math.min.apply(Math,a);if(!c&&b.isEmpty(a))return Infinity;var e={computed:Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;bd?1:0}),"value")};b.groupBy=function(a,c){var d={},e=b.isFunction(c)?c:function(a){return a[c]};j(a,function(a,b){var c=e(a,b);(d[c]||(d[c]=[])).push(a)});return d};b.sortedIndex=function(a, 17 | c,d){d||(d=b.identity);for(var e=0,f=a.length;e>1;d(a[g])=0})})};b.difference=function(a){var c=b.flatten(i.call(arguments,1));return b.filter(a,function(a){return!b.include(c,a)})};b.zip=function(){for(var a=i.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),e=0;e=0;d--)b=[a[d].apply(this,b)];return b[0]}}; 24 | b.after=function(a,b){return a<=0?b():function(){if(--a<1)return b.apply(this,arguments)}};b.keys=J||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var c=[],d;for(d in a)b.has(a,d)&&(c[c.length]=d);return c};b.values=function(a){return b.map(a,b.identity)};b.functions=b.methods=function(a){var c=[],d;for(d in a)b.isFunction(a[d])&&c.push(d);return c.sort()};b.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]=b[d]});return a};b.defaults=function(a){j(i.call(arguments, 25 | 1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return!b.isObject(a)?a:b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,b){return q(a,b,[])};b.isEmpty=function(a){if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(b.has(a,c))return false;return true};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=o||function(a){return l.call(a)=="[object Array]"};b.isObject=function(a){return a===Object(a)}; 26 | b.isArguments=function(a){return l.call(a)=="[object Arguments]"};if(!b.isArguments(arguments))b.isArguments=function(a){return!(!a||!b.has(a,"callee"))};b.isFunction=function(a){return l.call(a)=="[object Function]"};b.isString=function(a){return l.call(a)=="[object String]"};b.isNumber=function(a){return l.call(a)=="[object Number]"};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===true||a===false||l.call(a)=="[object Boolean]"};b.isDate=function(a){return l.call(a)=="[object Date]"}; 27 | b.isRegExp=function(a){return l.call(a)=="[object RegExp]"};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.has=function(a,b){return I.call(a,b)};b.noConflict=function(){r._=G;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=0;e/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")};b.mixin=function(a){j(b.functions(a), 28 | function(c){K(c,b[c]=a[c])})};var L=0;b.uniqueId=function(a){var b=L++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var t=/.^/,u=function(a){return a.replace(/\\\\/g,"\\").replace(/\\'/g,"'")};b.template=function(a,c){var d=b.templateSettings,d="var __p=[],print=function(){__p.push.apply(__p,arguments);};with(obj||{}){__p.push('"+a.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(d.escape||t,function(a,b){return"',_.escape("+ 29 | u(b)+"),'"}).replace(d.interpolate||t,function(a,b){return"',"+u(b)+",'"}).replace(d.evaluate||t,function(a,b){return"');"+u(b).replace(/[\r\n\t]/g," ")+";__p.push('"}).replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t")+"');}return __p.join('');",e=new Function("obj","_",d);return c?e(c,b):function(a){return e.call(this,a,b)}};b.chain=function(a){return b(a).chain()};var m=function(a){this._wrapped=a};b.prototype=m.prototype;var v=function(a,c){return c?b(a).chain():a},K=function(a,c){m.prototype[a]= 30 | function(){var a=i.call(arguments);H.call(a,this._wrapped);return v(c.apply(b,a),this._chain)}};b.mixin(b);j("pop,push,reverse,shift,sort,splice,unshift".split(","),function(a){var b=k[a];m.prototype[a]=function(){var d=this._wrapped;b.apply(d,arguments);var e=d.length;(a=="shift"||a=="splice")&&e===0&&delete d[0];return v(d,this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];m.prototype[a]=function(){return v(b.apply(this._wrapped,arguments),this._chain)}});m.prototype.chain=function(){this._chain= 31 | true;return this};m.prototype.value=function(){return this._wrapped}}).call(this); 32 | -------------------------------------------------------------------------------- /docs/_build/html/_static/language_data.js: -------------------------------------------------------------------------------- 1 | /* 2 | * language_data.js 3 | * ~~~~~~~~~~~~~~~~ 4 | * 5 | * This script contains the language-specific data used by searchtools.js, 6 | * namely the list of stopwords, stemmer, scorer and splitter. 7 | * 8 | * :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. 9 | * :license: BSD, see LICENSE for details. 10 | * 11 | */ 12 | 13 | var stopwords = ["a","and","are","as","at","be","but","by","for","if","in","into","is","it","near","no","not","of","on","or","such","that","the","their","then","there","these","they","this","to","was","will","with"]; 14 | 15 | 16 | /* Non-minified version JS is _stemmer.js if file is provided */ 17 | /** 18 | * Porter Stemmer 19 | */ 20 | var Stemmer = function() { 21 | 22 | var step2list = { 23 | ational: 'ate', 24 | tional: 'tion', 25 | enci: 'ence', 26 | anci: 'ance', 27 | izer: 'ize', 28 | bli: 'ble', 29 | alli: 'al', 30 | entli: 'ent', 31 | eli: 'e', 32 | ousli: 'ous', 33 | ization: 'ize', 34 | ation: 'ate', 35 | ator: 'ate', 36 | alism: 'al', 37 | iveness: 'ive', 38 | fulness: 'ful', 39 | ousness: 'ous', 40 | aliti: 'al', 41 | iviti: 'ive', 42 | biliti: 'ble', 43 | logi: 'log' 44 | }; 45 | 46 | var step3list = { 47 | icate: 'ic', 48 | ative: '', 49 | alize: 'al', 50 | iciti: 'ic', 51 | ical: 'ic', 52 | ful: '', 53 | ness: '' 54 | }; 55 | 56 | var c = "[^aeiou]"; // consonant 57 | var v = "[aeiouy]"; // vowel 58 | var C = c + "[^aeiouy]*"; // consonant sequence 59 | var V = v + "[aeiou]*"; // vowel sequence 60 | 61 | var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 62 | var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 63 | var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 64 | var s_v = "^(" + C + ")?" + v; // vowel in stem 65 | 66 | this.stemWord = function (w) { 67 | var stem; 68 | var suffix; 69 | var firstch; 70 | var origword = w; 71 | 72 | if (w.length < 3) 73 | return w; 74 | 75 | var re; 76 | var re2; 77 | var re3; 78 | var re4; 79 | 80 | firstch = w.substr(0,1); 81 | if (firstch == "y") 82 | w = firstch.toUpperCase() + w.substr(1); 83 | 84 | // Step 1a 85 | re = /^(.+?)(ss|i)es$/; 86 | re2 = /^(.+?)([^s])s$/; 87 | 88 | if (re.test(w)) 89 | w = w.replace(re,"$1$2"); 90 | else if (re2.test(w)) 91 | w = w.replace(re2,"$1$2"); 92 | 93 | // Step 1b 94 | re = /^(.+?)eed$/; 95 | re2 = /^(.+?)(ed|ing)$/; 96 | if (re.test(w)) { 97 | var fp = re.exec(w); 98 | re = new RegExp(mgr0); 99 | if (re.test(fp[1])) { 100 | re = /.$/; 101 | w = w.replace(re,""); 102 | } 103 | } 104 | else if (re2.test(w)) { 105 | var fp = re2.exec(w); 106 | stem = fp[1]; 107 | re2 = new RegExp(s_v); 108 | if (re2.test(stem)) { 109 | w = stem; 110 | re2 = /(at|bl|iz)$/; 111 | re3 = new RegExp("([^aeiouylsz])\\1$"); 112 | re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); 113 | if (re2.test(w)) 114 | w = w + "e"; 115 | else if (re3.test(w)) { 116 | re = /.$/; 117 | w = w.replace(re,""); 118 | } 119 | else if (re4.test(w)) 120 | w = w + "e"; 121 | } 122 | } 123 | 124 | // Step 1c 125 | re = /^(.+?)y$/; 126 | if (re.test(w)) { 127 | var fp = re.exec(w); 128 | stem = fp[1]; 129 | re = new RegExp(s_v); 130 | if (re.test(stem)) 131 | w = stem + "i"; 132 | } 133 | 134 | // Step 2 135 | re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; 136 | if (re.test(w)) { 137 | var fp = re.exec(w); 138 | stem = fp[1]; 139 | suffix = fp[2]; 140 | re = new RegExp(mgr0); 141 | if (re.test(stem)) 142 | w = stem + step2list[suffix]; 143 | } 144 | 145 | // Step 3 146 | re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; 147 | if (re.test(w)) { 148 | var fp = re.exec(w); 149 | stem = fp[1]; 150 | suffix = fp[2]; 151 | re = new RegExp(mgr0); 152 | if (re.test(stem)) 153 | w = stem + step3list[suffix]; 154 | } 155 | 156 | // Step 4 157 | re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; 158 | re2 = /^(.+?)(s|t)(ion)$/; 159 | if (re.test(w)) { 160 | var fp = re.exec(w); 161 | stem = fp[1]; 162 | re = new RegExp(mgr1); 163 | if (re.test(stem)) 164 | w = stem; 165 | } 166 | else if (re2.test(w)) { 167 | var fp = re2.exec(w); 168 | stem = fp[1] + fp[2]; 169 | re2 = new RegExp(mgr1); 170 | if (re2.test(stem)) 171 | w = stem; 172 | } 173 | 174 | // Step 5 175 | re = /^(.+?)e$/; 176 | if (re.test(w)) { 177 | var fp = re.exec(w); 178 | stem = fp[1]; 179 | re = new RegExp(mgr1); 180 | re2 = new RegExp(meq1); 181 | re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); 182 | if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) 183 | w = stem; 184 | } 185 | re = /ll$/; 186 | re2 = new RegExp(mgr1); 187 | if (re.test(w) && re2.test(w)) { 188 | re = /.$/; 189 | w = w.replace(re,""); 190 | } 191 | 192 | // and turn initial Y back to y 193 | if (firstch == "y") 194 | w = firstch.toLowerCase() + w.substr(1); 195 | return w; 196 | } 197 | } 198 | 199 | 200 | 201 | 202 | 203 | var splitChars = (function() { 204 | var result = {}; 205 | var singles = [96, 180, 187, 191, 215, 247, 749, 885, 903, 907, 909, 930, 1014, 1648, 206 | 1748, 1809, 2416, 2473, 2481, 2526, 2601, 2609, 2612, 2615, 2653, 2702, 207 | 2706, 2729, 2737, 2740, 2857, 2865, 2868, 2910, 2928, 2948, 2961, 2971, 208 | 2973, 3085, 3089, 3113, 3124, 3213, 3217, 3241, 3252, 3295, 3341, 3345, 209 | 3369, 3506, 3516, 3633, 3715, 3721, 3736, 3744, 3748, 3750, 3756, 3761, 210 | 3781, 3912, 4239, 4347, 4681, 4695, 4697, 4745, 4785, 4799, 4801, 4823, 211 | 4881, 5760, 5901, 5997, 6313, 7405, 8024, 8026, 8028, 8030, 8117, 8125, 212 | 8133, 8181, 8468, 8485, 8487, 8489, 8494, 8527, 11311, 11359, 11687, 11695, 213 | 11703, 11711, 11719, 11727, 11735, 12448, 12539, 43010, 43014, 43019, 43587, 214 | 43696, 43713, 64286, 64297, 64311, 64317, 64319, 64322, 64325, 65141]; 215 | var i, j, start, end; 216 | for (i = 0; i < singles.length; i++) { 217 | result[singles[i]] = true; 218 | } 219 | var ranges = [[0, 47], [58, 64], [91, 94], [123, 169], [171, 177], [182, 184], [706, 709], 220 | [722, 735], [741, 747], [751, 879], [888, 889], [894, 901], [1154, 1161], 221 | [1318, 1328], [1367, 1368], [1370, 1376], [1416, 1487], [1515, 1519], [1523, 1568], 222 | [1611, 1631], [1642, 1645], [1750, 1764], [1767, 1773], [1789, 1790], [1792, 1807], 223 | [1840, 1868], [1958, 1968], [1970, 1983], [2027, 2035], [2038, 2041], [2043, 2047], 224 | [2070, 2073], [2075, 2083], [2085, 2087], [2089, 2307], [2362, 2364], [2366, 2383], 225 | [2385, 2391], [2402, 2405], [2419, 2424], [2432, 2436], [2445, 2446], [2449, 2450], 226 | [2483, 2485], [2490, 2492], [2494, 2509], [2511, 2523], [2530, 2533], [2546, 2547], 227 | [2554, 2564], [2571, 2574], [2577, 2578], [2618, 2648], [2655, 2661], [2672, 2673], 228 | [2677, 2692], [2746, 2748], [2750, 2767], [2769, 2783], [2786, 2789], [2800, 2820], 229 | [2829, 2830], [2833, 2834], [2874, 2876], [2878, 2907], [2914, 2917], [2930, 2946], 230 | [2955, 2957], [2966, 2968], [2976, 2978], [2981, 2983], [2987, 2989], [3002, 3023], 231 | [3025, 3045], [3059, 3076], [3130, 3132], [3134, 3159], [3162, 3167], [3170, 3173], 232 | [3184, 3191], [3199, 3204], [3258, 3260], [3262, 3293], [3298, 3301], [3312, 3332], 233 | [3386, 3388], [3390, 3423], [3426, 3429], [3446, 3449], [3456, 3460], [3479, 3481], 234 | [3518, 3519], [3527, 3584], [3636, 3647], [3655, 3663], [3674, 3712], [3717, 3718], 235 | [3723, 3724], [3726, 3731], [3752, 3753], [3764, 3772], [3774, 3775], [3783, 3791], 236 | [3802, 3803], [3806, 3839], [3841, 3871], [3892, 3903], [3949, 3975], [3980, 4095], 237 | [4139, 4158], [4170, 4175], [4182, 4185], [4190, 4192], [4194, 4196], [4199, 4205], 238 | [4209, 4212], [4226, 4237], [4250, 4255], [4294, 4303], [4349, 4351], [4686, 4687], 239 | [4702, 4703], [4750, 4751], [4790, 4791], [4806, 4807], [4886, 4887], [4955, 4968], 240 | [4989, 4991], [5008, 5023], [5109, 5120], [5741, 5742], [5787, 5791], [5867, 5869], 241 | [5873, 5887], [5906, 5919], [5938, 5951], [5970, 5983], [6001, 6015], [6068, 6102], 242 | [6104, 6107], [6109, 6111], [6122, 6127], [6138, 6159], [6170, 6175], [6264, 6271], 243 | [6315, 6319], [6390, 6399], [6429, 6469], [6510, 6511], [6517, 6527], [6572, 6592], 244 | [6600, 6607], [6619, 6655], [6679, 6687], [6741, 6783], [6794, 6799], [6810, 6822], 245 | [6824, 6916], [6964, 6980], [6988, 6991], [7002, 7042], [7073, 7085], [7098, 7167], 246 | [7204, 7231], [7242, 7244], [7294, 7400], [7410, 7423], [7616, 7679], [7958, 7959], 247 | [7966, 7967], [8006, 8007], [8014, 8015], [8062, 8063], [8127, 8129], [8141, 8143], 248 | [8148, 8149], [8156, 8159], [8173, 8177], [8189, 8303], [8306, 8307], [8314, 8318], 249 | [8330, 8335], [8341, 8449], [8451, 8454], [8456, 8457], [8470, 8472], [8478, 8483], 250 | [8506, 8507], [8512, 8516], [8522, 8525], [8586, 9311], [9372, 9449], [9472, 10101], 251 | [10132, 11263], [11493, 11498], [11503, 11516], [11518, 11519], [11558, 11567], 252 | [11622, 11630], [11632, 11647], [11671, 11679], [11743, 11822], [11824, 12292], 253 | [12296, 12320], [12330, 12336], [12342, 12343], [12349, 12352], [12439, 12444], 254 | [12544, 12548], [12590, 12592], [12687, 12689], [12694, 12703], [12728, 12783], 255 | [12800, 12831], [12842, 12880], [12896, 12927], [12938, 12976], [12992, 13311], 256 | [19894, 19967], [40908, 40959], [42125, 42191], [42238, 42239], [42509, 42511], 257 | [42540, 42559], [42592, 42593], [42607, 42622], [42648, 42655], [42736, 42774], 258 | [42784, 42785], [42889, 42890], [42893, 43002], [43043, 43055], [43062, 43071], 259 | [43124, 43137], [43188, 43215], [43226, 43249], [43256, 43258], [43260, 43263], 260 | [43302, 43311], [43335, 43359], [43389, 43395], [43443, 43470], [43482, 43519], 261 | [43561, 43583], [43596, 43599], [43610, 43615], [43639, 43641], [43643, 43647], 262 | [43698, 43700], [43703, 43704], [43710, 43711], [43715, 43738], [43742, 43967], 263 | [44003, 44015], [44026, 44031], [55204, 55215], [55239, 55242], [55292, 55295], 264 | [57344, 63743], [64046, 64047], [64110, 64111], [64218, 64255], [64263, 64274], 265 | [64280, 64284], [64434, 64466], [64830, 64847], [64912, 64913], [64968, 65007], 266 | [65020, 65135], [65277, 65295], [65306, 65312], [65339, 65344], [65371, 65381], 267 | [65471, 65473], [65480, 65481], [65488, 65489], [65496, 65497]]; 268 | for (i = 0; i < ranges.length; i++) { 269 | start = ranges[i][0]; 270 | end = ranges[i][1]; 271 | for (j = start; j <= end; j++) { 272 | result[j] = true; 273 | } 274 | } 275 | return result; 276 | })(); 277 | 278 | function splitQuery(query) { 279 | var result = []; 280 | var start = -1; 281 | for (var i = 0; i < query.length; i++) { 282 | if (splitChars[query.charCodeAt(i)]) { 283 | if (start !== -1) { 284 | result.push(query.slice(start, i)); 285 | start = -1; 286 | } 287 | } else if (start === -1) { 288 | start = i; 289 | } 290 | } 291 | if (start !== -1) { 292 | result.push(query.slice(start)); 293 | } 294 | return result; 295 | } 296 | 297 | 298 | -------------------------------------------------------------------------------- /ovisbot/extensions/hackthebox/hackthebox.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | import logging 4 | import json 5 | import requests 6 | import discord 7 | import dataclasses 8 | import datetime 9 | 10 | from discord.ext import commands 11 | 12 | from ovisbot.helpers import success, escape_md 13 | from ovisbot.db_models import HTBUserMapping 14 | from json.decoder import JSONDecodeError 15 | from functools import wraps, partial 16 | from bs4 import BeautifulSoup 17 | from parse import parse 18 | import asyncio 19 | 20 | logging.basicConfig(level=logging.INFO) 21 | logger = logging.getLogger(__name__) 22 | 23 | 24 | @dataclasses.dataclass 25 | class HTBStats: 26 | points: int 27 | user_owns: int 28 | system_owns: int 29 | challs_solved: int 30 | global_rank: int 31 | 32 | @classmethod 33 | def parse(cls, *args): 34 | return cls(*list(map(int, args))) 35 | 36 | 37 | class HTBAPIException(Exception): 38 | pass 39 | 40 | 41 | class HTBAPIClient(object): 42 | """ 43 | HTB API: https://github.com/mxrch/htb_api 44 | """ 45 | 46 | def __init__(self, htb_creds_email, htb_creds_pass): 47 | self.root_url = "https://www.hackthebox.eu" 48 | self.authenticated = False 49 | self.session = requests.Session() 50 | self.htb_creds_email = htb_creds_email 51 | self.htb_creds_pass = htb_creds_pass 52 | self.headers = { 53 | "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_0)" 54 | "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36", 55 | } 56 | 57 | def handle_errors(func): 58 | @wraps(func) 59 | def func_wrapper(inst, *args, **kwargs): 60 | try: 61 | return func(inst, *args, **kwargs) 62 | except JSONDecodeError as e: 63 | raise HTBAPIException(e) 64 | 65 | return func_wrapper 66 | 67 | def authenticated(func): 68 | @wraps(func) 69 | def func_wrapper(inst, *args, **kwargs): 70 | if not inst._check_authenticated(): 71 | inst._login(inst.htb_creds_email, inst.htb_creds_pass) 72 | return func(inst, *args, **kwargs) 73 | 74 | return func_wrapper 75 | 76 | def get(self, url, headers={}): 77 | req = requests.get(url, headers=self.headers) 78 | return json.loads(req.text) 79 | 80 | def _get_points_from_soup(self, soup): 81 | h2_elems = soup.find_all("h2", class_="no-margins") 82 | for h2_elem in h2_elems: 83 | i_elem = h2_elem.find("i", class_="fa-crosshairs") 84 | if i_elem: 85 | return i_elem.parent.text 86 | 87 | def _get_system_owns_from_soup(self, soup): 88 | h2_elems = soup.find_all("h2", class_="no-margins") 89 | for h2_elem in h2_elems: 90 | i_elem = h2_elem.find("i", class_="pe-7s-ticket") 91 | if i_elem: 92 | return i_elem.parent.text 93 | 94 | def _get_user_owns_from_soup(self, soup): 95 | h2_elems = soup.find_all("h2", class_="no-margins") 96 | for h2_elem in h2_elems: 97 | i_elem = h2_elem.find("i", class_="pe-7s-user") 98 | if i_elem: 99 | return i_elem.parent.text 100 | 101 | def _get_rank_from_soup(self, soup): 102 | elem = soup(text=re.compile(r"is at position.*of the Hall of Fame")) 103 | if elem: 104 | return parse( 105 | "Member {username} is at position {rank} of the Hall of Fame.", 106 | str(elem[0]).strip(), 107 | )["rank"] 108 | return "0" 109 | 110 | def _get_challsolved_from_soup(self, soup): 111 | elem = soup(text=re.compile(r".*has solved.*challenges")) 112 | return parse( 113 | "{username} has solved {challsolved} challenges.", str(elem[0]).strip() 114 | )["challsolved"] 115 | 116 | def _check_authenticated(self): 117 | url = f"{self.root_url}/login" 118 | login = self.session.get(url, headers=self.headers) 119 | if b"loginForm" in login.content: 120 | self.authenticated = False 121 | return False 122 | self.authenticated = True 123 | return True 124 | 125 | def _login(self, email, password): 126 | url = f"{self.root_url}/login" 127 | page = self.session.get(url, headers=self.headers) 128 | soup = BeautifulSoup(page.content, "lxml") 129 | token = soup.find("input", {"name": "_token"}).get("value") 130 | login_data = {"_token": token, "email": email, "password": password} 131 | self.session.post(url, data=login_data, headers=self.headers) 132 | if self._check_authenticated(): 133 | logger.info("HTB authentication successful!") 134 | else: 135 | logger.error("HTB authentication failed... Disabling commands...") 136 | 137 | def login(self): 138 | self._login(self.htb_creds_email, self.htb_creds_pass) 139 | 140 | @handle_errors 141 | def identify_user(self, identifier): 142 | url = f"{self.root_url}/api/users/identifier/{identifier}/" 143 | return self.get(url) 144 | 145 | @authenticated 146 | @handle_errors 147 | def parse_user_stats(self, user_id): 148 | url = f"{self.root_url}/home/users/profile/{user_id}" 149 | headers = { 150 | "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_0)" 151 | "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36", 152 | } 153 | page = self.session.get(url, headers=headers) 154 | soup = BeautifulSoup(page.content, "lxml") 155 | return HTBStats.parse( 156 | self._get_points_from_soup(soup), 157 | self._get_user_owns_from_soup(soup), 158 | self._get_system_owns_from_soup(soup), 159 | self._get_challsolved_from_soup(soup), 160 | self._get_rank_from_soup(soup), 161 | ) 162 | 163 | 164 | class HackTheBox(commands.Cog): 165 | def __init__(self, bot): 166 | self.bot = bot 167 | self.api_client = HTBAPIClient( 168 | bot.config.HTB_CREDS_EMAIL, bot.config.HTB_CREDS_PASS 169 | ) 170 | 171 | @commands.group() 172 | async def htb(self, ctx): 173 | """ 174 | Collection of Hack The Box commands 175 | """ 176 | if ctx.invoked_subcommand is None: 177 | await ctx.send("Invalid command passed. Use `!help`.") 178 | 179 | @htb.command() 180 | async def connect(self, ctx, identifier=None): 181 | """ 182 | Link your Hack The Box account 183 | """ 184 | if isinstance(ctx.channel, discord.DMChannel): 185 | if identifier is None: 186 | msg = ( 187 | "Εν μου έδωκες account identifier! Πάεννε στο https://www.hackthebox.eu/home/settings" 188 | "τζαι πιαστο account identifier που εν κάτω κάτω! Μια ζωη να σας παρατζέλλω δαμέσα!" 189 | ) 190 | await ctx.send(msg) 191 | else: 192 | htb_user = self.api_client.identify_user(identifier) 193 | try: 194 | user_mapping = HTBUserMapping.objects.get( 195 | {"discord_user_id": ctx.author.id} 196 | ) 197 | user_mapping.htb_user = htb_user["user_name"] 198 | user_mapping.htb_user_id = htb_user["user_id"] 199 | except HTBUserMapping.DoesNotExist: 200 | user_mapping = HTBUserMapping( 201 | htb_user=htb_user["user_name"], 202 | htb_user_id=htb_user["user_id"], 203 | discord_user_id=ctx.author.id, 204 | ) 205 | finally: 206 | user_mapping.save() 207 | await ctx.send(f"Linked Hack-The-Box as {htb_user['user_name']}!") 208 | await success(ctx.message) 209 | self.api_client.parse_user_stats(htb_user["user_id"]) 210 | else: 211 | await ctx.channel.send( 212 | "Να σε κλέψουν τζαι να μεν πάρεις είδηση εσυ ρε... Στείλε DM" 213 | ) 214 | 215 | @htb.command() 216 | async def disconnect(self, ctx): 217 | """ 218 | Disconecct your Hack The Box account 219 | """ 220 | htb_mapping = HTBUserMapping.objects.get({"discord_user_id": ctx.author.id}) 221 | htb_mapping.delete() 222 | await success(ctx.message) 223 | 224 | @htb.command() 225 | async def stats(self, ctx, user=None): 226 | """ 227 | Display your Hack The Box stats or any given user's 228 | """ 229 | if user is None: 230 | user_id = ctx.author.id 231 | else: 232 | user_id = next((m.id for m in ctx.message.mentions), None) 233 | if user_id is None: 234 | return 235 | 236 | htb_mapping = HTBUserMapping.objects.get({"discord_user_id": user_id}) 237 | stats = self.api_client.parse_user_stats(htb_mapping.htb_user_id) 238 | 239 | embed = discord.Embed( 240 | title=f"{htb_mapping.htb_user}", 241 | colour=discord.Colour(0x7ED321), 242 | url=f"https://www.hackthebox.eu/home/users/profile/{htb_mapping.htb_user_id}", 243 | description=f"Currently at position **{stats.global_rank}** of the Hall of Fame.", 244 | timestamp=datetime.datetime.now(), 245 | ) 246 | 247 | embed.set_thumbnail(url="https://forum.hackthebox.eu/uploads/RJZMUY81IQLQ.png") 248 | embed.set_footer( 249 | text="CYberMouflons", icon_url="https://i.ibb.co/yW2mYjq/cybermouflons.png" 250 | ) 251 | 252 | embed.add_field(name="Points", value=f"{stats.points}", inline=True) 253 | embed.add_field(name="System Owns", value=f"{stats.system_owns}", inline=True) 254 | embed.add_field(name="User Owns", value=f"{stats.user_owns}", inline=True) 255 | embed.add_field( 256 | name="Challenges Solved", value=f"{stats.challs_solved}", inline=True 257 | ) 258 | 259 | await ctx.send(embed=embed) 260 | 261 | @htb.command() 262 | async def scoreboard(self, ctx): 263 | """ 264 | Displays internal Hack The Box scoreboard. 265 | """ 266 | limit = 10 267 | mappings = HTBUserMapping.objects.all() 268 | 269 | self.api_client.login() # just to avoid parallel logins 270 | 271 | async def getscore(mapping): 272 | return ( 273 | escape_md(self.bot.get_user(mapping.discord_user_id).name), 274 | self.api_client.parse_user_stats(mapping.htb_user_id), 275 | ) 276 | 277 | tasks = [getscore(mapping) for mapping in mappings] 278 | scores = [s for s in await asyncio.gather(*tasks)] 279 | 280 | scores = sorted(scores, key=lambda s: s[1].points, reverse=True)[:limit] 281 | scoreboard = "\n".join( 282 | "{0}. **{1}**\t{2}".format(idx + 1, s[0], s[1].points) 283 | for idx, s in enumerate(scores) 284 | ) 285 | embed = discord.Embed( 286 | title="Hack The Box Scoreboard", 287 | colour=discord.Colour(0x7ED321), 288 | url="https://www.hackthebox.eu", 289 | description=scoreboard, 290 | timestamp=datetime.datetime.now(), 291 | ) 292 | 293 | embed.set_thumbnail(url="https://forum.hackthebox.eu/uploads/RJZMUY81IQLQ.png") 294 | embed.set_footer( 295 | text="CYberMouflons", 296 | icon_url="https://i.ibb.co/yW2mYjq/cybermouflons.png", 297 | ) 298 | 299 | await ctx.send(embed=embed) 300 | 301 | @scoreboard.error 302 | @disconnect.error 303 | @connect.error 304 | @stats.error 305 | async def generic_error_handler(self, ctx, error): 306 | if isinstance(error.original, HTBUserMapping.DoesNotExist): 307 | await ctx.channel.send( 308 | "Ρε λεβεντη... εν βρίσκω συνδεδεμένο Hack The Box account! (`!htb connect `)" 309 | ) 310 | elif isinstance(error.original, HTBAPIException): 311 | await ctx.channel.send("Ούπς... κατι επήε λάθος ρε τσιάκκο!") 312 | 313 | 314 | async def setup(bot): 315 | await bot.add_cog(HackTheBox(bot)) 316 | -------------------------------------------------------------------------------- /ovisbot/commands/manage.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import discord 3 | import logging 4 | import os 5 | import json 6 | import ovisbot.locale as i18n 7 | 8 | from discord.ext import commands 9 | from functools import partial 10 | 11 | from ovisbot import __version__ 12 | from ovisbot.cog_manager import ( 13 | CogAlreadyInstalledException, 14 | CogSpecificationMissingException, 15 | ) 16 | from ovisbot.helpers import success, failed 17 | from ovisbot.db_models import CTF, SSHKey 18 | 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | 23 | class ManageCommandsMixin: 24 | # flake8: noqa: C901 25 | def load_commands(self): 26 | """Hooks commands with bot subclass""" 27 | bot = self 28 | 29 | @bot.group(hidden=True) 30 | @commands.has_role(bot.config.ADMIN_ROLE) 31 | async def manage(ctx): 32 | if ctx.invoked_subcommand is None: 33 | await ctx.send("Invalid command passed. Use !help.") 34 | 35 | @manage.command() 36 | async def version(ctx): 37 | """Displays bot version""" 38 | await ctx.send("OvisBot: v{0}".format(__version__)) 39 | 40 | @manage.command() 41 | async def showconfig(ctx): 42 | """Displays bot config""" 43 | await ctx.send("```{0}```".format(bot.config.__dict__)) 44 | 45 | @manage.command() 46 | async def maintenance(ctx): 47 | """Enables/Disables maintenance mode""" 48 | self.config.IS_MAINTENANCE = not self.config.IS_MAINTENANCE 49 | self.config.save() 50 | await success(ctx.message) 51 | if self.config.IS_MAINTENANCE: 52 | await ctx.send("Maintenance mode enabled!") 53 | else: 54 | await ctx.send("Maintenance mode disabled!") 55 | 56 | @manage.group() 57 | async def config(ctx): 58 | """ 59 | Group of commangs to manage bot configuration / Displays a config opions and values if run without a subcommand 60 | """ 61 | if ctx.subcommand_passed is None: 62 | await ctx.send("```" + bot.config.options_table() + "```") 63 | elif ctx.invoked_subcommand is None: 64 | self.help_command.context = ctx 65 | await failed(ctx.message) 66 | await ctx.send( 67 | i18n._("**Invalid command passed**. See below for more help") 68 | ) 69 | await self.help_command.command_callback(ctx, command=str(ctx.command)) 70 | 71 | @config.command() 72 | async def set(ctx, option, value): 73 | """Sets given config variable""" 74 | if hasattr(self.config, option): 75 | value = type(value)(value) 76 | setattr(self.config, option, value) 77 | self.config.save() 78 | await success(ctx.message) 79 | else: 80 | await ctx.send(i18n._("Property {0} does not exist".format(option))) 81 | await failed(ctx.message) 82 | 83 | @manage.group() 84 | async def keys(ctx): 85 | """ 86 | Group of commangs to manage extensions / Displays a list of all installed extensions if no subcommand passed 87 | """ 88 | if ctx.subcommand_passed is None: 89 | await ctx.send("```" + SSHKey.table_serialize() + "```") 90 | elif ctx.invoked_subcommand is None: 91 | self.help_command.context = ctx 92 | await failed(ctx.message) 93 | await ctx.send( 94 | i18n._("**Invalid command passed**. See below for more help") 95 | ) 96 | await self.help_command.command_callback(ctx, command=str(ctx.command)) 97 | 98 | @keys.command() 99 | async def add(ctx, keyname): 100 | """Adds an SSH key""" 101 | try: 102 | SSHKey.objects.get({"name": keyname}) 103 | except SSHKey.DoesNotExist: 104 | pass 105 | else: 106 | await ctx.send( 107 | i18n._("This key name already exists. Choose another one.") 108 | ) 109 | return 110 | 111 | if not isinstance(ctx.channel, discord.DMChannel): 112 | await ctx.send( 113 | i18n._("This is a public channel. Continue the process through DM.") 114 | ) 115 | 116 | # TODO: Create key manager to handle these sort of tasks 117 | async def prompt_keys(): 118 | """Creates coroutines to pick up private and public keys and saves key""" 119 | 120 | def is_key(add_key_msg, prefix, message): 121 | return ( 122 | isinstance(message.channel, discord.DMChannel) 123 | and add_key_msg.author == message.author 124 | and message.content.startswith(prefix) 125 | and message.attachments 126 | ) 127 | 128 | priv_prefix = "private" 129 | pub_prefix = "public" 130 | await ctx.author.send( 131 | i18n._( 132 | "Please upload your private and public keys an attachment in a message that starts with `{0}` and `{1}` respectively.".format( 133 | priv_prefix, pub_prefix 134 | ) 135 | ) 136 | ) 137 | 138 | async def wait_for_ssh_private_key(add_key_msg): 139 | """Coroutine that waits for a private key to be uploaded""" 140 | url = None 141 | try: 142 | message = await bot.wait_for( 143 | "message", 144 | timeout=60.0, 145 | check=partial(is_key, add_key_msg, priv_prefix), 146 | ) 147 | except asyncio.TimeoutError: 148 | await ctx.author.send( 149 | i18n._("Timed out... Try running `addkey` again") 150 | ) 151 | else: 152 | await ctx.author.send(i18n._("Saved private key successfully!")) 153 | url = message.attachments[0].url 154 | return url 155 | 156 | async def wait_for_ssh_public_key(add_key_msg): 157 | """Coroutine that waits for a public key to be uploaded""" 158 | url = None 159 | try: 160 | message = await bot.wait_for( 161 | "message", 162 | timeout=60.0, 163 | check=partial(is_key, add_key_msg, pub_prefix), 164 | ) 165 | except asyncio.TimeoutError: 166 | await ctx.author.send( 167 | i18n._("Timed out... Try running `addkey` again") 168 | ) 169 | else: 170 | await ctx.author.send(i18n._("Saved public key successfully!")) 171 | url = message.attachments[0].url 172 | return url 173 | 174 | privkey_prompt_task = bot.loop.create_task( 175 | wait_for_ssh_private_key(ctx.message) 176 | ) 177 | public_prompt_task = bot.loop.create_task( 178 | wait_for_ssh_public_key(ctx.message) 179 | ) 180 | 181 | # logger.info(type(task)) 182 | private = await privkey_prompt_task 183 | public = await public_prompt_task 184 | 185 | key = SSHKey( 186 | name=keyname, 187 | owner_name=ctx.author.display_name, 188 | owner_id=str(ctx.author.id), 189 | private_key=private, 190 | public_key=public, 191 | ) 192 | key.save() 193 | 194 | message = await ctx.author.send(i18n._("Key added!")) 195 | await success(message) 196 | 197 | bot.loop.create_task(prompt_keys()) 198 | 199 | @keys.command() 200 | async def rm(ctx, keyname): 201 | key = SSHKey.objects.get({"name": keyname}) 202 | key.delete() 203 | await success(ctx.message) 204 | 205 | @manage.group() 206 | async def extensions(ctx): 207 | """ 208 | Group of commangs to manage extensions / Displays a list of all installed extensions if no subcommand passed 209 | """ 210 | if ctx.subcommand_passed is None: 211 | await ctx.send("```" + self.cog_manager.cog_table() + "```") 212 | elif ctx.invoked_subcommand is None: 213 | self.help_command.context = ctx 214 | await failed(ctx.message) 215 | await ctx.send( 216 | i18n._("**Invalid command passed**. See below for more help") 217 | ) 218 | await self.help_command.command_callback(ctx, command=str(ctx.command)) 219 | 220 | @extensions.command() 221 | async def disable(ctx, name): 222 | """Disables an installed extension""" 223 | self.cog_manager.disable_cog(name) 224 | await success(ctx.message) 225 | 226 | @extensions.command() 227 | async def enable(ctx, name): 228 | """Enables an installed extension""" 229 | self.cog_manager.enable_cog(name) 230 | await success(ctx.message) 231 | 232 | @extensions.command() 233 | async def reset(ctx): 234 | """Resets extensions (Removes all third party extensions and enables builtin extensions)""" 235 | self.cog_manager.reset() 236 | await success(ctx.message) 237 | 238 | @extensions.command() 239 | async def reload(ctx, name): 240 | """Reloads an enabled extension""" 241 | self.cog_manager.reload(name) 242 | await success(ctx.message) 243 | 244 | @extensions.command() 245 | async def rm(ctx, name): 246 | """Removes an intalled extension""" 247 | self.cog_manager.remove(name) 248 | await success(ctx.message) 249 | 250 | @extensions.command() 251 | async def install(ctx, url, sshkey_name=None): 252 | """Installs a third party extension by git url""" 253 | sshkey = SSHKey.objects.get({"name": sshkey_name}) if sshkey_name else None 254 | self.cog_manager.install(url, sshkey) 255 | await success(ctx.message) 256 | 257 | @install.error 258 | @enable.error 259 | async def install_error(ctx, err): 260 | if isinstance(err.original, CogAlreadyInstalledException): 261 | await ctx.channel.send(i18n._("Extension already installed")) 262 | elif isinstance(err.original, SSHKey.DoesNotExist): 263 | await ctx.channel.send(i18n._("This key does not exist.")) 264 | elif isinstance(err.original, CogSpecificationMissingException): 265 | await ctx.channel.send( 266 | i18n._("Extension specification (extension.json) does not exist!") 267 | ) 268 | elif isinstance(err.original, discord.ext.commands.errors.ExtensionFailed): 269 | await ctx.channel.send(i18n._("Error when loading:")) 270 | logger.info(dir(err.original)) 271 | await ctx.channel.send("```" + err.original.args[0] + "```") 272 | await failed(ctx.message) 273 | 274 | @manage.command() 275 | @commands.has_role(bot.config.ADMIN_ROLE) 276 | async def sayin(ctx, channel_id, *, msg): 277 | """Sends the given message to the channel with the specified channel id""" 278 | channel = discord.utils.get(ctx.guild.text_channels, id=int(channel_id)) 279 | if channel is None: 280 | await ctx.channel.send(i18n._("Could not find channel...")) 281 | else: 282 | await channel.send(msg) 283 | await success(ctx.message) 284 | 285 | @manage.command() 286 | @commands.has_permissions(administrator=True) 287 | async def dropctfs(ctx): 288 | """Deletes CTF collection""" 289 | CTF._mongometa.collection.drop() 290 | await ctx.channel.send("Πάππαλα τα CTFs....") 291 | -------------------------------------------------------------------------------- /docs/_build/html/_static/alabaster.css: -------------------------------------------------------------------------------- 1 | @import url("basic.css"); 2 | 3 | /* -- page layout ----------------------------------------------------------- */ 4 | 5 | body { 6 | font-family: Georgia, serif; 7 | font-size: 17px; 8 | background-color: #fff; 9 | color: #000; 10 | margin: 0; 11 | padding: 0; 12 | } 13 | 14 | 15 | div.document { 16 | width: 940px; 17 | margin: 30px auto 0 auto; 18 | } 19 | 20 | div.documentwrapper { 21 | float: left; 22 | width: 100%; 23 | } 24 | 25 | div.bodywrapper { 26 | margin: 0 0 0 220px; 27 | } 28 | 29 | div.sphinxsidebar { 30 | width: 220px; 31 | font-size: 14px; 32 | line-height: 1.5; 33 | } 34 | 35 | hr { 36 | border: 1px solid #B1B4B6; 37 | } 38 | 39 | div.body { 40 | background-color: #fff; 41 | color: #3E4349; 42 | padding: 0 30px 0 30px; 43 | } 44 | 45 | div.body > .section { 46 | text-align: left; 47 | } 48 | 49 | div.footer { 50 | width: 940px; 51 | margin: 20px auto 30px auto; 52 | font-size: 14px; 53 | color: #888; 54 | text-align: right; 55 | } 56 | 57 | div.footer a { 58 | color: #888; 59 | } 60 | 61 | p.caption { 62 | font-family: inherit; 63 | font-size: inherit; 64 | } 65 | 66 | 67 | div.relations { 68 | display: none; 69 | } 70 | 71 | 72 | div.sphinxsidebar a { 73 | color: #444; 74 | text-decoration: none; 75 | border-bottom: 1px dotted #999; 76 | } 77 | 78 | div.sphinxsidebar a:hover { 79 | border-bottom: 1px solid #999; 80 | } 81 | 82 | div.sphinxsidebarwrapper { 83 | padding: 18px 10px; 84 | } 85 | 86 | div.sphinxsidebarwrapper p.logo { 87 | padding: 0; 88 | margin: -10px 0 0 0px; 89 | text-align: center; 90 | } 91 | 92 | div.sphinxsidebarwrapper h1.logo { 93 | margin-top: -10px; 94 | text-align: center; 95 | margin-bottom: 5px; 96 | text-align: left; 97 | } 98 | 99 | div.sphinxsidebarwrapper h1.logo-name { 100 | margin-top: 0px; 101 | } 102 | 103 | div.sphinxsidebarwrapper p.blurb { 104 | margin-top: 0; 105 | font-style: normal; 106 | } 107 | 108 | div.sphinxsidebar h3, 109 | div.sphinxsidebar h4 { 110 | font-family: Georgia, serif; 111 | color: #444; 112 | font-size: 24px; 113 | font-weight: normal; 114 | margin: 0 0 5px 0; 115 | padding: 0; 116 | } 117 | 118 | div.sphinxsidebar h4 { 119 | font-size: 20px; 120 | } 121 | 122 | div.sphinxsidebar h3 a { 123 | color: #444; 124 | } 125 | 126 | div.sphinxsidebar p.logo a, 127 | div.sphinxsidebar h3 a, 128 | div.sphinxsidebar p.logo a:hover, 129 | div.sphinxsidebar h3 a:hover { 130 | border: none; 131 | } 132 | 133 | div.sphinxsidebar p { 134 | color: #555; 135 | margin: 10px 0; 136 | } 137 | 138 | div.sphinxsidebar ul { 139 | margin: 10px 0; 140 | padding: 0; 141 | color: #000; 142 | } 143 | 144 | div.sphinxsidebar ul li.toctree-l1 > a { 145 | font-size: 120%; 146 | } 147 | 148 | div.sphinxsidebar ul li.toctree-l2 > a { 149 | font-size: 110%; 150 | } 151 | 152 | div.sphinxsidebar input { 153 | border: 1px solid #CCC; 154 | font-family: Georgia, serif; 155 | font-size: 1em; 156 | } 157 | 158 | div.sphinxsidebar hr { 159 | border: none; 160 | height: 1px; 161 | color: #AAA; 162 | background: #AAA; 163 | 164 | text-align: left; 165 | margin-left: 0; 166 | width: 50%; 167 | } 168 | 169 | div.sphinxsidebar .badge { 170 | border-bottom: none; 171 | } 172 | 173 | div.sphinxsidebar .badge:hover { 174 | border-bottom: none; 175 | } 176 | 177 | /* To address an issue with donation coming after search */ 178 | div.sphinxsidebar h3.donation { 179 | margin-top: 10px; 180 | } 181 | 182 | /* -- body styles ----------------------------------------------------------- */ 183 | 184 | a { 185 | color: #004B6B; 186 | text-decoration: underline; 187 | } 188 | 189 | a:hover { 190 | color: #6D4100; 191 | text-decoration: underline; 192 | } 193 | 194 | div.body h1, 195 | div.body h2, 196 | div.body h3, 197 | div.body h4, 198 | div.body h5, 199 | div.body h6 { 200 | font-family: Georgia, serif; 201 | font-weight: normal; 202 | margin: 30px 0px 10px 0px; 203 | padding: 0; 204 | } 205 | 206 | div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } 207 | div.body h2 { font-size: 180%; } 208 | div.body h3 { font-size: 150%; } 209 | div.body h4 { font-size: 130%; } 210 | div.body h5 { font-size: 100%; } 211 | div.body h6 { font-size: 100%; } 212 | 213 | a.headerlink { 214 | color: #DDD; 215 | padding: 0 4px; 216 | text-decoration: none; 217 | } 218 | 219 | a.headerlink:hover { 220 | color: #444; 221 | background: #EAEAEA; 222 | } 223 | 224 | div.body p, div.body dd, div.body li { 225 | line-height: 1.4em; 226 | } 227 | 228 | div.admonition { 229 | margin: 20px 0px; 230 | padding: 10px 30px; 231 | background-color: #EEE; 232 | border: 1px solid #CCC; 233 | } 234 | 235 | div.admonition tt.xref, div.admonition code.xref, div.admonition a tt { 236 | background-color: #FBFBFB; 237 | border-bottom: 1px solid #fafafa; 238 | } 239 | 240 | div.admonition p.admonition-title { 241 | font-family: Georgia, serif; 242 | font-weight: normal; 243 | font-size: 24px; 244 | margin: 0 0 10px 0; 245 | padding: 0; 246 | line-height: 1; 247 | } 248 | 249 | div.admonition p.last { 250 | margin-bottom: 0; 251 | } 252 | 253 | div.highlight { 254 | background-color: #fff; 255 | } 256 | 257 | dt:target, .highlight { 258 | background: #FAF3E8; 259 | } 260 | 261 | div.warning { 262 | background-color: #FCC; 263 | border: 1px solid #FAA; 264 | } 265 | 266 | div.danger { 267 | background-color: #FCC; 268 | border: 1px solid #FAA; 269 | -moz-box-shadow: 2px 2px 4px #D52C2C; 270 | -webkit-box-shadow: 2px 2px 4px #D52C2C; 271 | box-shadow: 2px 2px 4px #D52C2C; 272 | } 273 | 274 | div.error { 275 | background-color: #FCC; 276 | border: 1px solid #FAA; 277 | -moz-box-shadow: 2px 2px 4px #D52C2C; 278 | -webkit-box-shadow: 2px 2px 4px #D52C2C; 279 | box-shadow: 2px 2px 4px #D52C2C; 280 | } 281 | 282 | div.caution { 283 | background-color: #FCC; 284 | border: 1px solid #FAA; 285 | } 286 | 287 | div.attention { 288 | background-color: #FCC; 289 | border: 1px solid #FAA; 290 | } 291 | 292 | div.important { 293 | background-color: #EEE; 294 | border: 1px solid #CCC; 295 | } 296 | 297 | div.note { 298 | background-color: #EEE; 299 | border: 1px solid #CCC; 300 | } 301 | 302 | div.tip { 303 | background-color: #EEE; 304 | border: 1px solid #CCC; 305 | } 306 | 307 | div.hint { 308 | background-color: #EEE; 309 | border: 1px solid #CCC; 310 | } 311 | 312 | div.seealso { 313 | background-color: #EEE; 314 | border: 1px solid #CCC; 315 | } 316 | 317 | div.topic { 318 | background-color: #EEE; 319 | } 320 | 321 | p.admonition-title { 322 | display: inline; 323 | } 324 | 325 | p.admonition-title:after { 326 | content: ":"; 327 | } 328 | 329 | pre, tt, code { 330 | font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 331 | font-size: 0.9em; 332 | } 333 | 334 | .hll { 335 | background-color: #FFC; 336 | margin: 0 -12px; 337 | padding: 0 12px; 338 | display: block; 339 | } 340 | 341 | img.screenshot { 342 | } 343 | 344 | tt.descname, tt.descclassname, code.descname, code.descclassname { 345 | font-size: 0.95em; 346 | } 347 | 348 | tt.descname, code.descname { 349 | padding-right: 0.08em; 350 | } 351 | 352 | img.screenshot { 353 | -moz-box-shadow: 2px 2px 4px #EEE; 354 | -webkit-box-shadow: 2px 2px 4px #EEE; 355 | box-shadow: 2px 2px 4px #EEE; 356 | } 357 | 358 | table.docutils { 359 | border: 1px solid #888; 360 | -moz-box-shadow: 2px 2px 4px #EEE; 361 | -webkit-box-shadow: 2px 2px 4px #EEE; 362 | box-shadow: 2px 2px 4px #EEE; 363 | } 364 | 365 | table.docutils td, table.docutils th { 366 | border: 1px solid #888; 367 | padding: 0.25em 0.7em; 368 | } 369 | 370 | table.field-list, table.footnote { 371 | border: none; 372 | -moz-box-shadow: none; 373 | -webkit-box-shadow: none; 374 | box-shadow: none; 375 | } 376 | 377 | table.footnote { 378 | margin: 15px 0; 379 | width: 100%; 380 | border: 1px solid #EEE; 381 | background: #FDFDFD; 382 | font-size: 0.9em; 383 | } 384 | 385 | table.footnote + table.footnote { 386 | margin-top: -15px; 387 | border-top: none; 388 | } 389 | 390 | table.field-list th { 391 | padding: 0 0.8em 0 0; 392 | } 393 | 394 | table.field-list td { 395 | padding: 0; 396 | } 397 | 398 | table.field-list p { 399 | margin-bottom: 0.8em; 400 | } 401 | 402 | /* Cloned from 403 | * https://github.com/sphinx-doc/sphinx/commit/ef60dbfce09286b20b7385333d63a60321784e68 404 | */ 405 | .field-name { 406 | -moz-hyphens: manual; 407 | -ms-hyphens: manual; 408 | -webkit-hyphens: manual; 409 | hyphens: manual; 410 | } 411 | 412 | table.footnote td.label { 413 | width: .1px; 414 | padding: 0.3em 0 0.3em 0.5em; 415 | } 416 | 417 | table.footnote td { 418 | padding: 0.3em 0.5em; 419 | } 420 | 421 | dl { 422 | margin: 0; 423 | padding: 0; 424 | } 425 | 426 | dl dd { 427 | margin-left: 30px; 428 | } 429 | 430 | blockquote { 431 | margin: 0 0 0 30px; 432 | padding: 0; 433 | } 434 | 435 | ul, ol { 436 | /* Matches the 30px from the narrow-screen "li > ul" selector below */ 437 | margin: 10px 0 10px 30px; 438 | padding: 0; 439 | } 440 | 441 | pre { 442 | background: #EEE; 443 | padding: 7px 30px; 444 | margin: 15px 0px; 445 | line-height: 1.3em; 446 | } 447 | 448 | div.viewcode-block:target { 449 | background: #ffd; 450 | } 451 | 452 | dl pre, blockquote pre, li pre { 453 | margin-left: 0; 454 | padding-left: 30px; 455 | } 456 | 457 | tt, code { 458 | background-color: #ecf0f3; 459 | color: #222; 460 | /* padding: 1px 2px; */ 461 | } 462 | 463 | tt.xref, code.xref, a tt { 464 | background-color: #FBFBFB; 465 | border-bottom: 1px solid #fff; 466 | } 467 | 468 | a.reference { 469 | text-decoration: none; 470 | border-bottom: 1px dotted #004B6B; 471 | } 472 | 473 | /* Don't put an underline on images */ 474 | a.image-reference, a.image-reference:hover { 475 | border-bottom: none; 476 | } 477 | 478 | a.reference:hover { 479 | border-bottom: 1px solid #6D4100; 480 | } 481 | 482 | a.footnote-reference { 483 | text-decoration: none; 484 | font-size: 0.7em; 485 | vertical-align: top; 486 | border-bottom: 1px dotted #004B6B; 487 | } 488 | 489 | a.footnote-reference:hover { 490 | border-bottom: 1px solid #6D4100; 491 | } 492 | 493 | a:hover tt, a:hover code { 494 | background: #EEE; 495 | } 496 | 497 | 498 | @media screen and (max-width: 870px) { 499 | 500 | div.sphinxsidebar { 501 | display: none; 502 | } 503 | 504 | div.document { 505 | width: 100%; 506 | 507 | } 508 | 509 | div.documentwrapper { 510 | margin-left: 0; 511 | margin-top: 0; 512 | margin-right: 0; 513 | margin-bottom: 0; 514 | } 515 | 516 | div.bodywrapper { 517 | margin-top: 0; 518 | margin-right: 0; 519 | margin-bottom: 0; 520 | margin-left: 0; 521 | } 522 | 523 | ul { 524 | margin-left: 0; 525 | } 526 | 527 | li > ul { 528 | /* Matches the 30px from the "ul, ol" selector above */ 529 | margin-left: 30px; 530 | } 531 | 532 | .document { 533 | width: auto; 534 | } 535 | 536 | .footer { 537 | width: auto; 538 | } 539 | 540 | .bodywrapper { 541 | margin: 0; 542 | } 543 | 544 | .footer { 545 | width: auto; 546 | } 547 | 548 | .github { 549 | display: none; 550 | } 551 | 552 | 553 | 554 | } 555 | 556 | 557 | 558 | @media screen and (max-width: 875px) { 559 | 560 | body { 561 | margin: 0; 562 | padding: 20px 30px; 563 | } 564 | 565 | div.documentwrapper { 566 | float: none; 567 | background: #fff; 568 | } 569 | 570 | div.sphinxsidebar { 571 | display: block; 572 | float: none; 573 | width: 102.5%; 574 | margin: 50px -30px -20px -30px; 575 | padding: 10px 20px; 576 | background: #333; 577 | color: #FFF; 578 | } 579 | 580 | div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, 581 | div.sphinxsidebar h3 a { 582 | color: #fff; 583 | } 584 | 585 | div.sphinxsidebar a { 586 | color: #AAA; 587 | } 588 | 589 | div.sphinxsidebar p.logo { 590 | display: none; 591 | } 592 | 593 | div.document { 594 | width: 100%; 595 | margin: 0; 596 | } 597 | 598 | div.footer { 599 | display: none; 600 | } 601 | 602 | div.bodywrapper { 603 | margin: 0; 604 | } 605 | 606 | div.body { 607 | min-height: 0; 608 | padding: 0; 609 | } 610 | 611 | .rtd_doc_footer { 612 | display: none; 613 | } 614 | 615 | .document { 616 | width: auto; 617 | } 618 | 619 | .footer { 620 | width: auto; 621 | } 622 | 623 | .footer { 624 | width: auto; 625 | } 626 | 627 | .github { 628 | display: none; 629 | } 630 | } 631 | 632 | 633 | /* misc. */ 634 | 635 | .revsys-inline { 636 | display: none!important; 637 | } 638 | 639 | /* Make nested-list/multi-paragraph items look better in Releases changelog 640 | * pages. Without this, docutils' magical list fuckery causes inconsistent 641 | * formatting between different release sub-lists. 642 | */ 643 | div#changelog > div.section > ul > li > p:only-child { 644 | margin-bottom: 0; 645 | } 646 | 647 | /* Hide fugly table cell borders in ..bibliography:: directive output */ 648 | table.docutils.citation, table.docutils.citation td, table.docutils.citation th { 649 | border: none; 650 | /* Below needed in some edge cases; if not applied, bottom shadows appear */ 651 | -moz-box-shadow: none; 652 | -webkit-box-shadow: none; 653 | box-shadow: none; 654 | } 655 | 656 | 657 | /* relbar */ 658 | 659 | .related { 660 | line-height: 30px; 661 | width: 100%; 662 | font-size: 0.9rem; 663 | } 664 | 665 | .related.top { 666 | border-bottom: 1px solid #EEE; 667 | margin-bottom: 20px; 668 | } 669 | 670 | .related.bottom { 671 | border-top: 1px solid #EEE; 672 | } 673 | 674 | .related ul { 675 | padding: 0; 676 | margin: 0; 677 | list-style: none; 678 | } 679 | 680 | .related li { 681 | display: inline; 682 | } 683 | 684 | nav#rellinks { 685 | float: right; 686 | } 687 | 688 | nav#rellinks li+li:before { 689 | content: "|"; 690 | } 691 | 692 | nav#breadcrumbs li+li:before { 693 | content: "\00BB"; 694 | } 695 | 696 | /* Hide certain items when printing */ 697 | @media print { 698 | div.related { 699 | display: none; 700 | } 701 | } -------------------------------------------------------------------------------- /docs/_build/html/_static/basic.css: -------------------------------------------------------------------------------- 1 | /* 2 | * basic.css 3 | * ~~~~~~~~~ 4 | * 5 | * Sphinx stylesheet -- basic theme. 6 | * 7 | * :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. 8 | * :license: BSD, see LICENSE for details. 9 | * 10 | */ 11 | 12 | /* -- main layout ----------------------------------------------------------- */ 13 | 14 | div.clearer { 15 | clear: both; 16 | } 17 | 18 | /* -- relbar ---------------------------------------------------------------- */ 19 | 20 | div.related { 21 | width: 100%; 22 | font-size: 90%; 23 | } 24 | 25 | div.related h3 { 26 | display: none; 27 | } 28 | 29 | div.related ul { 30 | margin: 0; 31 | padding: 0 0 0 10px; 32 | list-style: none; 33 | } 34 | 35 | div.related li { 36 | display: inline; 37 | } 38 | 39 | div.related li.right { 40 | float: right; 41 | margin-right: 5px; 42 | } 43 | 44 | /* -- sidebar --------------------------------------------------------------- */ 45 | 46 | div.sphinxsidebarwrapper { 47 | padding: 10px 5px 0 10px; 48 | } 49 | 50 | div.sphinxsidebar { 51 | float: left; 52 | width: 230px; 53 | margin-left: -100%; 54 | font-size: 90%; 55 | word-wrap: break-word; 56 | overflow-wrap : break-word; 57 | } 58 | 59 | div.sphinxsidebar ul { 60 | list-style: none; 61 | } 62 | 63 | div.sphinxsidebar ul ul, 64 | div.sphinxsidebar ul.want-points { 65 | margin-left: 20px; 66 | list-style: square; 67 | } 68 | 69 | div.sphinxsidebar ul ul { 70 | margin-top: 0; 71 | margin-bottom: 0; 72 | } 73 | 74 | div.sphinxsidebar form { 75 | margin-top: 10px; 76 | } 77 | 78 | div.sphinxsidebar input { 79 | border: 1px solid #98dbcc; 80 | font-family: sans-serif; 81 | font-size: 1em; 82 | } 83 | 84 | div.sphinxsidebar #searchbox form.search { 85 | overflow: hidden; 86 | } 87 | 88 | div.sphinxsidebar #searchbox input[type="text"] { 89 | float: left; 90 | width: 80%; 91 | padding: 0.25em; 92 | box-sizing: border-box; 93 | } 94 | 95 | div.sphinxsidebar #searchbox input[type="submit"] { 96 | float: left; 97 | width: 20%; 98 | border-left: none; 99 | padding: 0.25em; 100 | box-sizing: border-box; 101 | } 102 | 103 | 104 | img { 105 | border: 0; 106 | max-width: 100%; 107 | } 108 | 109 | /* -- search page ----------------------------------------------------------- */ 110 | 111 | ul.search { 112 | margin: 10px 0 0 20px; 113 | padding: 0; 114 | } 115 | 116 | ul.search li { 117 | padding: 5px 0 5px 20px; 118 | background-image: url(file.png); 119 | background-repeat: no-repeat; 120 | background-position: 0 7px; 121 | } 122 | 123 | ul.search li a { 124 | font-weight: bold; 125 | } 126 | 127 | ul.search li div.context { 128 | color: #888; 129 | margin: 2px 0 0 30px; 130 | text-align: left; 131 | } 132 | 133 | ul.keywordmatches li.goodmatch a { 134 | font-weight: bold; 135 | } 136 | 137 | /* -- index page ------------------------------------------------------------ */ 138 | 139 | table.contentstable { 140 | width: 90%; 141 | margin-left: auto; 142 | margin-right: auto; 143 | } 144 | 145 | table.contentstable p.biglink { 146 | line-height: 150%; 147 | } 148 | 149 | a.biglink { 150 | font-size: 1.3em; 151 | } 152 | 153 | span.linkdescr { 154 | font-style: italic; 155 | padding-top: 5px; 156 | font-size: 90%; 157 | } 158 | 159 | /* -- general index --------------------------------------------------------- */ 160 | 161 | table.indextable { 162 | width: 100%; 163 | } 164 | 165 | table.indextable td { 166 | text-align: left; 167 | vertical-align: top; 168 | } 169 | 170 | table.indextable ul { 171 | margin-top: 0; 172 | margin-bottom: 0; 173 | list-style-type: none; 174 | } 175 | 176 | table.indextable > tbody > tr > td > ul { 177 | padding-left: 0em; 178 | } 179 | 180 | table.indextable tr.pcap { 181 | height: 10px; 182 | } 183 | 184 | table.indextable tr.cap { 185 | margin-top: 10px; 186 | background-color: #f2f2f2; 187 | } 188 | 189 | img.toggler { 190 | margin-right: 3px; 191 | margin-top: 3px; 192 | cursor: pointer; 193 | } 194 | 195 | div.modindex-jumpbox { 196 | border-top: 1px solid #ddd; 197 | border-bottom: 1px solid #ddd; 198 | margin: 1em 0 1em 0; 199 | padding: 0.4em; 200 | } 201 | 202 | div.genindex-jumpbox { 203 | border-top: 1px solid #ddd; 204 | border-bottom: 1px solid #ddd; 205 | margin: 1em 0 1em 0; 206 | padding: 0.4em; 207 | } 208 | 209 | /* -- domain module index --------------------------------------------------- */ 210 | 211 | table.modindextable td { 212 | padding: 2px; 213 | border-collapse: collapse; 214 | } 215 | 216 | /* -- general body styles --------------------------------------------------- */ 217 | 218 | div.body { 219 | min-width: 450px; 220 | max-width: 800px; 221 | } 222 | 223 | div.body p, div.body dd, div.body li, div.body blockquote { 224 | -moz-hyphens: auto; 225 | -ms-hyphens: auto; 226 | -webkit-hyphens: auto; 227 | hyphens: auto; 228 | } 229 | 230 | a.headerlink { 231 | visibility: hidden; 232 | } 233 | 234 | a.brackets:before, 235 | span.brackets > a:before{ 236 | content: "["; 237 | } 238 | 239 | a.brackets:after, 240 | span.brackets > a:after { 241 | content: "]"; 242 | } 243 | 244 | h1:hover > a.headerlink, 245 | h2:hover > a.headerlink, 246 | h3:hover > a.headerlink, 247 | h4:hover > a.headerlink, 248 | h5:hover > a.headerlink, 249 | h6:hover > a.headerlink, 250 | dt:hover > a.headerlink, 251 | caption:hover > a.headerlink, 252 | p.caption:hover > a.headerlink, 253 | div.code-block-caption:hover > a.headerlink { 254 | visibility: visible; 255 | } 256 | 257 | div.body p.caption { 258 | text-align: inherit; 259 | } 260 | 261 | div.body td { 262 | text-align: left; 263 | } 264 | 265 | .first { 266 | margin-top: 0 !important; 267 | } 268 | 269 | p.rubric { 270 | margin-top: 30px; 271 | font-weight: bold; 272 | } 273 | 274 | img.align-left, .figure.align-left, object.align-left { 275 | clear: left; 276 | float: left; 277 | margin-right: 1em; 278 | } 279 | 280 | img.align-right, .figure.align-right, object.align-right { 281 | clear: right; 282 | float: right; 283 | margin-left: 1em; 284 | } 285 | 286 | img.align-center, .figure.align-center, object.align-center { 287 | display: block; 288 | margin-left: auto; 289 | margin-right: auto; 290 | } 291 | 292 | img.align-default, .figure.align-default { 293 | display: block; 294 | margin-left: auto; 295 | margin-right: auto; 296 | } 297 | 298 | .align-left { 299 | text-align: left; 300 | } 301 | 302 | .align-center { 303 | text-align: center; 304 | } 305 | 306 | .align-default { 307 | text-align: center; 308 | } 309 | 310 | .align-right { 311 | text-align: right; 312 | } 313 | 314 | /* -- sidebars -------------------------------------------------------------- */ 315 | 316 | div.sidebar { 317 | margin: 0 0 0.5em 1em; 318 | border: 1px solid #ddb; 319 | padding: 7px 7px 0 7px; 320 | background-color: #ffe; 321 | width: 40%; 322 | float: right; 323 | } 324 | 325 | p.sidebar-title { 326 | font-weight: bold; 327 | } 328 | 329 | /* -- topics ---------------------------------------------------------------- */ 330 | 331 | div.topic { 332 | border: 1px solid #ccc; 333 | padding: 7px 7px 0 7px; 334 | margin: 10px 0 10px 0; 335 | } 336 | 337 | p.topic-title { 338 | font-size: 1.1em; 339 | font-weight: bold; 340 | margin-top: 10px; 341 | } 342 | 343 | /* -- admonitions ----------------------------------------------------------- */ 344 | 345 | div.admonition { 346 | margin-top: 10px; 347 | margin-bottom: 10px; 348 | padding: 7px; 349 | } 350 | 351 | div.admonition dt { 352 | font-weight: bold; 353 | } 354 | 355 | div.admonition dl { 356 | margin-bottom: 0; 357 | } 358 | 359 | p.admonition-title { 360 | margin: 0px 10px 5px 0px; 361 | font-weight: bold; 362 | } 363 | 364 | div.body p.centered { 365 | text-align: center; 366 | margin-top: 25px; 367 | } 368 | 369 | /* -- tables ---------------------------------------------------------------- */ 370 | 371 | table.docutils { 372 | border: 0; 373 | border-collapse: collapse; 374 | } 375 | 376 | table.align-center { 377 | margin-left: auto; 378 | margin-right: auto; 379 | } 380 | 381 | table.align-default { 382 | margin-left: auto; 383 | margin-right: auto; 384 | } 385 | 386 | table caption span.caption-number { 387 | font-style: italic; 388 | } 389 | 390 | table caption span.caption-text { 391 | } 392 | 393 | table.docutils td, table.docutils th { 394 | padding: 1px 8px 1px 5px; 395 | border-top: 0; 396 | border-left: 0; 397 | border-right: 0; 398 | border-bottom: 1px solid #aaa; 399 | } 400 | 401 | table.footnote td, table.footnote th { 402 | border: 0 !important; 403 | } 404 | 405 | th { 406 | text-align: left; 407 | padding-right: 5px; 408 | } 409 | 410 | table.citation { 411 | border-left: solid 1px gray; 412 | margin-left: 1px; 413 | } 414 | 415 | table.citation td { 416 | border-bottom: none; 417 | } 418 | 419 | th > p:first-child, 420 | td > p:first-child { 421 | margin-top: 0px; 422 | } 423 | 424 | th > p:last-child, 425 | td > p:last-child { 426 | margin-bottom: 0px; 427 | } 428 | 429 | /* -- figures --------------------------------------------------------------- */ 430 | 431 | div.figure { 432 | margin: 0.5em; 433 | padding: 0.5em; 434 | } 435 | 436 | div.figure p.caption { 437 | padding: 0.3em; 438 | } 439 | 440 | div.figure p.caption span.caption-number { 441 | font-style: italic; 442 | } 443 | 444 | div.figure p.caption span.caption-text { 445 | } 446 | 447 | /* -- field list styles ----------------------------------------------------- */ 448 | 449 | table.field-list td, table.field-list th { 450 | border: 0 !important; 451 | } 452 | 453 | .field-list ul { 454 | margin: 0; 455 | padding-left: 1em; 456 | } 457 | 458 | .field-list p { 459 | margin: 0; 460 | } 461 | 462 | .field-name { 463 | -moz-hyphens: manual; 464 | -ms-hyphens: manual; 465 | -webkit-hyphens: manual; 466 | hyphens: manual; 467 | } 468 | 469 | /* -- hlist styles ---------------------------------------------------------- */ 470 | 471 | table.hlist td { 472 | vertical-align: top; 473 | } 474 | 475 | 476 | /* -- other body styles ----------------------------------------------------- */ 477 | 478 | ol.arabic { 479 | list-style: decimal; 480 | } 481 | 482 | ol.loweralpha { 483 | list-style: lower-alpha; 484 | } 485 | 486 | ol.upperalpha { 487 | list-style: upper-alpha; 488 | } 489 | 490 | ol.lowerroman { 491 | list-style: lower-roman; 492 | } 493 | 494 | ol.upperroman { 495 | list-style: upper-roman; 496 | } 497 | 498 | li > p:first-child { 499 | margin-top: 0px; 500 | } 501 | 502 | li > p:last-child { 503 | margin-bottom: 0px; 504 | } 505 | 506 | dl.footnote > dt, 507 | dl.citation > dt { 508 | float: left; 509 | } 510 | 511 | dl.footnote > dd, 512 | dl.citation > dd { 513 | margin-bottom: 0em; 514 | } 515 | 516 | dl.footnote > dd:after, 517 | dl.citation > dd:after { 518 | content: ""; 519 | clear: both; 520 | } 521 | 522 | dl.field-list { 523 | display: grid; 524 | grid-template-columns: fit-content(30%) auto; 525 | } 526 | 527 | dl.field-list > dt { 528 | font-weight: bold; 529 | word-break: break-word; 530 | padding-left: 0.5em; 531 | padding-right: 5px; 532 | } 533 | 534 | dl.field-list > dt:after { 535 | content: ":"; 536 | } 537 | 538 | dl.field-list > dd { 539 | padding-left: 0.5em; 540 | margin-top: 0em; 541 | margin-left: 0em; 542 | margin-bottom: 0em; 543 | } 544 | 545 | dl { 546 | margin-bottom: 15px; 547 | } 548 | 549 | dd > p:first-child { 550 | margin-top: 0px; 551 | } 552 | 553 | dd ul, dd table { 554 | margin-bottom: 10px; 555 | } 556 | 557 | dd { 558 | margin-top: 3px; 559 | margin-bottom: 10px; 560 | margin-left: 30px; 561 | } 562 | 563 | dt:target, span.highlighted { 564 | background-color: #fbe54e; 565 | } 566 | 567 | rect.highlighted { 568 | fill: #fbe54e; 569 | } 570 | 571 | dl.glossary dt { 572 | font-weight: bold; 573 | font-size: 1.1em; 574 | } 575 | 576 | .optional { 577 | font-size: 1.3em; 578 | } 579 | 580 | .sig-paren { 581 | font-size: larger; 582 | } 583 | 584 | .versionmodified { 585 | font-style: italic; 586 | } 587 | 588 | .system-message { 589 | background-color: #fda; 590 | padding: 5px; 591 | border: 3px solid red; 592 | } 593 | 594 | .footnote:target { 595 | background-color: #ffa; 596 | } 597 | 598 | .line-block { 599 | display: block; 600 | margin-top: 1em; 601 | margin-bottom: 1em; 602 | } 603 | 604 | .line-block .line-block { 605 | margin-top: 0; 606 | margin-bottom: 0; 607 | margin-left: 1.5em; 608 | } 609 | 610 | .guilabel, .menuselection { 611 | font-family: sans-serif; 612 | } 613 | 614 | .accelerator { 615 | text-decoration: underline; 616 | } 617 | 618 | .classifier { 619 | font-style: oblique; 620 | } 621 | 622 | .classifier:before { 623 | font-style: normal; 624 | margin: 0.5em; 625 | content: ":"; 626 | } 627 | 628 | abbr, acronym { 629 | border-bottom: dotted 1px; 630 | cursor: help; 631 | } 632 | 633 | /* -- code displays --------------------------------------------------------- */ 634 | 635 | pre { 636 | overflow: auto; 637 | overflow-y: hidden; /* fixes display issues on Chrome browsers */ 638 | } 639 | 640 | span.pre { 641 | -moz-hyphens: none; 642 | -ms-hyphens: none; 643 | -webkit-hyphens: none; 644 | hyphens: none; 645 | } 646 | 647 | td.linenos pre { 648 | padding: 5px 0px; 649 | border: 0; 650 | background-color: transparent; 651 | color: #aaa; 652 | } 653 | 654 | table.highlighttable { 655 | margin-left: 0.5em; 656 | } 657 | 658 | table.highlighttable td { 659 | padding: 0 0.5em 0 0.5em; 660 | } 661 | 662 | div.code-block-caption { 663 | padding: 2px 5px; 664 | font-size: small; 665 | } 666 | 667 | div.code-block-caption code { 668 | background-color: transparent; 669 | } 670 | 671 | div.code-block-caption + div > div.highlight > pre { 672 | margin-top: 0; 673 | } 674 | 675 | div.doctest > div.highlight span.gp { /* gp: Generic.Prompt */ 676 | user-select: none; 677 | } 678 | 679 | div.code-block-caption span.caption-number { 680 | padding: 0.1em 0.3em; 681 | font-style: italic; 682 | } 683 | 684 | div.code-block-caption span.caption-text { 685 | } 686 | 687 | div.literal-block-wrapper { 688 | padding: 1em 1em 0; 689 | } 690 | 691 | div.literal-block-wrapper div.highlight { 692 | margin: 0; 693 | } 694 | 695 | code.descname { 696 | background-color: transparent; 697 | font-weight: bold; 698 | font-size: 1.2em; 699 | } 700 | 701 | code.descclassname { 702 | background-color: transparent; 703 | } 704 | 705 | code.xref, a code { 706 | background-color: transparent; 707 | font-weight: bold; 708 | } 709 | 710 | h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { 711 | background-color: transparent; 712 | } 713 | 714 | .viewcode-link { 715 | float: right; 716 | } 717 | 718 | .viewcode-back { 719 | float: right; 720 | font-family: sans-serif; 721 | } 722 | 723 | div.viewcode-block:target { 724 | margin: -1px -10px; 725 | padding: 0 10px; 726 | } 727 | 728 | /* -- math display ---------------------------------------------------------- */ 729 | 730 | img.math { 731 | vertical-align: middle; 732 | } 733 | 734 | div.body div.math p { 735 | text-align: center; 736 | } 737 | 738 | span.eqno { 739 | float: right; 740 | } 741 | 742 | span.eqno a.headerlink { 743 | position: relative; 744 | left: 0px; 745 | z-index: 1; 746 | } 747 | 748 | div.math:hover a.headerlink { 749 | visibility: visible; 750 | } 751 | 752 | /* -- printout stylesheet --------------------------------------------------- */ 753 | 754 | @media print { 755 | div.document, 756 | div.documentwrapper, 757 | div.bodywrapper { 758 | margin: 0 !important; 759 | width: 100%; 760 | } 761 | 762 | div.sphinxsidebar, 763 | div.related, 764 | div.footer, 765 | #top-link { 766 | display: none; 767 | } 768 | } --------------------------------------------------------------------------------