Compare commits
No commits in common. "master" and "v3" have entirely different histories.
|
@ -1,8 +0,0 @@
|
||||||
version = 1
|
|
||||||
|
|
||||||
[[analyzers]]
|
|
||||||
name = "python"
|
|
||||||
enabled = true
|
|
||||||
|
|
||||||
[analyzers.meta]
|
|
||||||
runtime_version = "3.x.x"
|
|
|
@ -1,7 +0,0 @@
|
||||||
# PostgreSQL
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
POSTGRES_HOST=postgres
|
|
||||||
POSTGRES_PORT=5432
|
|
||||||
POSTGRES_DB=tuxbot_bot
|
|
||||||
POSTGRES_USER=debug
|
|
||||||
POSTGRES_PASSWORD=debug
|
|
|
@ -1,4 +0,0 @@
|
||||||
# General
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
USE_DOCKER=yes
|
|
||||||
IPYTHONDIR=/app/.ipython
|
|
13
.gitignore
vendored
13
.gitignore
vendored
|
@ -33,20 +33,7 @@ __pycache__/
|
||||||
__pypackages__/
|
__pypackages__/
|
||||||
|
|
||||||
venv
|
venv
|
||||||
venv3.8
|
|
||||||
venv3.9
|
|
||||||
venv3.11
|
|
||||||
dist
|
dist
|
||||||
build
|
build
|
||||||
*.egg
|
*.egg
|
||||||
*.egg-info
|
*.egg-info
|
||||||
|
|
||||||
|
|
||||||
.ipython/
|
|
||||||
.env
|
|
||||||
.envs/*
|
|
||||||
!.envs/.local/
|
|
||||||
|
|
||||||
|
|
||||||
data/settings/
|
|
||||||
dump.rdb
|
|
|
@ -1,70 +1,44 @@
|
||||||
<component name="ProjectDictionaryState">
|
<component name="ProjectDictionaryState">
|
||||||
<dictionary name="romain">
|
<dictionary name="romain">
|
||||||
<words>
|
<words>
|
||||||
<w>aaaa</w>
|
|
||||||
<w>ajout</w>
|
|
||||||
<w>anglais</w>
|
<w>anglais</w>
|
||||||
<w>anonyme</w>
|
|
||||||
<w>appdirs</w>
|
<w>appdirs</w>
|
||||||
<w>apres</w>
|
<w>apres</w>
|
||||||
<w>asctime</w>
|
<w>asctime</w>
|
||||||
<w>commandstats</w>
|
<w>commandstats</w>
|
||||||
<w>crimeflare</w>
|
|
||||||
<w>ctype</w>
|
<w>ctype</w>
|
||||||
<w>debian</w>
|
<w>debian</w>
|
||||||
<w>dnskey</w>
|
|
||||||
<w>découverte</w>
|
<w>découverte</w>
|
||||||
<w>ffff</w>
|
|
||||||
<w>fonction</w>
|
<w>fonction</w>
|
||||||
<w>francais</w>
|
<w>francais</w>
|
||||||
<w>français</w>
|
<w>français</w>
|
||||||
<w>gitea</w>
|
|
||||||
<w>gnous</w>
|
<w>gnous</w>
|
||||||
<w>ipinfo</w>
|
<w>ipinfo</w>
|
||||||
<w>iplocalise</w>
|
<w>iplocalise</w>
|
||||||
<w>ipwhois</w>
|
|
||||||
<w>jishaku</w>
|
<w>jishaku</w>
|
||||||
<w>langue</w>
|
<w>langue</w>
|
||||||
<w>latlon</w>
|
|
||||||
<w>levelname</w>
|
<w>levelname</w>
|
||||||
<w>liste</w>
|
<w>liste</w>
|
||||||
<w>localiseip</w>
|
<w>localiseip</w>
|
||||||
<w>lundi</w>
|
|
||||||
<w>octobre</w>
|
<w>octobre</w>
|
||||||
<w>outout</w>
|
|
||||||
<w>outoutxyz</w>
|
|
||||||
<w>outouxyz</w>
|
|
||||||
<w>pacman</w>
|
<w>pacman</w>
|
||||||
<w>peeringdb</w>
|
|
||||||
<w>perso</w>
|
|
||||||
<w>postgre</w>
|
|
||||||
<w>postgresql</w>
|
<w>postgresql</w>
|
||||||
<w>pred</w>
|
<w>pred</w>
|
||||||
<w>pydig</w>
|
|
||||||
<w>pylint</w>
|
<w>pylint</w>
|
||||||
<w>regle</w>
|
|
||||||
<w>regles</w>
|
|
||||||
<w>releaselevel</w>
|
<w>releaselevel</w>
|
||||||
<w>rprint</w>
|
<w>rprint</w>
|
||||||
<w>skipcq</w>
|
|
||||||
<w>socketstats</w>
|
<w>socketstats</w>
|
||||||
<w>soit</w>
|
<w>soit</w>
|
||||||
<w>sondage</w>
|
|
||||||
<w>sondages</w>
|
|
||||||
<w>splt</w>
|
<w>splt</w>
|
||||||
<w>suivante</w>
|
<w>suivante</w>
|
||||||
<w>systemd</w>
|
<w>systemd</w>
|
||||||
<w>tablename</w>
|
|
||||||
<w>tempmute</w>
|
|
||||||
<w>tldr</w>
|
<w>tldr</w>
|
||||||
<w>tutux</w>
|
<w>tutux</w>
|
||||||
<w>tuxbot</w>
|
<w>tuxbot</w>
|
||||||
<w>tuxbot's</w>
|
<w>tuxbot's</w>
|
||||||
<w>tuxvenv</w>
|
<w>tuxvenv</w>
|
||||||
<w>venv</w>
|
<w>venv</w>
|
||||||
<w>webhook</w>
|
|
||||||
<w>webhooks</w>
|
<w>webhooks</w>
|
||||||
<w>youtrack</w>
|
|
||||||
<w>écrite</w>
|
<w>écrite</w>
|
||||||
</words>
|
</words>
|
||||||
</dictionary>
|
</dictionary>
|
||||||
|
|
|
@ -3,5 +3,5 @@
|
||||||
<component name="JavaScriptSettings">
|
<component name="JavaScriptSettings">
|
||||||
<option name="languageLevel" value="ES6" />
|
<option name="languageLevel" value="ES6" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (tuxbot_bot)" project-jdk-type="Python SDK" />
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8 (tuxbot-bot-rewrite)" project-jdk-type="Python SDK" />
|
||||||
</project>
|
</project>
|
|
@ -2,7 +2,7 @@
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ProjectModuleManager">
|
<component name="ProjectModuleManager">
|
||||||
<modules>
|
<modules>
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/tuxbot_bot.iml" filepath="$PROJECT_DIR$/.idea/tuxbot_bot.iml" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/tuxbot-bot-rewrite.iml" filepath="$PROJECT_DIR$/.idea/tuxbot-bot-rewrite.iml" />
|
||||||
</modules>
|
</modules>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
|
@ -5,13 +5,8 @@
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build" />
|
<excludeFolder url="file://$MODULE_DIR$/build" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/dist" />
|
<excludeFolder url="file://$MODULE_DIR$/dist" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/venv" />
|
<excludeFolder url="file://$MODULE_DIR$/venv" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/data" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/.mypy_cache" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/venv3.8" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/venv3.9" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/venv3.11" />
|
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="jdk" jdkName="Python 3.10 (tuxbot_bot)" jdkType="Python SDK" />
|
<orderEntry type="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
</component>
|
</component>
|
||||||
<component name="PyDocumentationSettings">
|
<component name="PyDocumentationSettings">
|
|
@ -1,14 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="WebResourcesPaths">
|
|
||||||
<contentEntries>
|
|
||||||
<entry url="file://$PROJECT_DIR$">
|
|
||||||
<entryData>
|
|
||||||
<resourceRoots>
|
|
||||||
<path value="file://$PROJECT_DIR$" />
|
|
||||||
</resourceRoots>
|
|
||||||
</entryData>
|
|
||||||
</entry>
|
|
||||||
</contentEntries>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
|
@ -4,19 +4,12 @@ good-names=
|
||||||
f, # (file) as f
|
f, # (file) as f
|
||||||
k, # for k, v in
|
k, # for k, v in
|
||||||
v, # for k, v in
|
v, # for k, v in
|
||||||
dt, # datetime
|
|
||||||
|
|
||||||
[MASTER]
|
[MASTER]
|
||||||
disable=
|
disable=
|
||||||
C0103, # invalid-name
|
|
||||||
C0114, # missing-module-docstring
|
C0114, # missing-module-docstring
|
||||||
C0115, # missing-class-docstring
|
C0115, # missing-class-docstring
|
||||||
C0116, # missing-function-docstring
|
C0116, # missing-function-docstring
|
||||||
C0415, # import-outside-toplevel
|
|
||||||
W0703, # broad-except
|
W0703, # broad-except
|
||||||
W0707, # raise-missing-from
|
|
||||||
R0801, # duplicate-code
|
|
||||||
R0901, # too-many-ancestors
|
|
||||||
R0902, # too-many-instance-attributes
|
R0902, # too-many-instance-attributes
|
||||||
R0903, # too-few-public-methods
|
R0903, # too-few-public-methods
|
||||||
E1136, # unsubscriptable-object (false positive with python 3.9)
|
|
||||||
|
|
71
Makefile
71
Makefile
|
@ -1,84 +1,31 @@
|
||||||
ifeq ($(ISPROD), 1)
|
PYTHON = python
|
||||||
DOCKER_LOCAL := docker-compose -f production.yml
|
VENV = venv
|
||||||
else
|
|
||||||
DOCKER_LOCAL := docker-compose -f local.yml
|
|
||||||
endif
|
|
||||||
|
|
||||||
INSTANCE := preprod
|
XGETTEXT_FLAGS = --no-wrap --language='python' --keyword=_ --from-code='UTF-8' --msgid-bugs-address='rick@gnous.eu' --width=79 --package-name='Tuxbot-bot'
|
||||||
|
|
||||||
DOCKER_TUXBOT := $(DOCKER_LOCAL) run --rm tuxbot
|
|
||||||
VIRTUAL_ENV := venv
|
|
||||||
PYTHON_PATH := $(VIRTUAL_ENV)/bin/python
|
|
||||||
|
|
||||||
XGETTEXT_FLAGS := --no-wrap --language='python' --keyword=_ --from-code='UTF-8' --msgid-bugs-address='rick@gnous.eu' --width=79 --package-name='Tuxbot-bot'
|
|
||||||
|
|
||||||
# Init
|
# Init
|
||||||
.PHONY: main
|
|
||||||
main:
|
main:
|
||||||
$(VIRTUAL_ENV)/bin/pip install -U pip setuptools
|
$(PYTHON) -m venv --clear $(VENV)
|
||||||
|
$(VENV)/bin/pip install -U pip setuptools
|
||||||
.PHONY: install
|
|
||||||
install:
|
install:
|
||||||
$(VIRTUAL_ENV)/bin/pip install .
|
$(VENV)/bin/pip install .
|
||||||
|
|
||||||
.PHONY: install-dev
|
|
||||||
install-dev:
|
|
||||||
$(VIRTUAL_ENV)/bin/pip install -r dev.requirements.txt
|
|
||||||
|
|
||||||
.PHONY: update
|
|
||||||
update:
|
update:
|
||||||
$(VIRTUAL_ENV)/bin/pip install --upgrade .
|
$(VENV)/bin/pip install -U .
|
||||||
|
|
||||||
.PHONY: update-all
|
|
||||||
update-all:
|
|
||||||
$(VIRTUAL_ENV)/bin/pip install --upgrade --force-reinstall .
|
|
||||||
|
|
||||||
.PHONY: dev
|
|
||||||
dev: style update
|
|
||||||
$(VIRTUAL_ENV)/bin/tuxbot
|
|
||||||
|
|
||||||
# Docker
|
|
||||||
.PHONY: docker
|
|
||||||
docker:
|
|
||||||
$(DOCKER_LOCAL) build
|
|
||||||
$(DOCKER_LOCAL) up -d
|
|
||||||
|
|
||||||
.PHONY: docker-start
|
|
||||||
docker-start:
|
|
||||||
$(DOCKER_TUXBOT) tuxbot
|
|
||||||
|
|
||||||
|
|
||||||
# Blackify code
|
# Blackify code
|
||||||
.PHONY: black
|
reformat:
|
||||||
black:
|
$(PYTHON) -m black `git ls-files "*.py"` --line-length=79 && pylint tuxbot
|
||||||
$(PYTHON_PATH) -m black `git ls-files "*.py"` --line-length=79
|
|
||||||
|
|
||||||
.PHONY: lint
|
|
||||||
lint:
|
|
||||||
$(PYTHON_PATH) -m pylint tuxbot
|
|
||||||
|
|
||||||
.PHONY: type
|
|
||||||
type:
|
|
||||||
$(PYTHON_PATH) -m mypy tuxbot
|
|
||||||
|
|
||||||
.PHONY: style
|
|
||||||
style: black lint type
|
|
||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
.PHONY: xgettext
|
|
||||||
xgettext:
|
xgettext:
|
||||||
for cog in tuxbot/cogs/*/; do \
|
for cog in tuxbot/cogs/*/; do \
|
||||||
xgettext `find $$cog -type f -name '*.py'` --output=$$cog/locales/messages.pot $(XGETTEXT_FLAGS); \
|
xgettext `find $$cog -type f -name '*.py'` --output=$$cog/locales/messages.pot $(XGETTEXT_FLAGS); \
|
||||||
done
|
done
|
||||||
|
|
||||||
.PHONY: msginit
|
|
||||||
msginit:
|
msginit:
|
||||||
for cog in tuxbot/cogs/*/; do \
|
for cog in tuxbot/cogs/*/; do \
|
||||||
msginit --input=$$cog/locales/messages.pot --output=$$cog/locales/fr-FR.po --locale=fr_FR.UTF-8 --no-translator; \
|
msginit --input=$$cog/locales/messages.pot --output=$$cog/locales/fr-FR.po --locale=fr_FR.UTF-8 --no-translator; \
|
||||||
msginit --input=$$cog/locales/messages.pot --output=$$cog/locales/en-US.po --locale=en_US.UTF-8 --no-translator; \
|
msginit --input=$$cog/locales/messages.pot --output=$$cog/locales/en-US.po --locale=en_US.UTF-8 --no-translator; \
|
||||||
done
|
done
|
||||||
|
|
||||||
.PHONY: msgmerge
|
|
||||||
msgmerge:
|
msgmerge:
|
||||||
for cog in tuxbot/cogs/*/; do \
|
for cog in tuxbot/cogs/*/; do \
|
||||||
msgmerge --update $$cog/locales/fr-FR.po $$cog/locales/messages.pot; \
|
msgmerge --update $$cog/locales/fr-FR.po $$cog/locales/messages.pot; \
|
||||||
|
|
74
README.rst
74
README.rst
|
@ -1,4 +1,4 @@
|
||||||
|image0| |image1| |image2| |image3|
|
|image0| |image1|
|
||||||
|
|
||||||
.. role:: bash(code)
|
.. role:: bash(code)
|
||||||
:language: bash
|
:language: bash
|
||||||
|
@ -14,7 +14,7 @@ Installing the pre-requirements
|
||||||
|
|
||||||
- The pre-requirements are:
|
- The pre-requirements are:
|
||||||
|
|
||||||
- Python 3.8 or greater
|
- Python 3.7 or greater
|
||||||
- Pip
|
- Pip
|
||||||
- Git
|
- Git
|
||||||
|
|
||||||
|
@ -26,9 +26,9 @@ Arch Linux
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ sudo pacman -Syu python python-pip python-virtualenv git make gcc postgresql
|
$ sudo pacman -Syu python python-pip python-virtualenv git
|
||||||
|
|
||||||
Continue to `configure postgresql <#configure-postgresql>`__.
|
Continue to `create the venv <#creating-the-virtual-environment>`__.
|
||||||
|
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
|
@ -38,21 +38,9 @@ Debian
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ sudo apt update
|
$ sudo apt update
|
||||||
$ sudo apt -y install python3 python3-dev python3-pip python3-venv git make gcc postgresql postgresql-client
|
$ sudo apt -y install python3 python3-dev python3-pip python3-venv git
|
||||||
|
|
||||||
Continue to `configure postgresql <#configure-postgresql>`__.
|
Continue to `create the venv <#creating-the-virtual-environment>`__.
|
||||||
|
|
||||||
--------------
|
|
||||||
|
|
||||||
RHEL and derivatives (CentOS, Fedora...)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ sudo dnf update
|
|
||||||
$ sudo dnf install python3 python3-devel python3-pip python3-virtualenv git make gcc postgresql-server postgresql-contrib
|
|
||||||
|
|
||||||
Continue to `configure postgresql <#configure-postgresql>`__.
|
|
||||||
|
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
|
@ -61,43 +49,6 @@ Windows
|
||||||
|
|
||||||
*not for now and not for the future*
|
*not for now and not for the future*
|
||||||
|
|
||||||
--------------
|
|
||||||
|
|
||||||
Configure PostgreSQL
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
Now, you need to setup PostgreSQL
|
|
||||||
|
|
||||||
Operating systems
|
|
||||||
~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Arch Linux
|
|
||||||
^^^^^^^^^^
|
|
||||||
|
|
||||||
https://wiki.archlinux.org/index.php/PostgreSQL
|
|
||||||
|
|
||||||
Continue to `create the venv <#creating-the-virtual-environment>`__.
|
|
||||||
|
|
||||||
--------------
|
|
||||||
|
|
||||||
Debian
|
|
||||||
^^^^^^
|
|
||||||
|
|
||||||
https://wiki.debian.org/PostgreSql
|
|
||||||
|
|
||||||
Continue to `create the venv <#creating-the-virtual-environment>`__.
|
|
||||||
|
|
||||||
--------------
|
|
||||||
|
|
||||||
RHEL and derivatives (CentOS, Fedora...)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
https://fedoraproject.org/wiki/PostgreSQL
|
|
||||||
|
|
||||||
Continue to `create the venv <#creating-the-virtual-environment>`__.
|
|
||||||
|
|
||||||
--------------
|
|
||||||
|
|
||||||
Creating the Virtual Environment
|
Creating the Virtual Environment
|
||||||
--------------------------------
|
--------------------------------
|
||||||
|
|
||||||
|
@ -110,7 +61,7 @@ two commands:
|
||||||
$ make install
|
$ make install
|
||||||
|
|
||||||
Now, switch your environment to the virtual one by run this single
|
Now, switch your environment to the virtual one by run this single
|
||||||
command: :bash:`source ~/venv/bin/activate`
|
command: :bash:`source ~/tuxvenv/bin/activate`
|
||||||
|
|
||||||
Configuration
|
Configuration
|
||||||
-------------
|
-------------
|
||||||
|
@ -118,12 +69,12 @@ Configuration
|
||||||
It's time to set up your first instance, to do this, you can simply
|
It's time to set up your first instance, to do this, you can simply
|
||||||
execute this command:
|
execute this command:
|
||||||
|
|
||||||
:bash:`tuxbot-setup`
|
:bash:`tuxbot-setup [your instance name]`
|
||||||
|
|
||||||
After following the instructions, you can run your instance by executing
|
After following the instructions, you can run your instance by executing
|
||||||
this command:
|
this command:
|
||||||
|
|
||||||
:bash:`tuxbot`
|
:bash:`tuxbot [your instance name]`
|
||||||
|
|
||||||
Update
|
Update
|
||||||
------
|
------
|
||||||
|
@ -134,8 +85,5 @@ To update the whole bot after a :bash:`git pull`, just execute
|
||||||
|
|
||||||
$ make update
|
$ make update
|
||||||
|
|
||||||
.. |image0| image:: https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10-%23007ec6
|
.. |image0| image:: https://img.shields.io/badge/python-3.7%20%7C%203.8%20%7C%203.9%20%7C%203.10-%23007ec6
|
||||||
.. |image1| image:: https://img.shields.io/github/issues/Rom1-J/tuxbot-bot
|
.. |image1| image:: https://img.shields.io/badge/dynamic/json?color=%23dfb317&label=issues&query=%24.open_issues_count&suffix=%20open&url=https%3A%2F%2Fgit.gnous.eu%2Fapi%2Fv1%2Frepos%2FGnousEU%2Ftuxbot-bot%2F
|
||||||
.. |image2| image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
|
||||||
.. |image3| image:: https://wakatime.com/badge/github/Rom1-J/tuxbot-bot.svg
|
|
||||||
:target: https://wakatime.com/badge/github/Rom1-J/tuxbot-bot
|
|
|
@ -1,38 +0,0 @@
|
||||||
FROM python:3.9-slim-buster
|
|
||||||
|
|
||||||
ENV PYTHONUNBUFFERED 1
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE 1
|
|
||||||
|
|
||||||
RUN apt-get update \
|
|
||||||
# dependencies for building Python packages
|
|
||||||
&& apt-get install -y build-essential \
|
|
||||||
# psycopg2 dependencies
|
|
||||||
&& apt-get install -y libpq-dev \
|
|
||||||
# Translations dependencies
|
|
||||||
&& apt-get install -y gettext \
|
|
||||||
# Git
|
|
||||||
&& apt-get install -y git \
|
|
||||||
# cleaning up unused files
|
|
||||||
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Requirements are installed here to ensure they will be cached.
|
|
||||||
COPY ./dev.requirements.txt /app/dev.requirements.txt
|
|
||||||
COPY ./tuxbot /app/tuxbot
|
|
||||||
COPY ./data /app/data
|
|
||||||
COPY ./setup.cfg /app/setup.cfg
|
|
||||||
COPY ./setup.py /app/setup.py
|
|
||||||
RUN pip install -r /app/dev.requirements.txt
|
|
||||||
RUN pip install ./app
|
|
||||||
|
|
||||||
COPY ./compose/production/tuxbot/entrypoint /entrypoint
|
|
||||||
RUN sed -i 's/\r$//g' /entrypoint
|
|
||||||
RUN chmod +x /entrypoint
|
|
||||||
|
|
||||||
COPY ./compose/local/tuxbot/start /start
|
|
||||||
RUN sed -i 's/\r$//g' /start
|
|
||||||
RUN chmod +x /start
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ENTRYPOINT ["/entrypoint"]
|
|
|
@ -1,6 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -o errexit
|
|
||||||
set -o pipefail
|
|
||||||
set -o nounset
|
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
FROM postgres:12.3
|
|
||||||
|
|
||||||
COPY ./compose/production/postgres/maintenance /usr/local/bin/maintenance
|
|
||||||
RUN chmod +x /usr/local/bin/maintenance/*
|
|
||||||
RUN mv /usr/local/bin/maintenance/* /usr/local/bin \
|
|
||||||
&& rmdir /usr/local/bin/maintenance
|
|
|
@ -1,5 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
|
|
||||||
BACKUP_DIR_PATH='/backups'
|
|
||||||
BACKUP_FILE_PREFIX='backup'
|
|
|
@ -1,12 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
|
|
||||||
countdown() {
|
|
||||||
declare desc="A simple countdown. Source: https://superuser.com/a/611582"
|
|
||||||
local seconds="${1}"
|
|
||||||
local d=$(($(date +%s) + "${seconds}"))
|
|
||||||
while [ "$d" -ge `date +%s` ]; do
|
|
||||||
echo -ne "$(date -u --date @$(($d - `date +%s`)) +%H:%M:%S)\r";
|
|
||||||
sleep 0.1
|
|
||||||
done
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
|
|
||||||
message_newline() {
|
|
||||||
echo
|
|
||||||
}
|
|
||||||
|
|
||||||
message_debug()
|
|
||||||
{
|
|
||||||
echo -e "DEBUG: ${@}"
|
|
||||||
}
|
|
||||||
|
|
||||||
message_welcome()
|
|
||||||
{
|
|
||||||
echo -e "\e[1m${@}\e[0m"
|
|
||||||
}
|
|
||||||
|
|
||||||
message_warning()
|
|
||||||
{
|
|
||||||
echo -e "\e[33mWARNING\e[0m: ${@}"
|
|
||||||
}
|
|
||||||
|
|
||||||
message_error()
|
|
||||||
{
|
|
||||||
echo -e "\e[31mERROR\e[0m: ${@}"
|
|
||||||
}
|
|
||||||
|
|
||||||
message_info()
|
|
||||||
{
|
|
||||||
echo -e "\e[37mINFO\e[0m: ${@}"
|
|
||||||
}
|
|
||||||
|
|
||||||
message_suggestion()
|
|
||||||
{
|
|
||||||
echo -e "\e[33mSUGGESTION\e[0m: ${@}"
|
|
||||||
}
|
|
||||||
|
|
||||||
message_success()
|
|
||||||
{
|
|
||||||
echo -e "\e[32mSUCCESS\e[0m: ${@}"
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
|
|
||||||
yes_no() {
|
|
||||||
declare desc="Prompt for confirmation. \$\"\{1\}\": confirmation message."
|
|
||||||
local arg1="${1}"
|
|
||||||
|
|
||||||
local response=
|
|
||||||
read -r -p "${arg1} (y/[n])? " response
|
|
||||||
if [[ "${response}" =~ ^[Yy]$ ]]
|
|
||||||
then
|
|
||||||
exit 0
|
|
||||||
else
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
|
|
||||||
### Create a database backup.
|
|
||||||
###
|
|
||||||
### Usage:
|
|
||||||
### $ docker-compose -f <environment>.yml (exec |run --rm) postgres backup
|
|
||||||
|
|
||||||
|
|
||||||
set -o errexit
|
|
||||||
set -o pipefail
|
|
||||||
set -o nounset
|
|
||||||
|
|
||||||
|
|
||||||
working_dir="$(dirname ${0})"
|
|
||||||
source "${working_dir}/_sourced/constants.sh"
|
|
||||||
source "${working_dir}/_sourced/messages.sh"
|
|
||||||
|
|
||||||
|
|
||||||
message_welcome "Backing up the '${POSTGRES_DB}' database..."
|
|
||||||
|
|
||||||
|
|
||||||
if [[ "${POSTGRES_USER}" == "postgres" ]]; then
|
|
||||||
message_error "Backing up as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
export PGHOST="${POSTGRES_HOST}"
|
|
||||||
export PGPORT="${POSTGRES_PORT}"
|
|
||||||
export PGUSER="${POSTGRES_USER}"
|
|
||||||
export PGPASSWORD="${POSTGRES_PASSWORD}"
|
|
||||||
export PGDATABASE="${POSTGRES_DB}"
|
|
||||||
|
|
||||||
backup_filename="${BACKUP_FILE_PREFIX}_$(date +'%Y_%m_%dT%H_%M_%S').sql.gz"
|
|
||||||
pg_dump | gzip > "${BACKUP_DIR_PATH}/${backup_filename}"
|
|
||||||
|
|
||||||
|
|
||||||
message_success "'${POSTGRES_DB}' database backup '${backup_filename}' has been created and placed in '${BACKUP_DIR_PATH}'."
|
|
|
@ -1,22 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
|
|
||||||
### View backups.
|
|
||||||
###
|
|
||||||
### Usage:
|
|
||||||
### $ docker-compose -f <environment>.yml (exec |run --rm) postgres backups
|
|
||||||
|
|
||||||
|
|
||||||
set -o errexit
|
|
||||||
set -o pipefail
|
|
||||||
set -o nounset
|
|
||||||
|
|
||||||
|
|
||||||
working_dir="$(dirname ${0})"
|
|
||||||
source "${working_dir}/_sourced/constants.sh"
|
|
||||||
source "${working_dir}/_sourced/messages.sh"
|
|
||||||
|
|
||||||
|
|
||||||
message_welcome "These are the backups you have got:"
|
|
||||||
|
|
||||||
ls -lht "${BACKUP_DIR_PATH}"
|
|
|
@ -1,55 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
|
|
||||||
### Restore database from a backup.
|
|
||||||
###
|
|
||||||
### Parameters:
|
|
||||||
### <1> filename of an existing backup.
|
|
||||||
###
|
|
||||||
### Usage:
|
|
||||||
### $ docker-compose -f <environment>.yml (exec |run --rm) postgres restore <1>
|
|
||||||
|
|
||||||
|
|
||||||
set -o errexit
|
|
||||||
set -o pipefail
|
|
||||||
set -o nounset
|
|
||||||
|
|
||||||
|
|
||||||
working_dir="$(dirname ${0})"
|
|
||||||
source "${working_dir}/_sourced/constants.sh"
|
|
||||||
source "${working_dir}/_sourced/messages.sh"
|
|
||||||
|
|
||||||
|
|
||||||
if [[ -z ${1+x} ]]; then
|
|
||||||
message_error "Backup filename is not specified yet it is a required parameter. Make sure you provide one and try again."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
backup_filename="${BACKUP_DIR_PATH}/${1}"
|
|
||||||
if [[ ! -f "${backup_filename}" ]]; then
|
|
||||||
message_error "No backup with the specified filename found. Check out the 'backups' maintenance script output to see if there is one and try again."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
message_welcome "Restoring the '${POSTGRES_DB}' database from the '${backup_filename}' backup..."
|
|
||||||
|
|
||||||
if [[ "${POSTGRES_USER}" == "postgres" ]]; then
|
|
||||||
message_error "Restoring as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
export PGHOST="${POSTGRES_HOST}"
|
|
||||||
export PGPORT="${POSTGRES_PORT}"
|
|
||||||
export PGUSER="${POSTGRES_USER}"
|
|
||||||
export PGPASSWORD="${POSTGRES_PASSWORD}"
|
|
||||||
export PGDATABASE="${POSTGRES_DB}"
|
|
||||||
|
|
||||||
message_info "Dropping the database..."
|
|
||||||
dropdb "${PGDATABASE}"
|
|
||||||
|
|
||||||
message_info "Creating a new database..."
|
|
||||||
createdb --owner="${POSTGRES_USER}"
|
|
||||||
|
|
||||||
message_info "Applying the backup to the new database..."
|
|
||||||
gunzip -c "${backup_filename}" | psql "${POSTGRES_DB}"
|
|
||||||
|
|
||||||
message_success "The '${POSTGRES_DB}' database has been restored from the '${backup_filename}' backup."
|
|
|
@ -1,40 +0,0 @@
|
||||||
FROM node:10-stretch-slim as client-builder
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Python build stage
|
|
||||||
FROM python:3.9-slim-buster
|
|
||||||
|
|
||||||
ENV PYTHONUNBUFFERED 1
|
|
||||||
|
|
||||||
RUN apt-get update \
|
|
||||||
# dependencies for building Python packages
|
|
||||||
&& apt-get install -y build-essential \
|
|
||||||
# psycopg2 dependencies
|
|
||||||
&& apt-get install -y libpq-dev \
|
|
||||||
# Translations dependencies
|
|
||||||
&& apt-get install -y gettext \
|
|
||||||
# cleaning up unused files
|
|
||||||
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
RUN addgroup --system tuxbot \
|
|
||||||
&& adduser --system --ingroup tuxbot tuxbot
|
|
||||||
|
|
||||||
# Requirements are installed here to ensure they will be cached.
|
|
||||||
COPY --chown=tuxbot:tuxbot ./compose/production/tuxbot/entrypoint /entrypoint
|
|
||||||
RUN sed -i 's/\r$//g' /entrypoint
|
|
||||||
RUN chmod +x /entrypoint
|
|
||||||
|
|
||||||
|
|
||||||
COPY --chown=tuxbot:tuxbot ./compose/production/tuxbot/start /start
|
|
||||||
RUN sed -i 's/\r$//g' /start
|
|
||||||
RUN chmod +x /start
|
|
||||||
COPY --from=client-builder --chown=tuxbot:tuxbot /app /app
|
|
||||||
|
|
||||||
|
|
||||||
USER tuxbot
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ENTRYPOINT ["/entrypoint"]
|
|
|
@ -1,43 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -o errexit
|
|
||||||
set -o pipefail
|
|
||||||
set -o nounset
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if [ -z "${POSTGRES_USER}" ]; then
|
|
||||||
base_postgres_image_default_user='postgres'
|
|
||||||
export POSTGRES_USER="${base_postgres_image_default_user}"
|
|
||||||
fi
|
|
||||||
export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}"
|
|
||||||
|
|
||||||
echo "psql at: ${DATABASE_URL}"
|
|
||||||
|
|
||||||
postgres_ready() {
|
|
||||||
python << END
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import asyncpg
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
try:
|
|
||||||
conn = await asyncpg.connect('postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}')
|
|
||||||
except Exception:
|
|
||||||
sys.exit(-1)
|
|
||||||
await conn.close()
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
asyncio.get_event_loop().run_until_complete(main())
|
|
||||||
|
|
||||||
END
|
|
||||||
}
|
|
||||||
until postgres_ready; do
|
|
||||||
>&2 echo 'Waiting for PostgreSQL to become available...'
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
>&2 echo 'PostgreSQL is available'
|
|
||||||
|
|
||||||
exec "$@"
|
|
|
@ -1,8 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -o errexit
|
|
||||||
set -o pipefail
|
|
||||||
set -o nounset
|
|
||||||
|
|
||||||
|
|
||||||
tuxbot dev
|
|
|
@ -1,3 +0,0 @@
|
||||||
pylint>=2.6.0
|
|
||||||
black>=20.8b1
|
|
||||||
mypy>=0.812
|
|
34
local.yml
34
local.yml
|
@ -1,34 +0,0 @@
|
||||||
version: '3'
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
local_postgres_data: {}
|
|
||||||
local_postgres_data_backups: {}
|
|
||||||
|
|
||||||
services:
|
|
||||||
tuxbot:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: ./compose/local/tuxbot/Dockerfile
|
|
||||||
restart: always
|
|
||||||
image: tuxbot_bot_local_tuxbot
|
|
||||||
container_name: tuxbot
|
|
||||||
depends_on:
|
|
||||||
- postgres
|
|
||||||
volumes:
|
|
||||||
- .:/app:z
|
|
||||||
env_file:
|
|
||||||
- ./.envs/.local/.tuxbot
|
|
||||||
- ./.envs/.local/.postgres
|
|
||||||
command: /start
|
|
||||||
|
|
||||||
postgres:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: ./compose/production/postgres/Dockerfile
|
|
||||||
image: tuxbot_bot_production_postgres
|
|
||||||
container_name: postgres
|
|
||||||
volumes:
|
|
||||||
- local_postgres_data:/var/lib/postgresql/data:Z
|
|
||||||
- local_postgres_data_backups:/backups:z
|
|
||||||
env_file:
|
|
||||||
- ./.envs/.local/.postgres
|
|
|
@ -1,30 +0,0 @@
|
||||||
version: '3'
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
production_postgres_data: {}
|
|
||||||
production_postgres_data_backups: {}
|
|
||||||
production_traefik: {}
|
|
||||||
|
|
||||||
services:
|
|
||||||
tuxbot:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: ./compose/production/tuxbot/Dockerfile
|
|
||||||
image: tuxbot_bot_production_tuxbot
|
|
||||||
depends_on:
|
|
||||||
- postgres
|
|
||||||
env_file:
|
|
||||||
- ./.envs/.production/.tuxbot
|
|
||||||
- ./.envs/.production/.postgres
|
|
||||||
command: /start
|
|
||||||
|
|
||||||
postgres:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: ./compose/production/postgres/Dockerfile
|
|
||||||
image: tuxbot_bot_production_postgres
|
|
||||||
volumes:
|
|
||||||
- production_postgres_data:/var/lib/postgresql/data:Z
|
|
||||||
- production_postgres_data_backups:/backups:z
|
|
||||||
env_file:
|
|
||||||
- ./.envs/.production/.postgres
|
|
25
setup.cfg
25
setup.cfg
|
@ -1,7 +1,7 @@
|
||||||
[metadata]
|
[metadata]
|
||||||
name = Tuxbot-bot
|
name = Tuxbot-bot
|
||||||
version = attr: tuxbot.__version__
|
version = attr: tuxbot.__version__
|
||||||
url = https://github.com/Rom1-J/tuxbot-bot/
|
url = https://git.gnous.eu/gnouseu/tuxbot-bot/
|
||||||
author = Romain J.
|
author = Romain J.
|
||||||
author_email = romain@gnous.eu
|
author_email = romain@gnous.eu
|
||||||
maintainer = Romain J.
|
maintainer = Romain J.
|
||||||
|
@ -13,25 +13,18 @@ platforms = linux
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
packages = find_namespace:
|
packages = find_namespace:
|
||||||
python_requires = >=3.8
|
python_requires = >=3.7
|
||||||
install_requires =
|
install_requires =
|
||||||
aiocache>=0.11.1
|
appdirs>=1.4.4
|
||||||
asyncpg>=0.21.0
|
|
||||||
Babel>=2.8.0
|
Babel>=2.8.0
|
||||||
beautifulsoup4>=4.9.3
|
black==20.8b1
|
||||||
discord.py @ git+https://github.com/Rapptz/discord.py
|
discord.py==1.5.0
|
||||||
discord-ext-menus
|
discord_flags==2.1.1
|
||||||
humanize>=2.6.0
|
humanize==2.6.0
|
||||||
ipinfo>=4.1.0
|
jishaku>=1.19.1.200
|
||||||
ipwhois>=1.2.0
|
|
||||||
jishaku @ git+https://github.com/Gorialis/jishaku
|
|
||||||
psutil>=5.7.2
|
psutil>=5.7.2
|
||||||
pydig>=0.3.0
|
rich>=6.0.0
|
||||||
; ralgo @ git+https://github.com/Rom1-J/ralgo
|
|
||||||
rich>=9.10.0
|
|
||||||
sentry_sdk>=0.20.2
|
|
||||||
structured_config>=4.12
|
structured_config>=4.12
|
||||||
tortoise-orm>=0.16.17
|
|
||||||
|
|
||||||
[options.entry_points]
|
[options.entry_points]
|
||||||
console_scripts =
|
console_scripts =
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -1,5 +1,5 @@
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
python_requires=">=3.8",
|
python_requires=">=3.7",
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import os
|
import os
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
build = os.popen("/usr/bin/git rev-parse --short HEAD").read().strip()
|
build = os.popen("git rev-parse --short HEAD").read().strip()
|
||||||
info = os.popen('/usr/bin/git log -n 3 -s --format="%s"').read().strip()
|
info = os.popen('git log -n 1 -s --format="%s"').read().strip()
|
||||||
|
|
||||||
VersionInfo = namedtuple(
|
VersionInfo = namedtuple(
|
||||||
"VersionInfo", "major minor micro releaselevel build, info"
|
"VersionInfo", "major minor micro releaselevel build, info"
|
||||||
|
|
|
@ -1,28 +1,32 @@
|
||||||
import sys
|
from typing import NoReturn
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.traceback import install
|
||||||
from tuxbot import ExitCodes
|
from tuxbot import ExitCodes
|
||||||
from tuxbot.core.utils.console import console
|
|
||||||
|
console = Console()
|
||||||
|
install(console=console)
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> NoReturn:
|
||||||
try:
|
try:
|
||||||
from .__run__ import run # pylint: disable=import-outside-toplevel
|
from .__run__ import run # pylint: disable=import-outside-toplevel
|
||||||
|
|
||||||
run()
|
run()
|
||||||
except SystemExit as exc:
|
except SystemExit as exc:
|
||||||
if exc.code == ExitCodes.RESTART:
|
if exc.code == ExitCodes.RESTART:
|
||||||
sys.exit(exc.code)
|
# reimport to load changes
|
||||||
|
from .__run__ import run # pylint: disable=import-outside-toplevel
|
||||||
|
|
||||||
|
run()
|
||||||
else:
|
else:
|
||||||
raise exc
|
raise exc
|
||||||
except Exception:
|
except Exception:
|
||||||
console.print_exception(
|
console.print_exception()
|
||||||
show_locals=True, word_wrap=True, extra_lines=5
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
try:
|
try:
|
||||||
main()
|
main()
|
||||||
except Exception:
|
except Exception:
|
||||||
console.print_exception(
|
console.print_exception()
|
||||||
show_locals=True, word_wrap=True, extra_lines=5
|
|
||||||
)
|
|
||||||
|
|
|
@ -4,34 +4,80 @@ import logging
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
import tracemalloc
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
|
from typing import NoReturn
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
|
import humanize
|
||||||
import pip
|
import pip
|
||||||
from rich.columns import Columns
|
from rich.columns import Columns
|
||||||
|
from rich.console import Console
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
|
from rich.traceback import install
|
||||||
from rich.table import Table, box
|
from rich.table import Table, box
|
||||||
|
from rich.text import Text
|
||||||
from rich import print as rprint
|
from rich import print as rprint
|
||||||
|
|
||||||
import tuxbot.logging
|
import tuxbot.logging
|
||||||
from tuxbot.core.bot import Tux
|
from tuxbot.core.bot import Tux
|
||||||
from tuxbot.core.utils import data_manager
|
from tuxbot.core import data_manager
|
||||||
from tuxbot.core.utils.console import console
|
from tuxbot.core import config
|
||||||
from . import __version__, version_info, ExitCodes
|
from . import __version__, version_info, ExitCodes
|
||||||
|
|
||||||
log = logging.getLogger("tuxbot.main")
|
log = logging.getLogger("tuxbot.main")
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
install(console=console)
|
||||||
|
tracemalloc.start()
|
||||||
|
|
||||||
BORDER_STYLE = "not dim"
|
BORDER_STYLE = "not dim"
|
||||||
|
|
||||||
|
|
||||||
def debug_info() -> None:
|
def list_instances() -> NoReturn:
|
||||||
|
"""List all available instances"""
|
||||||
|
app_config = config.ConfigFile(
|
||||||
|
data_manager.config_dir / "config.yaml", config.AppConfig
|
||||||
|
).config
|
||||||
|
|
||||||
|
console.print(
|
||||||
|
Panel("[bold green]Instances", style="green"), justify="center"
|
||||||
|
)
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
columns = Columns(expand=True, padding=2, align="center")
|
||||||
|
for instance, details in app_config.Instances.items():
|
||||||
|
active = details["active"]
|
||||||
|
last_run = (
|
||||||
|
humanize.naturaltime(
|
||||||
|
datetime.now() - datetime.fromtimestamp(details["last_run"])
|
||||||
|
)
|
||||||
|
or "[i]unknown"
|
||||||
|
)
|
||||||
|
|
||||||
|
table = Table(
|
||||||
|
style="dim", border_style=BORDER_STYLE, box=box.HEAVY_HEAD
|
||||||
|
)
|
||||||
|
table.add_column("Name")
|
||||||
|
table.add_column(("Running since" if active else "Last run"))
|
||||||
|
table.add_row(instance, last_run)
|
||||||
|
table.title = Text(instance, style="green" if active else "red")
|
||||||
|
columns.add_renderable(table)
|
||||||
|
console.print(columns)
|
||||||
|
console.print()
|
||||||
|
|
||||||
|
sys.exit(os.EX_OK)
|
||||||
|
|
||||||
|
|
||||||
|
def debug_info() -> NoReturn:
|
||||||
"""Show debug info relatives to the bot"""
|
"""Show debug info relatives to the bot"""
|
||||||
python_version = sys.version.replace("\n", "")
|
python_version = sys.version.replace("\n", "")
|
||||||
pip_version = pip.__version__
|
pip_version = pip.__version__
|
||||||
tuxbot_version = __version__
|
tuxbot_version = __version__
|
||||||
dpy_version = discord.__version__
|
dpy_version = discord.__version__
|
||||||
|
|
||||||
uptime = os.popen("/usr/bin/uptime").read().strip().split()
|
uptime = os.popen("uptime").read().strip().split()
|
||||||
|
|
||||||
console.print(
|
console.print(
|
||||||
Panel("[bold blue]Debug Info", style="blue"), justify="center"
|
Panel("[bold blue]Debug Info", style="blue"), justify="center"
|
||||||
|
@ -95,7 +141,7 @@ def parse_cli_flags(args: list) -> Namespace:
|
||||||
"""
|
"""
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Tuxbot - OpenSource bot",
|
description="Tuxbot - OpenSource bot",
|
||||||
usage="tuxbot [arguments]",
|
usage="tuxbot <instance_name> [arguments]",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--version",
|
"--version",
|
||||||
|
@ -106,14 +152,27 @@ def parse_cli_flags(args: list) -> Namespace:
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--debug", action="store_true", help="Show debug information."
|
"--debug", action="store_true", help="Show debug information."
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--list-instances",
|
||||||
|
"-L",
|
||||||
|
action="store_true",
|
||||||
|
help="List all instance names",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--token", "-T", type=str, help="Run Tuxbot with passed token"
|
"--token", "-T", type=str, help="Run Tuxbot with passed token"
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"instance_name",
|
||||||
|
nargs="?",
|
||||||
|
help="Name of the bot instance created during `tuxbot-setup`.",
|
||||||
|
)
|
||||||
|
|
||||||
return parser.parse_args(args)
|
args = parser.parse_args(args)
|
||||||
|
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
async def shutdown_handler(tux: Tux, signal_type, exit_code=None) -> None:
|
async def shutdown_handler(tux: Tux, signal_type, exit_code=None) -> NoReturn:
|
||||||
"""Handler when the bot shutdown
|
"""Handler when the bot shutdown
|
||||||
|
|
||||||
It cancels all running task.
|
It cancels all running task.
|
||||||
|
@ -154,7 +213,7 @@ async def run_bot(tux: Tux, cli_flags: Namespace) -> None:
|
||||||
None
|
None
|
||||||
When exiting, this function return None.
|
When exiting, this function return None.
|
||||||
"""
|
"""
|
||||||
data_path = data_manager.data_path
|
data_path = data_manager.data_path(tux.instance_name)
|
||||||
|
|
||||||
tuxbot.logging.init_logging(10, location=data_path / "logs")
|
tuxbot.logging.init_logging(10, location=data_path / "logs")
|
||||||
|
|
||||||
|
@ -173,9 +232,9 @@ async def run_bot(tux: Tux, cli_flags: Namespace) -> None:
|
||||||
try:
|
try:
|
||||||
await tux.load_packages()
|
await tux.load_packages()
|
||||||
console.print()
|
console.print()
|
||||||
await tux.start(token=token)
|
await tux.start(token=token, bot=True)
|
||||||
except discord.LoginFailure:
|
except discord.LoginFailure:
|
||||||
log.critical("This token appears to be invalid.")
|
log.critical("This token appears to be valid.")
|
||||||
console.print()
|
console.print()
|
||||||
console.print(
|
console.print(
|
||||||
"[prompt.invalid]This token appears to be valid. [i]exiting...[/i]"
|
"[prompt.invalid]This token appears to be valid. [i]exiting...[/i]"
|
||||||
|
@ -188,12 +247,14 @@ async def run_bot(tux: Tux, cli_flags: Namespace) -> None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def run() -> None:
|
def run() -> NoReturn:
|
||||||
"""Main function"""
|
"""Main function"""
|
||||||
tux = None
|
tux = None
|
||||||
cli_flags = parse_cli_flags(sys.argv[1:])
|
cli_flags = parse_cli_flags(sys.argv[1:])
|
||||||
|
|
||||||
if cli_flags.debug:
|
if cli_flags.list_instances:
|
||||||
|
list_instances()
|
||||||
|
elif cli_flags.debug:
|
||||||
debug_info()
|
debug_info()
|
||||||
elif cli_flags.version:
|
elif cli_flags.version:
|
||||||
rprint(f"Tuxbot V{version_info.major}")
|
rprint(f"Tuxbot V{version_info.major}")
|
||||||
|
@ -205,6 +266,13 @@ def run() -> None:
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if not cli_flags.instance_name:
|
||||||
|
console.print(
|
||||||
|
"[red]No instance provided ! "
|
||||||
|
"You can use 'tuxbot -L' to list all available instances"
|
||||||
|
)
|
||||||
|
sys.exit(ExitCodes.CRITICAL)
|
||||||
|
|
||||||
tux = Tux(
|
tux = Tux(
|
||||||
cli_flags=cli_flags,
|
cli_flags=cli_flags,
|
||||||
description="Tuxbot, made from and for OpenSource",
|
description="Tuxbot, made from and for OpenSource",
|
||||||
|
@ -228,7 +296,7 @@ def run() -> None:
|
||||||
raise
|
raise
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
log.error("Unexpected exception (%s): ", type(exc))
|
log.error("Unexpected exception (%s): ", type(exc))
|
||||||
console.print_exception(show_locals=True)
|
console.print_exception()
|
||||||
if tux is not None:
|
if tux is not None:
|
||||||
loop.run_until_complete(shutdown_handler(tux, None, 1))
|
loop.run_until_complete(shutdown_handler(tux, None, 1))
|
||||||
finally:
|
finally:
|
||||||
|
|
|
@ -1,57 +0,0 @@
|
||||||
import logging
|
|
||||||
|
|
||||||
from discord.ext import commands
|
|
||||||
from jishaku.models import copy_context_with
|
|
||||||
|
|
||||||
from tuxbot.core.utils import checks
|
|
||||||
from tuxbot.core.bot import Tux
|
|
||||||
from tuxbot.core.i18n import (
|
|
||||||
Translator,
|
|
||||||
)
|
|
||||||
from tuxbot.core.utils.functions.extra import (
|
|
||||||
command_extra,
|
|
||||||
ContextPlus,
|
|
||||||
)
|
|
||||||
|
|
||||||
log = logging.getLogger("tuxbot.cogs.Admin")
|
|
||||||
_ = Translator("Admin", __file__)
|
|
||||||
|
|
||||||
|
|
||||||
class Admin(commands.Cog):
|
|
||||||
def __init__(self, bot: Tux):
|
|
||||||
self.bot = bot
|
|
||||||
|
|
||||||
# =========================================================================
|
|
||||||
# =========================================================================
|
|
||||||
|
|
||||||
@command_extra(name="quit", aliases=["shutdown"], deletable=False)
|
|
||||||
@checks.is_owner()
|
|
||||||
async def _quit(self, ctx: ContextPlus):
|
|
||||||
await ctx.send("*quit...*")
|
|
||||||
await self.bot.shutdown()
|
|
||||||
|
|
||||||
@command_extra(name="restart", deletable=False)
|
|
||||||
@checks.is_owner()
|
|
||||||
async def _restart(self, ctx: ContextPlus):
|
|
||||||
await ctx.send("*restart...*")
|
|
||||||
await self.bot.shutdown(restart=True)
|
|
||||||
|
|
||||||
@command_extra(name="update", deletable=False)
|
|
||||||
@checks.is_owner()
|
|
||||||
async def _update(self, ctx: ContextPlus):
|
|
||||||
sh = "jsk sh"
|
|
||||||
|
|
||||||
git = f"{sh} git pull"
|
|
||||||
update = f"{sh} make update"
|
|
||||||
|
|
||||||
git_command_ctx = await copy_context_with(
|
|
||||||
ctx, content=ctx.prefix + git
|
|
||||||
)
|
|
||||||
update_command_ctx = await copy_context_with(
|
|
||||||
ctx, content=ctx.prefix + update
|
|
||||||
)
|
|
||||||
|
|
||||||
await git_command_ctx.command.invoke(git_command_ctx)
|
|
||||||
await update_command_ctx.command.invoke(update_command_ctx)
|
|
||||||
|
|
||||||
await self._restart(ctx)
|
|
|
@ -1,12 +0,0 @@
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from structured_config import Structure
|
|
||||||
|
|
||||||
HAS_MODELS = False
|
|
||||||
|
|
||||||
|
|
||||||
class AdminConfig(Structure):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
extra: Dict[str, Dict] = {}
|
|
|
@ -1,18 +0,0 @@
|
||||||
# SOME DESCRIPTIVE TITLE.
|
|
||||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
|
||||||
# This file is distributed under the same license as the Tuxbot-bot package.
|
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
||||||
#
|
|
||||||
#, fuzzy
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: Tuxbot-bot\n"
|
|
||||||
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
|
|
||||||
"POT-Creation-Date: 2021-03-01 14:59+0100\n"
|
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
||||||
"Language: \n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=CHARSET\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
|
@ -1,19 +0,0 @@
|
||||||
from collections import namedtuple
|
|
||||||
|
|
||||||
from tuxbot.core.bot import Tux
|
|
||||||
from .custom import Custom
|
|
||||||
from .config import CustomConfig, HAS_MODELS
|
|
||||||
|
|
||||||
VersionInfo = namedtuple("VersionInfo", "major minor micro release_level")
|
|
||||||
version_info = VersionInfo(major=1, minor=0, micro=0, release_level="alpha")
|
|
||||||
|
|
||||||
__version__ = "v{}.{}.{}-{}".format(
|
|
||||||
version_info.major,
|
|
||||||
version_info.minor,
|
|
||||||
version_info.micro,
|
|
||||||
version_info.release_level,
|
|
||||||
).replace("\n", "")
|
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: Tux):
|
|
||||||
bot.add_cog(Custom(bot))
|
|
|
@ -1,12 +0,0 @@
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from structured_config import Structure
|
|
||||||
|
|
||||||
HAS_MODELS = False
|
|
||||||
|
|
||||||
|
|
||||||
class CustomConfig(Structure):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
extra: Dict[str, Dict] = {}
|
|
|
@ -1,112 +0,0 @@
|
||||||
import logging
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
import discord
|
|
||||||
from discord.ext import commands
|
|
||||||
|
|
||||||
from tuxbot.cogs.Custom.functions.converters import AliasConvertor
|
|
||||||
from tuxbot.core.bot import Tux
|
|
||||||
from tuxbot.core.config import set_for_key, search_for, set_if_none
|
|
||||||
from tuxbot.core.config import Config
|
|
||||||
from tuxbot.core.i18n import (
|
|
||||||
Translator,
|
|
||||||
find_locale,
|
|
||||||
get_locale_name,
|
|
||||||
list_locales,
|
|
||||||
)
|
|
||||||
from tuxbot.core.utils.functions.extra import (
|
|
||||||
group_extra,
|
|
||||||
ContextPlus,
|
|
||||||
)
|
|
||||||
|
|
||||||
log = logging.getLogger("tuxbot.cogs.Custom")
|
|
||||||
_ = Translator("Custom", __file__)
|
|
||||||
|
|
||||||
|
|
||||||
class Custom(commands.Cog):
|
|
||||||
def __init__(self, bot: Tux):
|
|
||||||
self.bot = bot
|
|
||||||
|
|
||||||
async def cog_command_error(self, ctx, error):
|
|
||||||
if isinstance(error, commands.BadArgument):
|
|
||||||
await ctx.send(_(str(error), ctx, self.bot.config))
|
|
||||||
|
|
||||||
# =========================================================================
|
|
||||||
# =========================================================================
|
|
||||||
|
|
||||||
async def _get_aliases(self, ctx: ContextPlus) -> dict:
|
|
||||||
return search_for(self.bot.config.Users, ctx.author.id, "aliases")
|
|
||||||
|
|
||||||
async def _save_lang(self, ctx: ContextPlus, lang: str) -> None:
|
|
||||||
set_for_key(
|
|
||||||
self.bot.config.Users, ctx.author.id, Config.User, locale=lang
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _save_alias(self, ctx: ContextPlus, alias: dict) -> None:
|
|
||||||
set_for_key(
|
|
||||||
self.bot.config.Users, ctx.author.id, Config.User, alias=alias
|
|
||||||
)
|
|
||||||
|
|
||||||
# =========================================================================
|
|
||||||
# =========================================================================
|
|
||||||
|
|
||||||
@group_extra(name="custom", aliases=["perso"], deletable=True)
|
|
||||||
@commands.guild_only()
|
|
||||||
async def _custom(self, ctx: ContextPlus):
|
|
||||||
"""Manage custom settings."""
|
|
||||||
|
|
||||||
@_custom.command(name="locale", aliases=["langue", "lang"])
|
|
||||||
async def _custom_locale(self, ctx: ContextPlus, lang: str):
|
|
||||||
try:
|
|
||||||
await self._save_lang(ctx, find_locale(lang.lower()))
|
|
||||||
await ctx.send(
|
|
||||||
_(
|
|
||||||
"Locale changed for you to {lang} successfully",
|
|
||||||
ctx,
|
|
||||||
self.bot.config,
|
|
||||||
).format(lang=f"`{get_locale_name(lang).lower()}`")
|
|
||||||
)
|
|
||||||
except NotImplementedError:
|
|
||||||
e = discord.Embed(
|
|
||||||
title=_("List of available locales: ", ctx, self.bot.config),
|
|
||||||
description=list_locales,
|
|
||||||
color=0x36393E,
|
|
||||||
)
|
|
||||||
|
|
||||||
await ctx.send(embed=e)
|
|
||||||
|
|
||||||
@_custom.command(name="alias", aliases=["aliases"])
|
|
||||||
async def _custom_alias(self, ctx: ContextPlus, *, alias: AliasConvertor):
|
|
||||||
args: List[str] = str(alias).split(" | ")
|
|
||||||
|
|
||||||
command = args[0]
|
|
||||||
custom = args[1]
|
|
||||||
|
|
||||||
user_aliases = await self._get_aliases(ctx)
|
|
||||||
|
|
||||||
if not user_aliases:
|
|
||||||
set_if_none(self.bot.config.Users, ctx.author.id, Config.User)
|
|
||||||
user_aliases = await self._get_aliases(ctx)
|
|
||||||
|
|
||||||
if custom in user_aliases.keys():
|
|
||||||
return await ctx.send(
|
|
||||||
_(
|
|
||||||
"The alias `{alias}` is already defined "
|
|
||||||
"for the command `{command}`",
|
|
||||||
ctx,
|
|
||||||
self.bot.config,
|
|
||||||
).format(alias=custom, command=user_aliases.get(custom))
|
|
||||||
)
|
|
||||||
|
|
||||||
user_aliases[custom] = command
|
|
||||||
|
|
||||||
await self._save_alias(ctx, user_aliases)
|
|
||||||
|
|
||||||
await ctx.send(
|
|
||||||
_(
|
|
||||||
"The alias `{alias}` for the command `{command}` "
|
|
||||||
"was successfully created",
|
|
||||||
ctx,
|
|
||||||
self.bot.config,
|
|
||||||
).format(alias=custom, command=command)
|
|
||||||
)
|
|
|
@ -1,29 +0,0 @@
|
||||||
from discord.ext import commands
|
|
||||||
from jishaku.models import copy_context_with
|
|
||||||
|
|
||||||
|
|
||||||
def _(x):
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
class AliasConvertor(commands.Converter):
|
|
||||||
async def convert(self, ctx, argument):
|
|
||||||
args = argument.split(" | ")
|
|
||||||
|
|
||||||
if len(args) <= 1:
|
|
||||||
raise commands.BadArgument(
|
|
||||||
_("Alias must be like `[command] | [alias]`")
|
|
||||||
)
|
|
||||||
|
|
||||||
command_ctx = await copy_context_with(
|
|
||||||
ctx, content=ctx.prefix + args[0]
|
|
||||||
)
|
|
||||||
alias_ctx = await copy_context_with(ctx, content=ctx.prefix + args[1])
|
|
||||||
|
|
||||||
if command_ctx.command is None:
|
|
||||||
raise commands.BadArgument(_("Unknown command"))
|
|
||||||
|
|
||||||
if args[0] != args[1] and alias_ctx.command is not None:
|
|
||||||
raise commands.BadArgument(_("Command already exists"))
|
|
||||||
|
|
||||||
return argument
|
|
|
@ -1,51 +0,0 @@
|
||||||
# French translations for Tuxbot-bot package
|
|
||||||
# Traductions françaises du paquet Tuxbot-bot.
|
|
||||||
# Copyright (C) 2020 THE Tuxbot-bot'S COPYRIGHT HOLDER
|
|
||||||
# This file is distributed under the same license as the Tuxbot-bot package.
|
|
||||||
# Automatically generated, 2020.
|
|
||||||
#
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: Tuxbot-bot\n"
|
|
||||||
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
|
|
||||||
"POT-Creation-Date: 2021-01-19 14:39+0100\n"
|
|
||||||
"PO-Revision-Date: 2021-01-19 14:39+0100\n"
|
|
||||||
"Last-Translator: Automatically generated\n"
|
|
||||||
"Language-Team: none\n"
|
|
||||||
"Language: fr\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
|
||||||
#: tuxbot/cogs/Custom/custom.py:69
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Locale changed for you to {lang} successfully"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Custom/custom.py:76
|
|
||||||
msgid "List of available locales: "
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Custom/custom.py:95
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "The alias `{alias}` is already defined for the command `{command}`"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Custom/custom.py:123
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "The alias `{alias}` for the command `{command}` was successfully created"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Custom/functions/converters.py:14
|
|
||||||
msgid "Alias must be like `[command] | [alias]`"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Custom/functions/converters.py:23
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Unknown command"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Custom/functions/converters.py:26
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Command already exists"
|
|
||||||
msgstr ""
|
|
|
@ -1,52 +0,0 @@
|
||||||
# French translations for Tuxbot-bot package
|
|
||||||
# Traductions françaises du paquet Tuxbot-bot.
|
|
||||||
# Copyright (C) 2020 THE Tuxbot-bot'S COPYRIGHT HOLDER
|
|
||||||
# This file is distributed under the same license as the Tuxbot-bot package.
|
|
||||||
# Automatically generated, 2020.
|
|
||||||
#
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: Tuxbot-bot\n"
|
|
||||||
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
|
|
||||||
"POT-Creation-Date: 2021-01-19 14:39+0100\n"
|
|
||||||
"PO-Revision-Date: 2021-01-19 14:39+0100\n"
|
|
||||||
"Last-Translator: Automatically generated\n"
|
|
||||||
"Language-Team: none\n"
|
|
||||||
"Language: fr\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Custom/custom.py:69
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Locale changed for you to {lang} successfully"
|
|
||||||
msgstr "Langue changée pour vous en {lang} avec succès"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Custom/custom.py:76
|
|
||||||
msgid "List of available locales: "
|
|
||||||
msgstr "Liste des langues disponibles: "
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Custom/custom.py:95
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "The alias `{alias}` is already defined for the command `{command}`"
|
|
||||||
msgstr "L'alias `{alias}` est déjà défini pour la commande `{command}`"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Custom/custom.py:123
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "The alias `{alias}` for the command `{command}` was successfully created"
|
|
||||||
msgstr "L'alias `{alias}` pour la commande `{command}` a été créé avec succès"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Custom/functions/converters.py:14
|
|
||||||
msgid "Alias must be like `[command] | [alias]`"
|
|
||||||
msgstr "L'alias doit être comme `[command] | [alias"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Custom/functions/converters.py:23
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Unknown command"
|
|
||||||
msgstr "Commande inconnue"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Custom/functions/converters.py:26
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Command already exists"
|
|
||||||
msgstr "La commande existe déjà"
|
|
|
@ -1,49 +0,0 @@
|
||||||
# SOME DESCRIPTIVE TITLE.
|
|
||||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
|
||||||
# This file is distributed under the same license as the Tuxbot-bot package.
|
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
||||||
#
|
|
||||||
#, fuzzy
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: Tuxbot-bot\n"
|
|
||||||
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
|
|
||||||
"POT-Creation-Date: 2021-05-17 00:04+0200\n"
|
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
||||||
"Language: \n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=CHARSET\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Custom/custom.py:64
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Locale changed for you to {lang} successfully"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Custom/custom.py:71
|
|
||||||
msgid "List of available locales: "
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Custom/custom.py:94
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "The alias `{alias}` is already defined for the command `{command}`"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Custom/custom.py:107
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "The alias `{alias}` for the command `{command}` was successfully created"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Custom/functions/converters.py:15
|
|
||||||
msgid "Alias must be like `[command] | [alias]`"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Custom/functions/converters.py:24
|
|
||||||
msgid "Unknown command"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Custom/functions/converters.py:27
|
|
||||||
msgid "Command already exists"
|
|
||||||
msgstr ""
|
|
|
@ -1 +0,0 @@
|
||||||
# pylint: disable=cyclic-import
|
|
|
@ -1,20 +0,0 @@
|
||||||
from collections import namedtuple
|
|
||||||
|
|
||||||
from tuxbot.core.bot import Tux
|
|
||||||
from .dev import Dev
|
|
||||||
from .config import DevConfig, HAS_MODELS
|
|
||||||
|
|
||||||
VersionInfo = namedtuple("VersionInfo", "major minor micro release_level")
|
|
||||||
version_info = VersionInfo(major=0, minor=1, micro=0, release_level="alpha")
|
|
||||||
|
|
||||||
__version__ = "v{}.{}.{}-{}".format(
|
|
||||||
version_info.major,
|
|
||||||
version_info.minor,
|
|
||||||
version_info.micro,
|
|
||||||
version_info.release_level,
|
|
||||||
).replace("\n", "")
|
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: Tux):
|
|
||||||
cog = Dev(bot)
|
|
||||||
bot.add_cog(cog)
|
|
|
@ -1,12 +0,0 @@
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from structured_config import Structure
|
|
||||||
|
|
||||||
HAS_MODELS = False
|
|
||||||
|
|
||||||
|
|
||||||
class DevConfig(Structure):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
extra: Dict[str, Dict] = {}
|
|
|
@ -1,142 +0,0 @@
|
||||||
import logging
|
|
||||||
import random
|
|
||||||
import string
|
|
||||||
|
|
||||||
import discord
|
|
||||||
from discord.enums import ButtonStyle
|
|
||||||
from discord import ui, SelectOption
|
|
||||||
from discord.ext import commands
|
|
||||||
|
|
||||||
from tuxbot.cogs.Dev.functions.utils import TicTacToe
|
|
||||||
from tuxbot.core.bot import Tux
|
|
||||||
from tuxbot.core.i18n import (
|
|
||||||
Translator,
|
|
||||||
)
|
|
||||||
from tuxbot.core.utils import checks
|
|
||||||
from tuxbot.core.utils.functions.extra import command_extra, ContextPlus
|
|
||||||
|
|
||||||
log = logging.getLogger("tuxbot.cogs.Dev")
|
|
||||||
_ = Translator("Dev", __file__)
|
|
||||||
|
|
||||||
|
|
||||||
class Test(ui.View):
|
|
||||||
@ui.button(label="label1", disabled=True, style=ButtonStyle.grey)
|
|
||||||
async def label1(self, button, interaction):
|
|
||||||
print("label1")
|
|
||||||
|
|
||||||
print(type(button), button)
|
|
||||||
print(type(interaction), interaction)
|
|
||||||
|
|
||||||
@ui.button(label="label2", style=ButtonStyle.danger)
|
|
||||||
async def label2(self, button, interaction):
|
|
||||||
print("label2")
|
|
||||||
|
|
||||||
print(type(button), button)
|
|
||||||
print(type(interaction), interaction)
|
|
||||||
|
|
||||||
|
|
||||||
class Test2(ui.View):
|
|
||||||
@ui.select(
|
|
||||||
placeholder="placeholder",
|
|
||||||
min_values=1,
|
|
||||||
max_values=3,
|
|
||||||
options=[
|
|
||||||
SelectOption(
|
|
||||||
label="label1",
|
|
||||||
value="value1",
|
|
||||||
description="description1",
|
|
||||||
),
|
|
||||||
SelectOption(
|
|
||||||
label="label2",
|
|
||||||
value="value2",
|
|
||||||
description="description2",
|
|
||||||
),
|
|
||||||
SelectOption(
|
|
||||||
label="label3",
|
|
||||||
value="value3",
|
|
||||||
description="description3",
|
|
||||||
),
|
|
||||||
SelectOption(
|
|
||||||
label="label4",
|
|
||||||
value="value4",
|
|
||||||
description="description4",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def select1(self, *args, **kwargs):
|
|
||||||
print("select1")
|
|
||||||
|
|
||||||
print(args)
|
|
||||||
print(kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class Dev(commands.Cog):
|
|
||||||
def __init__(self, bot: Tux):
|
|
||||||
self.bot = bot
|
|
||||||
|
|
||||||
# =========================================================================
|
|
||||||
# =========================================================================
|
|
||||||
|
|
||||||
@command_extra(name="crash", deletable=True)
|
|
||||||
@checks.is_owner()
|
|
||||||
async def _crash(self, ctx: ContextPlus, crash_type: str):
|
|
||||||
if crash_type == "ZeroDivisionError":
|
|
||||||
await ctx.send(str(5 / 0))
|
|
||||||
elif crash_type == "TypeError":
|
|
||||||
# noinspection PyTypeChecker
|
|
||||||
await ctx.send(str(int([]))) # type: ignore
|
|
||||||
elif crash_type == "IndexError":
|
|
||||||
await ctx.send(str([0][5]))
|
|
||||||
|
|
||||||
# =========================================================================
|
|
||||||
|
|
||||||
@command_extra(name="test", deletable=True)
|
|
||||||
@checks.is_owner()
|
|
||||||
async def _test(self, ctx: ContextPlus):
|
|
||||||
button = ui.Button(
|
|
||||||
style=ButtonStyle.primary,
|
|
||||||
label="test",
|
|
||||||
)
|
|
||||||
button2 = ui.Button(
|
|
||||||
style=ButtonStyle.secondary,
|
|
||||||
label="test2",
|
|
||||||
)
|
|
||||||
button3 = ui.Button(
|
|
||||||
style=ButtonStyle.green,
|
|
||||||
label="test3",
|
|
||||||
)
|
|
||||||
button4 = ui.Button(
|
|
||||||
style=ButtonStyle.blurple,
|
|
||||||
label="test4",
|
|
||||||
)
|
|
||||||
button5 = ui.Button(
|
|
||||||
style=ButtonStyle.danger,
|
|
||||||
label="test5",
|
|
||||||
)
|
|
||||||
|
|
||||||
view = ui.View()
|
|
||||||
view.add_item(button)
|
|
||||||
view.add_item(button2)
|
|
||||||
view.add_item(button3)
|
|
||||||
view.add_item(button4)
|
|
||||||
view.add_item(button5)
|
|
||||||
|
|
||||||
await ctx.send("test", view=view)
|
|
||||||
|
|
||||||
# =========================================================================
|
|
||||||
|
|
||||||
@command_extra(name="test2", deletable=True)
|
|
||||||
@checks.is_owner()
|
|
||||||
async def _test2(self, ctx: ContextPlus):
|
|
||||||
await ctx.send(view=Test2())
|
|
||||||
|
|
||||||
# =========================================================================
|
|
||||||
|
|
||||||
@command_extra(name="test3", deletable=False)
|
|
||||||
async def _test3(self, ctx: ContextPlus, opponent: discord.Member):
|
|
||||||
game = await ctx.send(f"Turn: {ctx.author}")
|
|
||||||
game_id = "".join(random.choices(string.ascii_letters, k=10))
|
|
||||||
|
|
||||||
view = TicTacToe(ctx.message.author, opponent, game, game_id=game_id)
|
|
||||||
|
|
||||||
await game.edit(content=f"Turn: {ctx.author}", view=view)
|
|
|
@ -1,161 +0,0 @@
|
||||||
from typing import List, Optional, Dict
|
|
||||||
|
|
||||||
import discord
|
|
||||||
from discord import ui
|
|
||||||
from discord.enums import ButtonStyle
|
|
||||||
|
|
||||||
|
|
||||||
class TicTacToe(ui.View):
|
|
||||||
turn: int = 0
|
|
||||||
grid: Dict[str, List[List[Optional[int]]]] = {}
|
|
||||||
win: bool = False
|
|
||||||
|
|
||||||
def __init__(self, player: discord.Member, opponent: discord.Member,
|
|
||||||
game: discord.Message, game_id: str):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self.player = player
|
|
||||||
self.opponent = opponent
|
|
||||||
self.game = game
|
|
||||||
|
|
||||||
self.game_id = game_id
|
|
||||||
|
|
||||||
self.init_grid()
|
|
||||||
|
|
||||||
def init_grid(self):
|
|
||||||
self.grid[self.game_id]: List[List[Optional[int]]] = [
|
|
||||||
[None for _ in range(3)]
|
|
||||||
for _ in range(3)
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_grid(self):
|
|
||||||
return self.grid[self.game_id]
|
|
||||||
|
|
||||||
def get_turn(self):
|
|
||||||
return self.player if self.turn == 0 else self.opponent
|
|
||||||
|
|
||||||
def get_emoji(self):
|
|
||||||
return "❌" if self.turn == 0 else "⭕"
|
|
||||||
|
|
||||||
def check_win(self):
|
|
||||||
wins = [
|
|
||||||
[self.get_grid()[0][0], self.get_grid()[0][1], self.get_grid()[0][2]],
|
|
||||||
[self.get_grid()[1][0], self.get_grid()[1][1], self.get_grid()[1][2]],
|
|
||||||
[self.get_grid()[2][0], self.get_grid()[2][1], self.get_grid()[2][2]],
|
|
||||||
[self.get_grid()[0][0], self.get_grid()[1][0], self.get_grid()[2][0]],
|
|
||||||
[self.get_grid()[0][1], self.get_grid()[1][1], self.get_grid()[2][1]],
|
|
||||||
[self.get_grid()[0][2], self.get_grid()[1][2], self.get_grid()[2][2]],
|
|
||||||
[self.get_grid()[0][0], self.get_grid()[1][1], self.get_grid()[2][2]],
|
|
||||||
[self.get_grid()[2][0], self.get_grid()[1][1], self.get_grid()[0][2]],
|
|
||||||
]
|
|
||||||
|
|
||||||
return [self.turn, self.turn, self.turn] in wins
|
|
||||||
|
|
||||||
async def congrats(self):
|
|
||||||
self.win = True
|
|
||||||
del self.grid[self.game_id]
|
|
||||||
|
|
||||||
await self.game.edit(
|
|
||||||
content=f"{self.get_turn()} wins!",
|
|
||||||
view=self
|
|
||||||
)
|
|
||||||
|
|
||||||
def set_pos(self, i, j):
|
|
||||||
self.get_grid()[i][j] = self.turn
|
|
||||||
|
|
||||||
async def next_turn(self, i, j):
|
|
||||||
if self.win:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.set_pos(i, j)
|
|
||||||
|
|
||||||
if self.check_win():
|
|
||||||
return await self.congrats()
|
|
||||||
|
|
||||||
self.turn = 1 if self.turn == 0 else 0
|
|
||||||
|
|
||||||
await self.game.edit(
|
|
||||||
content=f"Turn {self.get_turn()}",
|
|
||||||
view=self
|
|
||||||
)
|
|
||||||
|
|
||||||
# =========================================================================
|
|
||||||
# =========================================================================
|
|
||||||
|
|
||||||
@ui.button(label="•", style=ButtonStyle.grey, group=1)
|
|
||||||
async def button_1(self, button: ui.Button,
|
|
||||||
interaction: discord.Interaction):
|
|
||||||
if button.label == "•" and interaction.user == self.get_turn():
|
|
||||||
button.label = ""
|
|
||||||
button.emoji = self.get_emoji()
|
|
||||||
await self.next_turn(0, 0)
|
|
||||||
|
|
||||||
@ui.button(label="•", style=ButtonStyle.grey, group=1)
|
|
||||||
async def button_2(self, button: ui.Button,
|
|
||||||
interaction: discord.Interaction):
|
|
||||||
if button.label == "•" and interaction.user == self.get_turn():
|
|
||||||
button.label = ""
|
|
||||||
button.emoji = self.get_emoji()
|
|
||||||
await self.next_turn(0, 1)
|
|
||||||
|
|
||||||
@ui.button(label="•", style=ButtonStyle.grey, group=1)
|
|
||||||
async def button_3(self, button: ui.Button,
|
|
||||||
interaction: discord.Interaction):
|
|
||||||
if button.label == "•" and interaction.user == self.get_turn():
|
|
||||||
button.label = ""
|
|
||||||
button.emoji = self.get_emoji()
|
|
||||||
await self.next_turn(0, 2)
|
|
||||||
|
|
||||||
# =========================================================================
|
|
||||||
# =========================================================================
|
|
||||||
|
|
||||||
@ui.button(label="•", style=ButtonStyle.grey, group=2)
|
|
||||||
async def button_4(self, button: ui.Button,
|
|
||||||
interaction: discord.Interaction):
|
|
||||||
if button.label == "•" and interaction.user == self.get_turn():
|
|
||||||
button.label = ""
|
|
||||||
button.emoji = self.get_emoji()
|
|
||||||
await self.next_turn(1, 0)
|
|
||||||
|
|
||||||
@ui.button(label="•", style=ButtonStyle.grey, group=2)
|
|
||||||
async def button_5(self, button: ui.Button,
|
|
||||||
interaction: discord.Interaction):
|
|
||||||
if button.label == "•" and interaction.user == self.get_turn():
|
|
||||||
button.label = ""
|
|
||||||
button.emoji = self.get_emoji()
|
|
||||||
await self.next_turn(1, 1)
|
|
||||||
|
|
||||||
@ui.button(label="•", style=ButtonStyle.grey, group=2)
|
|
||||||
async def button_6(self, button: ui.Button,
|
|
||||||
interaction: discord.Interaction):
|
|
||||||
if button.label == "•" and interaction.user == self.get_turn():
|
|
||||||
button.label = ""
|
|
||||||
button.emoji = self.get_emoji()
|
|
||||||
await self.next_turn(1, 2)
|
|
||||||
|
|
||||||
# =========================================================================
|
|
||||||
# =========================================================================
|
|
||||||
|
|
||||||
@ui.button(label="•", style=ButtonStyle.grey, group=3)
|
|
||||||
async def button_7(self, button: ui.Button,
|
|
||||||
interaction: discord.Interaction):
|
|
||||||
if button.label == "•" and interaction.user == self.get_turn():
|
|
||||||
button.label = ""
|
|
||||||
button.emoji = self.get_emoji()
|
|
||||||
await self.next_turn(2, 0)
|
|
||||||
|
|
||||||
@ui.button(label="•", style=ButtonStyle.grey, group=3)
|
|
||||||
async def button_8(self, button: ui.Button,
|
|
||||||
interaction: discord.Interaction):
|
|
||||||
if button.label == "•" and interaction.user == self.get_turn():
|
|
||||||
button.label = ""
|
|
||||||
button.emoji = self.get_emoji()
|
|
||||||
await self.next_turn(2, 1)
|
|
||||||
|
|
||||||
@ui.button(label="•", style=ButtonStyle.grey, group=3)
|
|
||||||
async def button_9(self, button: ui.Button,
|
|
||||||
interaction: discord.Interaction):
|
|
||||||
if button.label == "•" and interaction.user == self.get_turn():
|
|
||||||
button.label = ""
|
|
||||||
button.emoji = self.get_emoji()
|
|
||||||
await self.next_turn(2, 2)
|
|
|
@ -1,18 +0,0 @@
|
||||||
# English translations for Tuxbot-bot package.
|
|
||||||
# Copyright (C) 2020 THE Tuxbot-bot'S COPYRIGHT HOLDER
|
|
||||||
# This file is distributed under the same license as the Tuxbot-bot package.
|
|
||||||
# Automatically generated, 2020.
|
|
||||||
#
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: Tuxbot-bot\n"
|
|
||||||
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
|
|
||||||
"POT-Creation-Date: 2020-10-21 01:15+0200\n"
|
|
||||||
"PO-Revision-Date: 2020-10-21 01:15+0200\n"
|
|
||||||
"Last-Translator: Automatically generated\n"
|
|
||||||
"Language-Team: none\n"
|
|
||||||
"Language: en_US\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
|
|
@ -1,19 +0,0 @@
|
||||||
# French translations for Tuxbot-bot package
|
|
||||||
# Traductions françaises du paquet Tuxbot-bot.
|
|
||||||
# Copyright (C) 2020 THE Tuxbot-bot'S COPYRIGHT HOLDER
|
|
||||||
# This file is distributed under the same license as the Tuxbot-bot package.
|
|
||||||
# Automatically generated, 2020.
|
|
||||||
#
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: Tuxbot-bot\n"
|
|
||||||
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
|
|
||||||
"POT-Creation-Date: 2020-10-21 01:15+0200\n"
|
|
||||||
"PO-Revision-Date: 2020-10-21 01:15+0200\n"
|
|
||||||
"Last-Translator: Automatically generated\n"
|
|
||||||
"Language-Team: none\n"
|
|
||||||
"Language: fr\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
|
|
@ -1,18 +0,0 @@
|
||||||
# SOME DESCRIPTIVE TITLE.
|
|
||||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
|
||||||
# This file is distributed under the same license as the Tuxbot-bot package.
|
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
||||||
#
|
|
||||||
#, fuzzy
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: Tuxbot-bot\n"
|
|
||||||
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
|
|
||||||
"POT-Creation-Date: 2020-10-21 01:15+0200\n"
|
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
||||||
"Language: \n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=CHARSET\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
|
@ -1,19 +0,0 @@
|
||||||
from collections import namedtuple
|
|
||||||
|
|
||||||
from tuxbot.core.bot import Tux
|
|
||||||
from .linux import Linux
|
|
||||||
from .config import LinuxConfig, HAS_MODELS
|
|
||||||
|
|
||||||
VersionInfo = namedtuple("VersionInfo", "major minor micro release_level")
|
|
||||||
version_info = VersionInfo(major=1, minor=0, micro=0, release_level="alpha")
|
|
||||||
|
|
||||||
__version__ = "v{}.{}.{}-{}".format(
|
|
||||||
version_info.major,
|
|
||||||
version_info.minor,
|
|
||||||
version_info.micro,
|
|
||||||
version_info.release_level,
|
|
||||||
).replace("\n", "")
|
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: Tux):
|
|
||||||
bot.add_cog(Linux(bot))
|
|
|
@ -1,12 +0,0 @@
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from structured_config import Structure
|
|
||||||
|
|
||||||
HAS_MODELS = False
|
|
||||||
|
|
||||||
|
|
||||||
class LinuxConfig(Structure):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
extra: Dict[str, Dict] = {}
|
|
|
@ -1,77 +0,0 @@
|
||||||
import asyncio
|
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
|
|
||||||
from tuxbot.cogs.Linux.functions.exceptions import CNFException
|
|
||||||
|
|
||||||
|
|
||||||
def _(x):
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
class CNF:
|
|
||||||
_url = "https://command-not-found.com/{}"
|
|
||||||
_content: BeautifulSoup
|
|
||||||
|
|
||||||
command: str
|
|
||||||
|
|
||||||
description: str = ""
|
|
||||||
meta: dict = {}
|
|
||||||
distro: dict = {}
|
|
||||||
|
|
||||||
def __init__(self, command: str):
|
|
||||||
self.command = command
|
|
||||||
|
|
||||||
# =========================================================================
|
|
||||||
# =========================================================================
|
|
||||||
|
|
||||||
async def fetch(self):
|
|
||||||
try:
|
|
||||||
async with aiohttp.ClientSession() as cs:
|
|
||||||
async with cs.get(self._url.format(self.command)) as s:
|
|
||||||
if s.status == 200:
|
|
||||||
self._content = BeautifulSoup(
|
|
||||||
await s.text(), "html.parser"
|
|
||||||
)
|
|
||||||
return self.parse()
|
|
||||||
|
|
||||||
except (aiohttp.ClientError, asyncio.exceptions.TimeoutError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
raise CNFException(_("Something went wrong ..."))
|
|
||||||
|
|
||||||
def parse(self):
|
|
||||||
info = self._content.find("div", class_="row-command-info")
|
|
||||||
distro = self._content.find_all("div", class_="command-install")
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.description = info.find("p", class_="my-0").text.strip()
|
|
||||||
except AttributeError:
|
|
||||||
self.description = "N/A"
|
|
||||||
|
|
||||||
try:
|
|
||||||
for m in info.find("ul", class_="list-group").find_all("li"):
|
|
||||||
row = m.text.strip().split("\n")
|
|
||||||
|
|
||||||
self.meta[row[0].lower()[:-1]] = row[1]
|
|
||||||
except AttributeError:
|
|
||||||
self.meta = {}
|
|
||||||
|
|
||||||
try:
|
|
||||||
del distro[0] # unused row
|
|
||||||
|
|
||||||
for d in distro:
|
|
||||||
self.distro[
|
|
||||||
d.find("dt").text.strip().split("\n")[-1].strip()
|
|
||||||
] = d.find("code").text
|
|
||||||
except (AttributeError, IndexError):
|
|
||||||
self.distro = {}
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
return {
|
|
||||||
"command": self.command,
|
|
||||||
"description": self.description,
|
|
||||||
"meta": self.meta,
|
|
||||||
"distro": self.distro,
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
from discord.ext import commands
|
|
||||||
|
|
||||||
|
|
||||||
class LinuxException(commands.BadArgument):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class CNFException(LinuxException):
|
|
||||||
pass
|
|
|
@ -1,17 +0,0 @@
|
||||||
from aiocache import cached, Cache
|
|
||||||
from aiocache.serializers import PickleSerializer
|
|
||||||
|
|
||||||
from tuxbot.cogs.Linux.functions.cnf import CNF
|
|
||||||
|
|
||||||
|
|
||||||
@cached(
|
|
||||||
ttl=24 * 3600,
|
|
||||||
serializer=PickleSerializer(),
|
|
||||||
cache=Cache.MEMORY,
|
|
||||||
namespace="linux",
|
|
||||||
)
|
|
||||||
async def get_from_cnf(command: str) -> dict:
|
|
||||||
cnf = CNF(command)
|
|
||||||
await cnf.fetch()
|
|
||||||
|
|
||||||
return cnf.to_dict()
|
|
|
@ -1,56 +0,0 @@
|
||||||
import logging
|
|
||||||
|
|
||||||
import discord
|
|
||||||
from discord.ext import commands
|
|
||||||
|
|
||||||
from tuxbot.cogs.Linux.functions.utils import get_from_cnf
|
|
||||||
from tuxbot.core.utils.functions.extra import command_extra, ContextPlus
|
|
||||||
from tuxbot.core.bot import Tux
|
|
||||||
from tuxbot.core.i18n import (
|
|
||||||
Translator,
|
|
||||||
)
|
|
||||||
|
|
||||||
log = logging.getLogger("tuxbot.cogs.Linux")
|
|
||||||
_ = Translator("Linux", __file__)
|
|
||||||
|
|
||||||
|
|
||||||
class Linux(commands.Cog):
|
|
||||||
def __init__(self, bot: Tux):
|
|
||||||
self.bot = bot
|
|
||||||
|
|
||||||
async def cog_before_invoke(self, ctx: ContextPlus):
|
|
||||||
await ctx.trigger_typing()
|
|
||||||
|
|
||||||
# =========================================================================
|
|
||||||
# =========================================================================
|
|
||||||
|
|
||||||
@command_extra(name="cnf")
|
|
||||||
async def _cnf(self, ctx: ContextPlus, command: str):
|
|
||||||
cnf = await get_from_cnf(command)
|
|
||||||
|
|
||||||
if cnf["distro"]:
|
|
||||||
e = discord.Embed(title=f"{cnf['description']} ({cnf['command']})")
|
|
||||||
|
|
||||||
description = (
|
|
||||||
"__Maintainer:__ {maintainer}\n"
|
|
||||||
"__Homepage:__ [{homepage}]({homepage})\n"
|
|
||||||
"__Section:__ {section}".format(
|
|
||||||
maintainer=cnf["meta"].get("maintainer", "N/A"),
|
|
||||||
homepage=cnf["meta"].get("homepage", "N/A"),
|
|
||||||
section=cnf["meta"].get("section", "N/A"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
e.description = description
|
|
||||||
|
|
||||||
e.set_footer(
|
|
||||||
text="Powered by https://command-not-found.com/ "
|
|
||||||
"and with his authorization"
|
|
||||||
)
|
|
||||||
|
|
||||||
for k, v in cnf["distro"].items():
|
|
||||||
e.add_field(name=f"**__{k}__**", value=f"```{v}```")
|
|
||||||
|
|
||||||
return await ctx.send(embed=e)
|
|
||||||
|
|
||||||
await ctx.send(_("No result found", ctx, self.bot.config))
|
|
|
@ -1,26 +0,0 @@
|
||||||
import logging
|
|
||||||
from collections import namedtuple
|
|
||||||
|
|
||||||
from discord.ext import commands
|
|
||||||
|
|
||||||
from tuxbot.core.bot import Tux
|
|
||||||
from .logs import Logs, GatewayHandler
|
|
||||||
from .config import LogsConfig, HAS_MODELS
|
|
||||||
|
|
||||||
VersionInfo = namedtuple("VersionInfo", "major minor micro release_level")
|
|
||||||
version_info = VersionInfo(major=1, minor=0, micro=0, release_level="alpha")
|
|
||||||
|
|
||||||
__version__ = "v{}.{}.{}-{}".format(
|
|
||||||
version_info.major,
|
|
||||||
version_info.minor,
|
|
||||||
version_info.micro,
|
|
||||||
version_info.release_level,
|
|
||||||
).replace("\n", "")
|
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: Tux):
|
|
||||||
cog = Logs(bot)
|
|
||||||
bot.add_cog(cog)
|
|
||||||
|
|
||||||
handler = GatewayHandler(cog)
|
|
||||||
logging.getLogger().addHandler(handler)
|
|
|
@ -1,27 +0,0 @@
|
||||||
from collections import Counter
|
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
|
|
||||||
def sort_by(_events: Counter) -> Dict[str, dict]:
|
|
||||||
majors = (
|
|
||||||
"guild",
|
|
||||||
"channel",
|
|
||||||
"message",
|
|
||||||
"invite",
|
|
||||||
"integration",
|
|
||||||
"presence",
|
|
||||||
"voice",
|
|
||||||
"other",
|
|
||||||
)
|
|
||||||
sorted_events: Dict[str, Dict] = {m: {} for m in majors}
|
|
||||||
|
|
||||||
for event, count in _events:
|
|
||||||
done = False
|
|
||||||
for m in majors:
|
|
||||||
if event.lower().startswith(m):
|
|
||||||
sorted_events[m][event] = count
|
|
||||||
done = True
|
|
||||||
if not done:
|
|
||||||
sorted_events["other"][event] = count
|
|
||||||
|
|
||||||
return sorted_events
|
|
|
@ -1,26 +0,0 @@
|
||||||
# English translations for Tuxbot-bot package.
|
|
||||||
# Copyright (C) 2020 THE Tuxbot-bot'S COPYRIGHT HOLDER
|
|
||||||
# This file is distributed under the same license as the Tuxbot-bot package.
|
|
||||||
# Automatically generated, 2020.
|
|
||||||
#
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: Tuxbot-bot\n"
|
|
||||||
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
|
|
||||||
"POT-Creation-Date: 2021-01-26 15:18+0100\n"
|
|
||||||
"PO-Revision-Date: 2020-10-21 01:15+0200\n"
|
|
||||||
"Last-Translator: Automatically generated\n"
|
|
||||||
"Language-Team: none\n"
|
|
||||||
"Language: en_US\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Logs/logs.py:295
|
|
||||||
msgid "Sockets stats"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Logs/logs.py:297
|
|
||||||
msgid "{} socket events observed ({:.2f}/minute):"
|
|
||||||
msgstr ""
|
|
|
@ -1,27 +0,0 @@
|
||||||
# French translations for Tuxbot-bot package
|
|
||||||
# Traductions françaises du paquet Tuxbot-bot.
|
|
||||||
# Copyright (C) 2020 THE Tuxbot-bot'S COPYRIGHT HOLDER
|
|
||||||
# This file is distributed under the same license as the Tuxbot-bot package.
|
|
||||||
# Automatically generated, 2020.
|
|
||||||
#
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: Tuxbot-bot\n"
|
|
||||||
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
|
|
||||||
"POT-Creation-Date: 2020-10-21 01:15+0200\n"
|
|
||||||
"PO-Revision-Date: 2020-10-21 01:15+0200\n"
|
|
||||||
"Last-Translator: Automatically generated\n"
|
|
||||||
"Language-Team: none\n"
|
|
||||||
"Language: fr\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Logs/logs.py:295
|
|
||||||
msgid "Sockets stats"
|
|
||||||
msgstr "Statistiques des évenements"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Logs/logs.py:297
|
|
||||||
msgid "{} socket events observed ({:.2f}/minute):"
|
|
||||||
msgstr "{} evenements ont été observés ({:.2f}/minute)"
|
|
|
@ -1,30 +0,0 @@
|
||||||
# SOME DESCRIPTIVE TITLE.
|
|
||||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
|
||||||
# This file is distributed under the same license as the Tuxbot-bot package.
|
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
||||||
#
|
|
||||||
#, fuzzy
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: Tuxbot-bot\n"
|
|
||||||
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
|
|
||||||
"POT-Creation-Date: 2021-05-17 00:04+0200\n"
|
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
||||||
"Language: \n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=CHARSET\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Logs/logs.py:274
|
|
||||||
msgid "```An error occurred, the bot owner has been advertised...```"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Logs/logs.py:334
|
|
||||||
msgid "Sockets stats"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Logs/logs.py:336
|
|
||||||
msgid "{} socket events observed ({:.2f}/minute):"
|
|
||||||
msgstr ""
|
|
|
@ -1,359 +0,0 @@
|
||||||
import asyncio
|
|
||||||
import datetime
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import textwrap
|
|
||||||
import traceback
|
|
||||||
from collections import defaultdict
|
|
||||||
from logging import LogRecord
|
|
||||||
from typing import Any, Dict, List, DefaultDict
|
|
||||||
|
|
||||||
import discord
|
|
||||||
import humanize
|
|
||||||
import psutil
|
|
||||||
import sentry_sdk
|
|
||||||
from discord.ext import commands, tasks
|
|
||||||
from structured_config import ConfigFile
|
|
||||||
|
|
||||||
from tuxbot.core.bot import Tux
|
|
||||||
from tuxbot.core.i18n import (
|
|
||||||
Translator,
|
|
||||||
)
|
|
||||||
from tuxbot.core.utils.functions.extra import (
|
|
||||||
command_extra,
|
|
||||||
ContextPlus,
|
|
||||||
)
|
|
||||||
from tuxbot.core.utils.data_manager import cogs_data_path
|
|
||||||
from .config import LogsConfig
|
|
||||||
from .functions.utils import sort_by
|
|
||||||
from ...core.utils.functions.utils import shorten
|
|
||||||
|
|
||||||
log = logging.getLogger("tuxbot.cogs.Logs")
|
|
||||||
_ = Translator("Logs", __file__)
|
|
||||||
|
|
||||||
|
|
||||||
class GatewayHandler(logging.Handler):
|
|
||||||
def __init__(self, cog):
|
|
||||||
self.cog = cog
|
|
||||||
super().__init__(logging.INFO)
|
|
||||||
|
|
||||||
def filter(self, record: LogRecord):
|
|
||||||
return (
|
|
||||||
record.name == "discord.gateway"
|
|
||||||
or "Shard ID" in record.msg
|
|
||||||
or "Websocket closed " in record.msg
|
|
||||||
)
|
|
||||||
|
|
||||||
def emit(self, record: LogRecord):
|
|
||||||
self.cog.add_record(record)
|
|
||||||
|
|
||||||
|
|
||||||
class Logs(commands.Cog):
|
|
||||||
def __init__(self, bot: Tux):
|
|
||||||
self.bot = bot
|
|
||||||
self.process = psutil.Process()
|
|
||||||
self._batch_lock = asyncio.Lock()
|
|
||||||
self._data_batch: List[Dict[str, Any]] = []
|
|
||||||
self._gateway_queue: asyncio.Queue = asyncio.Queue()
|
|
||||||
self.gateway_worker.start() # pylint: disable=no-member
|
|
||||||
|
|
||||||
self.__config: LogsConfig = ConfigFile(
|
|
||||||
str(cogs_data_path("Logs") / "config.yaml"),
|
|
||||||
LogsConfig,
|
|
||||||
).config
|
|
||||||
|
|
||||||
self._resumes: List[datetime.datetime] = []
|
|
||||||
self._identifies: DefaultDict[Any, list] = defaultdict(list)
|
|
||||||
|
|
||||||
self.old_on_error = bot.on_error
|
|
||||||
bot.on_error = self.on_error
|
|
||||||
|
|
||||||
if self.bot.instance_name != "dev":
|
|
||||||
# pylint: disable=abstract-class-instantiated
|
|
||||||
sentry_sdk.init(
|
|
||||||
dsn=self.__config.sentryKey,
|
|
||||||
traces_sample_rate=1.0,
|
|
||||||
environment=self.bot.instance_name,
|
|
||||||
debug=False,
|
|
||||||
attach_stacktrace=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
def cog_unload(self):
|
|
||||||
self.bot.on_error = self.old_on_error
|
|
||||||
|
|
||||||
async def on_error(self, event, *args, **kwargs):
|
|
||||||
raise # pylint: disable=misplaced-bare-raise
|
|
||||||
|
|
||||||
# =========================================================================
|
|
||||||
# =========================================================================
|
|
||||||
|
|
||||||
def webhook(self, log_type):
|
|
||||||
webhook = discord.Webhook.from_url(
|
|
||||||
getattr(self.__config, log_type),
|
|
||||||
session=self.bot.session,
|
|
||||||
)
|
|
||||||
return webhook
|
|
||||||
|
|
||||||
async def send_guild_stats(self, e, guild):
|
|
||||||
e.add_field(name="Name", value=guild.name)
|
|
||||||
e.add_field(name="ID", value=guild.id)
|
|
||||||
e.add_field(name="Shard ID", value=guild.shard_id or "N/A")
|
|
||||||
e.add_field(
|
|
||||||
name="Owner", value=f"{guild.owner} (ID: {guild.owner.id})"
|
|
||||||
)
|
|
||||||
|
|
||||||
bots = sum(member.bot for member in guild.members)
|
|
||||||
total = guild.member_count
|
|
||||||
online = sum(
|
|
||||||
member.status is discord.Status.online for member in guild.members
|
|
||||||
)
|
|
||||||
|
|
||||||
e.add_field(name="Members", value=str(total))
|
|
||||||
e.add_field(name="Bots", value=f"{bots} ({bots / total:.2%})")
|
|
||||||
e.add_field(name="Online", value=f"{online} ({online / total:.2%})")
|
|
||||||
|
|
||||||
if guild.icon:
|
|
||||||
e.set_thumbnail(url=guild.icon_url)
|
|
||||||
|
|
||||||
if guild.me:
|
|
||||||
e.timestamp = guild.me.joined_at
|
|
||||||
|
|
||||||
await self.webhook("guilds").send(embed=e)
|
|
||||||
|
|
||||||
def add_record(self, record: LogRecord):
|
|
||||||
self._gateway_queue.put_nowait(record)
|
|
||||||
|
|
||||||
async def notify_gateway_status(self, record: LogRecord):
|
|
||||||
types = {"INFO": ":information_source:", "WARNING": ":warning:"}
|
|
||||||
|
|
||||||
emoji = types.get(record.levelname, ":heavy_multiplication_x:")
|
|
||||||
dt = datetime.datetime.utcfromtimestamp(record.created)
|
|
||||||
msg = (
|
|
||||||
f"{emoji} `[{dt:%Y-%m-%d %H:%M:%S}] "
|
|
||||||
f"{await shorten(record.msg, 1500)}`"
|
|
||||||
)
|
|
||||||
await self.webhook("gateway").send(msg)
|
|
||||||
|
|
||||||
def clear_gateway_data(self):
|
|
||||||
one_week_ago = datetime.datetime.utcnow() - datetime.timedelta(days=7)
|
|
||||||
to_remove = [
|
|
||||||
index
|
|
||||||
for index, dt in enumerate(self._resumes)
|
|
||||||
if dt < one_week_ago
|
|
||||||
]
|
|
||||||
for index in reversed(to_remove):
|
|
||||||
del self._resumes[index]
|
|
||||||
|
|
||||||
for _, dates in self._identifies.items():
|
|
||||||
to_remove = [
|
|
||||||
index for index, dt in enumerate(dates) if dt < one_week_ago
|
|
||||||
]
|
|
||||||
for index in reversed(to_remove):
|
|
||||||
del dates[index]
|
|
||||||
|
|
||||||
async def register_command(self, ctx: ContextPlus):
|
|
||||||
if ctx.command is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
command = ctx.command.qualified_name
|
|
||||||
self.bot.stats["commands"][command] += 1
|
|
||||||
message = ctx.message
|
|
||||||
if ctx.guild is None:
|
|
||||||
destination = "Private Message"
|
|
||||||
guild_id = None
|
|
||||||
else:
|
|
||||||
destination = f"#{message.channel} ({message.guild})"
|
|
||||||
guild_id = ctx.guild.id
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
"%s: %s in %s > %s",
|
|
||||||
message.created_at,
|
|
||||||
message.author,
|
|
||||||
destination,
|
|
||||||
message.content,
|
|
||||||
)
|
|
||||||
async with self._batch_lock:
|
|
||||||
self._data_batch.append(
|
|
||||||
{
|
|
||||||
"guild": guild_id,
|
|
||||||
"channel": ctx.channel.id,
|
|
||||||
"author": ctx.author.id,
|
|
||||||
"used": message.created_at.isoformat(),
|
|
||||||
"prefix": ctx.prefix,
|
|
||||||
"command": command,
|
|
||||||
"failed": ctx.command_failed,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# =========================================================================
|
|
||||||
# =========================================================================
|
|
||||||
|
|
||||||
@tasks.loop()
|
|
||||||
async def gateway_worker(self):
|
|
||||||
record = await self._gateway_queue.get()
|
|
||||||
await self.notify_gateway_status(record)
|
|
||||||
|
|
||||||
@commands.Cog.listener()
|
|
||||||
async def on_command_completion(self, ctx: ContextPlus):
|
|
||||||
await self.register_command(ctx)
|
|
||||||
|
|
||||||
@commands.Cog.listener()
|
|
||||||
async def on_socket_response(self, msg):
|
|
||||||
self.bot.stats["socket"][msg.get("t")] += 1
|
|
||||||
|
|
||||||
@commands.Cog.listener()
|
|
||||||
async def on_guild_join(self, guild: discord.guild):
|
|
||||||
e = discord.Embed(colour=0x53DDA4, title="New Guild") # green colour
|
|
||||||
await self.send_guild_stats(e, guild)
|
|
||||||
|
|
||||||
@commands.Cog.listener()
|
|
||||||
async def on_guild_remove(self, guild: discord.guild):
|
|
||||||
e = discord.Embed(colour=0xDD5F53, title="Left Guild") # red colour
|
|
||||||
await self.send_guild_stats(e, guild)
|
|
||||||
|
|
||||||
@commands.Cog.listener()
|
|
||||||
async def on_message(self, message: discord.message):
|
|
||||||
if message.guild is None:
|
|
||||||
e = discord.Embed(colour=0x0A97F5, title="New DM") # blue colour
|
|
||||||
e.set_author(
|
|
||||||
name=message.author,
|
|
||||||
icon_url=message.author.avatar.url,
|
|
||||||
)
|
|
||||||
e.description = message.content
|
|
||||||
if len(message.attachments) > 0:
|
|
||||||
e.set_image(url=message.attachments[0].url)
|
|
||||||
e.set_footer(text=f"User ID: {message.author.id}")
|
|
||||||
await self.webhook("dm").send(embed=e)
|
|
||||||
|
|
||||||
@commands.Cog.listener()
|
|
||||||
async def on_command_error(
|
|
||||||
self, ctx: ContextPlus, error: commands.CommandError
|
|
||||||
):
|
|
||||||
await self.register_command(ctx)
|
|
||||||
if not isinstance(
|
|
||||||
error, (commands.CommandInvokeError, commands.ConversionError)
|
|
||||||
):
|
|
||||||
return
|
|
||||||
|
|
||||||
error = error.original
|
|
||||||
if isinstance(error, (discord.Forbidden, discord.NotFound)):
|
|
||||||
return
|
|
||||||
|
|
||||||
self.bot.console.log(
|
|
||||||
"Command Error, check sentry or discord error channel"
|
|
||||||
)
|
|
||||||
|
|
||||||
e = discord.Embed(title="Command Error", colour=0xCC3366)
|
|
||||||
e.add_field(name="Name", value=ctx.command.qualified_name)
|
|
||||||
e.add_field(name="Author", value=f"{ctx.author} (ID: {ctx.author.id})")
|
|
||||||
|
|
||||||
fmt = f"Channel: {ctx.channel} (ID: {ctx.channel.id})"
|
|
||||||
if ctx.guild:
|
|
||||||
fmt = f"{fmt}\nGuild: {ctx.guild} (ID: {ctx.guild.id})"
|
|
||||||
|
|
||||||
e.add_field(name="Location", value=fmt, inline=False)
|
|
||||||
e.add_field(
|
|
||||||
name="Content",
|
|
||||||
value=textwrap.shorten(ctx.message.content, width=512),
|
|
||||||
)
|
|
||||||
e.add_field(
|
|
||||||
name="Bot Instance",
|
|
||||||
value=self.bot.instance_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
exc = "".join(
|
|
||||||
traceback.format_exception(
|
|
||||||
type(error), error, error.__traceback__, chain=False
|
|
||||||
)
|
|
||||||
)
|
|
||||||
e.description = f"```py\n{exc}\n```"
|
|
||||||
e.timestamp = datetime.datetime.utcnow()
|
|
||||||
await self.webhook("errors").send(embed=e)
|
|
||||||
|
|
||||||
e.description = _(
|
|
||||||
"```An error occurred, the bot owner has been advertised...```",
|
|
||||||
ctx,
|
|
||||||
self.bot.config,
|
|
||||||
)
|
|
||||||
e.remove_field(0)
|
|
||||||
e.remove_field(1)
|
|
||||||
e.remove_field(1)
|
|
||||||
|
|
||||||
if self.bot.instance_name != "dev":
|
|
||||||
sentry_sdk.capture_exception(error)
|
|
||||||
e.set_footer(text=sentry_sdk.last_event_id())
|
|
||||||
|
|
||||||
await ctx.send(embed=e)
|
|
||||||
|
|
||||||
@commands.Cog.listener()
|
|
||||||
async def on_socket_raw_send(self, data):
|
|
||||||
if '"op":2' not in data and '"op":6' not in data:
|
|
||||||
return
|
|
||||||
|
|
||||||
back_to_json = json.loads(data)
|
|
||||||
if back_to_json["op"] == 2:
|
|
||||||
payload = back_to_json["d"]
|
|
||||||
inner_shard = payload.get("shard", [0])
|
|
||||||
self._identifies[inner_shard[0]].append(datetime.datetime.utcnow())
|
|
||||||
else:
|
|
||||||
self._resumes.append(datetime.datetime.utcnow())
|
|
||||||
|
|
||||||
self.clear_gateway_data()
|
|
||||||
|
|
||||||
# =========================================================================
|
|
||||||
# =========================================================================
|
|
||||||
|
|
||||||
@command_extra(name="commandstats", hidden=True, deletable=True)
|
|
||||||
@commands.is_owner()
|
|
||||||
async def _commandstats(self, ctx: ContextPlus, limit=20):
|
|
||||||
counter = self.bot.stats["commands"]
|
|
||||||
width = len(max(counter, key=len)) + 1
|
|
||||||
|
|
||||||
if limit > 0:
|
|
||||||
common = counter.most_common(limit)
|
|
||||||
else:
|
|
||||||
common = counter.most_common()[limit:]
|
|
||||||
|
|
||||||
output = "\n".join(f"{k:<{width}}: {c}" for k, c in common)
|
|
||||||
|
|
||||||
await ctx.send(f"```\n{output}\n```")
|
|
||||||
|
|
||||||
@command_extra(name="socketstats", hidden=True, deletable=True)
|
|
||||||
async def _socketstats(self, ctx: ContextPlus):
|
|
||||||
delta = datetime.datetime.now() - self.bot.uptime
|
|
||||||
minutes = delta.total_seconds() / 60
|
|
||||||
|
|
||||||
counter = self.bot.stats["socket"]
|
|
||||||
if None in counter:
|
|
||||||
counter.pop(None)
|
|
||||||
|
|
||||||
total = sum(self.bot.stats["socket"].values())
|
|
||||||
cpm = total / minutes
|
|
||||||
|
|
||||||
e = discord.Embed(
|
|
||||||
title=_("Sockets stats", ctx, self.bot.config),
|
|
||||||
description=_(
|
|
||||||
"{} socket events observed ({:.2f}/minute):",
|
|
||||||
ctx,
|
|
||||||
self.bot.config,
|
|
||||||
).format(total, cpm),
|
|
||||||
color=discord.colour.Color.green(),
|
|
||||||
)
|
|
||||||
|
|
||||||
for major, events in sort_by(counter.most_common()).items():
|
|
||||||
if events:
|
|
||||||
output = "\n".join(f"{k}: {v}" for k, v in events.items())
|
|
||||||
e.add_field(
|
|
||||||
name=major.capitalize(),
|
|
||||||
value=f"```\n{output}\n```",
|
|
||||||
inline=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
await ctx.send(embed=e)
|
|
||||||
|
|
||||||
@command_extra(name="uptime")
|
|
||||||
async def _uptime(self, ctx: ContextPlus):
|
|
||||||
uptime = humanize.naturaltime(
|
|
||||||
datetime.datetime.now() - self.bot.uptime
|
|
||||||
)
|
|
||||||
await ctx.send(f"Uptime: **{uptime}**")
|
|
|
@ -1,19 +0,0 @@
|
||||||
from collections import namedtuple
|
|
||||||
|
|
||||||
from tuxbot.core.bot import Tux
|
|
||||||
from .mod import Mod
|
|
||||||
from .config import ModConfig, HAS_MODELS
|
|
||||||
|
|
||||||
VersionInfo = namedtuple("VersionInfo", "major minor micro release_level")
|
|
||||||
version_info = VersionInfo(major=1, minor=0, micro=0, release_level="alpha")
|
|
||||||
|
|
||||||
__version__ = "v{}.{}.{}-{}".format(
|
|
||||||
version_info.major,
|
|
||||||
version_info.minor,
|
|
||||||
version_info.micro,
|
|
||||||
version_info.release_level,
|
|
||||||
).replace("\n", "")
|
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: Tux):
|
|
||||||
bot.add_cog(Mod(bot))
|
|
|
@ -1,12 +0,0 @@
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from structured_config import Structure
|
|
||||||
|
|
||||||
HAS_MODELS = True
|
|
||||||
|
|
||||||
|
|
||||||
class ModConfig(Structure):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
extra: Dict[str, Dict] = {}
|
|
|
@ -1,68 +0,0 @@
|
||||||
from discord.ext import commands
|
|
||||||
from discord.ext.commands import Context
|
|
||||||
|
|
||||||
from tuxbot.cogs.Mod.functions.exceptions import (
|
|
||||||
RuleTooLongException,
|
|
||||||
UnknownRuleException,
|
|
||||||
NonMessageException,
|
|
||||||
NonBotMessageException,
|
|
||||||
ReasonTooLongException,
|
|
||||||
)
|
|
||||||
from tuxbot.cogs.Mod.models import Rule
|
|
||||||
|
|
||||||
|
|
||||||
def _(x):
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
class RuleIDConverter(commands.Converter):
|
|
||||||
async def convert(self, ctx: Context, argument: str): # skipcq: PYL-W0613
|
|
||||||
if not argument.isdigit():
|
|
||||||
raise UnknownRuleException(_("Unknown rule"))
|
|
||||||
|
|
||||||
arg = int(argument)
|
|
||||||
|
|
||||||
rule_row = await Rule.get_or_none(server_id=ctx.guild.id, rule_id=arg)
|
|
||||||
|
|
||||||
if not rule_row:
|
|
||||||
raise UnknownRuleException(_("Unknown rule"))
|
|
||||||
|
|
||||||
return arg
|
|
||||||
|
|
||||||
|
|
||||||
class RuleConverter(commands.Converter):
|
|
||||||
async def convert(self, ctx: Context, argument: str): # skipcq: PYL-W0613
|
|
||||||
if len(argument) > 300:
|
|
||||||
raise RuleTooLongException(
|
|
||||||
_("Rule length must be 300 characters or lower.")
|
|
||||||
)
|
|
||||||
|
|
||||||
return argument
|
|
||||||
|
|
||||||
|
|
||||||
class BotMessageConverter(commands.Converter):
|
|
||||||
async def convert(self, ctx: Context, argument: str): # skipcq: PYL-W0613
|
|
||||||
try:
|
|
||||||
m = await commands.MessageConverter().convert(ctx, argument)
|
|
||||||
|
|
||||||
if m.author == ctx.me:
|
|
||||||
return m
|
|
||||||
|
|
||||||
raise NonBotMessageException(_("Please provide one of my message"))
|
|
||||||
except commands.BadArgument:
|
|
||||||
raise NonMessageException(
|
|
||||||
_("Please provide a message in this guild")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ReasonConverter(commands.Converter):
|
|
||||||
async def convert(self, ctx: Context, argument: str): # skipcq: PYL-W0613
|
|
||||||
if argument is None:
|
|
||||||
return f"{ctx.author.display_name} (ID: {ctx.author.id})"
|
|
||||||
|
|
||||||
if len(argument) > 300:
|
|
||||||
raise ReasonTooLongException(
|
|
||||||
_("Reason length must be 300 characters or lower.")
|
|
||||||
)
|
|
||||||
|
|
||||||
return argument
|
|
|
@ -1,25 +0,0 @@
|
||||||
from discord.ext import commands
|
|
||||||
|
|
||||||
|
|
||||||
class ModException(commands.BadArgument):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class RuleTooLongException(ModException):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class UnknownRuleException(ModException):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class NonMessageException(ModException):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class NonBotMessageException(ModException):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ReasonTooLongException(ModException):
|
|
||||||
pass
|
|
|
@ -1,52 +0,0 @@
|
||||||
from typing import Optional, List
|
|
||||||
|
|
||||||
from tuxbot.cogs.Mod.models import MuteRole
|
|
||||||
from tuxbot.cogs.Mod.models.rules import Rule
|
|
||||||
from tuxbot.core.config import set_for_key
|
|
||||||
from tuxbot.core.config import Config
|
|
||||||
|
|
||||||
from tuxbot.core.bot import Tux
|
|
||||||
from tuxbot.core.utils.functions.extra import ContextPlus
|
|
||||||
|
|
||||||
|
|
||||||
async def save_lang(bot: Tux, ctx: ContextPlus, lang: str) -> None:
|
|
||||||
set_for_key(bot.config.Servers, ctx.guild.id, Config.Server, locale=lang)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_server_rules(guild_id: int) -> List[Rule]:
|
|
||||||
return await Rule.filter(server_id=guild_id).all().order_by("rule_id")
|
|
||||||
|
|
||||||
|
|
||||||
def get_most_recent_server_rules(rules: List[Rule]) -> Rule:
|
|
||||||
return sorted(rules, key=lambda r: r.updated_at, reverse=True)[0]
|
|
||||||
|
|
||||||
|
|
||||||
def paginate_server_rules(rules: List[Rule]) -> List[str]:
|
|
||||||
body = [""]
|
|
||||||
|
|
||||||
for rule in rules:
|
|
||||||
if len(body[-1] + format_rule(rule)) > 2000:
|
|
||||||
body.append(format_rule(rule) + "\n")
|
|
||||||
else:
|
|
||||||
body[-1] += format_rule(rule) + "\n"
|
|
||||||
|
|
||||||
return body
|
|
||||||
|
|
||||||
|
|
||||||
def format_rule(rule: Rule) -> str:
|
|
||||||
return f"**{rule.rule_id}** - {rule.content}"
|
|
||||||
|
|
||||||
|
|
||||||
async def get_mute_role(guild_id: int) -> Optional[MuteRole]:
|
|
||||||
return await MuteRole.get_or_none(server_id=guild_id)
|
|
||||||
|
|
||||||
|
|
||||||
async def create_mute_role(guild_id: int, role_id: int) -> MuteRole:
|
|
||||||
role_row = await MuteRole()
|
|
||||||
|
|
||||||
role_row.server_id = guild_id # type: ignore
|
|
||||||
role_row.role_id = role_id # type: ignore
|
|
||||||
|
|
||||||
await role_row.save()
|
|
||||||
|
|
||||||
return role_row
|
|
|
@ -1,100 +0,0 @@
|
||||||
# English translations for Tuxbot-bot package.
|
|
||||||
# Copyright (C) 2020 THE Tuxbot-bot'S COPYRIGHT HOLDER
|
|
||||||
# This file is distributed under the same license as the Tuxbot-bot package.
|
|
||||||
# Automatically generated, 2020.
|
|
||||||
#
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: Tuxbot-bot\n"
|
|
||||||
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
|
|
||||||
"POT-Creation-Date: 2021-01-19 14:42+0100\n"
|
|
||||||
"PO-Revision-Date: 2020-06-10 00:38+0200\n"
|
|
||||||
"Last-Translator: Automatically generated\n"
|
|
||||||
"Language-Team: none\n"
|
|
||||||
"Language: en_US\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:67
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Locale changed to {lang} successfully"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:78
|
|
||||||
msgid "List of available locales: "
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:103
|
|
||||||
msgid ""
|
|
||||||
"{}please read the following rule: \n"
|
|
||||||
"{}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:121
|
|
||||||
msgid "No rules found for this server"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:120
|
|
||||||
msgid "Rules for {}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:126
|
|
||||||
msgid "Latest change: {}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:140
|
|
||||||
msgid "Rules for {} ({}/{})"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:159
|
|
||||||
msgid ""
|
|
||||||
"Following rule added: \n"
|
|
||||||
"{}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:182
|
|
||||||
msgid ""
|
|
||||||
"Following rule updated: \n"
|
|
||||||
"{}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:200
|
|
||||||
msgid ""
|
|
||||||
"Following rule deleted: \n"
|
|
||||||
"{}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:287 tuxbot/cogs/Mod/mod.py:383
|
|
||||||
msgid "Missing members"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:294 tuxbot/cogs/Mod/mod.py:320
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:390
|
|
||||||
msgid "No mute role has been specified for this guild"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:346
|
|
||||||
msgid "Mute role successfully defined"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/functions/converters.py:22
|
|
||||||
msgid "Unknown rule"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/functions/converters.py:31
|
|
||||||
msgid "Rule length must be 300 characters or lower."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/functions/converters.py:50
|
|
||||||
msgid "Please provide one of my message"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/functions/converters.py:53
|
|
||||||
msgid "Please provide a message in this guild"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/functions/converters.py:62
|
|
||||||
msgid "Reason length must be 300 characters or lower."
|
|
||||||
msgstr ""
|
|
|
@ -1,109 +0,0 @@
|
||||||
# French translations for Tuxbot-bot package
|
|
||||||
# Traductions françaises du paquet Tuxbot-bot.
|
|
||||||
# Copyright (C) 2020 THE Tuxbot-bot'S COPYRIGHT HOLDER
|
|
||||||
# This file is distributed under the same license as the Tuxbot-bot package.
|
|
||||||
# Automatically generated, 2020.
|
|
||||||
#
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: Tuxbot-bot\n"
|
|
||||||
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
|
|
||||||
"POT-Creation-Date: 2021-01-19 14:42+0100\n"
|
|
||||||
"PO-Revision-Date: 2020-06-10 00:38+0200\n"
|
|
||||||
"Last-Translator: Automatically generated\n"
|
|
||||||
"Language-Team: none\n"
|
|
||||||
"Language: fr\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:67
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Locale changed to {lang} successfully"
|
|
||||||
msgstr "Langue changée pour {lang} avec succès"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:78
|
|
||||||
msgid "List of available locales: "
|
|
||||||
msgstr "Liste des langues disponibles : "
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:103
|
|
||||||
msgid ""
|
|
||||||
"{}please read the following rule: \n"
|
|
||||||
"{}"
|
|
||||||
msgstr ""
|
|
||||||
"{}merci de lire la règle suivante : \n"
|
|
||||||
"{}"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:121
|
|
||||||
msgid "No rules found for this server"
|
|
||||||
msgstr "Aucune règle trouvée pour ce serveur"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:120
|
|
||||||
msgid "Rules for {}"
|
|
||||||
msgstr "Règles pour {}"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:126
|
|
||||||
msgid "Latest change: {}"
|
|
||||||
msgstr "Dernières modifications : {}"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:140
|
|
||||||
msgid "Rules for {} ({}/{})"
|
|
||||||
msgstr "Règles pour {} ({}/{})"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:159
|
|
||||||
msgid ""
|
|
||||||
"Following rule added: \n"
|
|
||||||
"{}"
|
|
||||||
msgstr ""
|
|
||||||
"La règle suivante a été ajoutée: \n"
|
|
||||||
"{}"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:182
|
|
||||||
msgid ""
|
|
||||||
"Following rule updated: \n"
|
|
||||||
"{}"
|
|
||||||
msgstr ""
|
|
||||||
"La règle suivante a été modifiée: \n"
|
|
||||||
"{}"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:200
|
|
||||||
msgid ""
|
|
||||||
"Following rule deleted: \n"
|
|
||||||
"{}"
|
|
||||||
msgstr ""
|
|
||||||
"La règle suivante a été supprimée: \n"
|
|
||||||
"{}"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:287 tuxbot/cogs/Mod/mod.py:383
|
|
||||||
msgid "Missing members"
|
|
||||||
msgstr "Membres inexistants"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:294 tuxbot/cogs/Mod/mod.py:320
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:390
|
|
||||||
msgid "No mute role has been specified for this guild"
|
|
||||||
msgstr "Aucun rôle mute n'a été spécifié pour ce serveur"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:346
|
|
||||||
msgid "Mute role successfully defined"
|
|
||||||
msgstr "Rôle mute défini avec succès"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/functions/converters.py:22
|
|
||||||
msgid "Unknown rule"
|
|
||||||
msgstr "Règle inconnue"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/functions/converters.py:31
|
|
||||||
msgid "Rule length must be 300 characters or lower."
|
|
||||||
msgstr "La règle doit faire 300 characters ou moins"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/functions/converters.py:50
|
|
||||||
msgid "Please provide one of my message"
|
|
||||||
msgstr "Merci de donner un de mes messages"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/functions/converters.py:53
|
|
||||||
msgid "Please provide a message in this guild"
|
|
||||||
msgstr "Merci de donner un message dans ce serveur"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/functions/converters.py:62
|
|
||||||
msgid "Reason length must be 300 characters or lower."
|
|
||||||
msgstr "La raison doit faire 300 characters ou moins"
|
|
|
@ -1,101 +0,0 @@
|
||||||
# SOME DESCRIPTIVE TITLE.
|
|
||||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
|
||||||
# This file is distributed under the same license as the Tuxbot-bot package.
|
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
||||||
#
|
|
||||||
#, fuzzy
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: Tuxbot-bot\n"
|
|
||||||
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
|
|
||||||
"POT-Creation-Date: 2021-05-17 00:04+0200\n"
|
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
||||||
"Language: \n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=CHARSET\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:81
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Locale changed to {lang} successfully"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:92
|
|
||||||
msgid "List of available locales: "
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:117
|
|
||||||
msgid ""
|
|
||||||
"{}please read the following rule: \n"
|
|
||||||
"{}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:135 tuxbot/cogs/Mod/mod.py:237
|
|
||||||
msgid "No rules found for this server"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:139 tuxbot/cogs/Mod/mod.py:241
|
|
||||||
msgid "Rules for {}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:145 tuxbot/cogs/Mod/mod.py:247
|
|
||||||
msgid "Latest change: {}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:159 tuxbot/cogs/Mod/mod.py:264
|
|
||||||
msgid "Rules for {} ({}/{})"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:180
|
|
||||||
msgid ""
|
|
||||||
"Following rule added: \n"
|
|
||||||
"{}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:203
|
|
||||||
msgid ""
|
|
||||||
"Following rule updated: \n"
|
|
||||||
"{}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:221
|
|
||||||
msgid ""
|
|
||||||
"Following rule deleted: \n"
|
|
||||||
"{}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:287 tuxbot/cogs/Mod/mod.py:383
|
|
||||||
msgid "Missing members"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:294 tuxbot/cogs/Mod/mod.py:320
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:390
|
|
||||||
msgid "No mute role has been specified for this guild"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/mod.py:346
|
|
||||||
msgid "Mute role successfully defined"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/functions/converters.py:21
|
|
||||||
#: tuxbot/cogs/Mod/functions/converters.py:28
|
|
||||||
msgid "Unknown rule"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/functions/converters.py:37
|
|
||||||
msgid "Rule length must be 300 characters or lower."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/functions/converters.py:51
|
|
||||||
msgid "Please provide one of my message"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/functions/converters.py:54
|
|
||||||
msgid "Please provide a message in this guild"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Mod/functions/converters.py:62
|
|
||||||
msgid "Reason length must be 300 characters or lower."
|
|
||||||
msgstr ""
|
|
|
@ -1,402 +0,0 @@
|
||||||
import logging
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import discord
|
|
||||||
from discord.ext import commands
|
|
||||||
|
|
||||||
from tuxbot.cogs.Mod.functions.converters import (
|
|
||||||
RuleConverter,
|
|
||||||
RuleIDConverter,
|
|
||||||
BotMessageConverter,
|
|
||||||
ReasonConverter,
|
|
||||||
)
|
|
||||||
from tuxbot.cogs.Mod.functions.exceptions import (
|
|
||||||
RuleTooLongException,
|
|
||||||
UnknownRuleException,
|
|
||||||
NonMessageException,
|
|
||||||
NonBotMessageException,
|
|
||||||
ReasonTooLongException,
|
|
||||||
)
|
|
||||||
from tuxbot.cogs.Mod.functions.utils import (
|
|
||||||
save_lang,
|
|
||||||
get_server_rules,
|
|
||||||
format_rule,
|
|
||||||
get_most_recent_server_rules,
|
|
||||||
paginate_server_rules,
|
|
||||||
get_mute_role,
|
|
||||||
create_mute_role,
|
|
||||||
)
|
|
||||||
from tuxbot.cogs.Mod.models.rules import Rule
|
|
||||||
from tuxbot.core.utils import checks
|
|
||||||
from tuxbot.core.bot import Tux
|
|
||||||
|
|
||||||
from tuxbot.core.i18n import (
|
|
||||||
Translator,
|
|
||||||
find_locale,
|
|
||||||
get_locale_name,
|
|
||||||
list_locales,
|
|
||||||
)
|
|
||||||
from tuxbot.core.utils.functions.extra import (
|
|
||||||
group_extra,
|
|
||||||
ContextPlus,
|
|
||||||
command_extra,
|
|
||||||
)
|
|
||||||
|
|
||||||
log = logging.getLogger("tuxbot.cogs.Mod")
|
|
||||||
_ = Translator("Mod", __file__)
|
|
||||||
|
|
||||||
|
|
||||||
class Mod(commands.Cog):
|
|
||||||
def __init__(self, bot: Tux):
|
|
||||||
self.bot = bot
|
|
||||||
|
|
||||||
async def cog_command_error(self, ctx: ContextPlus, error):
|
|
||||||
if isinstance(
|
|
||||||
error,
|
|
||||||
(
|
|
||||||
RuleTooLongException,
|
|
||||||
UnknownRuleException,
|
|
||||||
NonMessageException,
|
|
||||||
NonBotMessageException,
|
|
||||||
ReasonTooLongException,
|
|
||||||
),
|
|
||||||
):
|
|
||||||
return await ctx.send(_(str(error), ctx, self.bot.config))
|
|
||||||
|
|
||||||
# =========================================================================
|
|
||||||
# =========================================================================
|
|
||||||
|
|
||||||
@group_extra(name="lang", aliases=["locale", "langue"], deletable=True)
|
|
||||||
@commands.guild_only()
|
|
||||||
@checks.is_admin()
|
|
||||||
async def _lang(self, ctx: ContextPlus):
|
|
||||||
"""Manage lang settings."""
|
|
||||||
|
|
||||||
@_lang.command(name="set", aliases=["define", "choice"])
|
|
||||||
async def _lang_set(self, ctx: ContextPlus, lang: str):
|
|
||||||
try:
|
|
||||||
await save_lang(self.bot, ctx, find_locale(lang.lower()))
|
|
||||||
await ctx.send(
|
|
||||||
_(
|
|
||||||
"Locale changed to {lang} successfully",
|
|
||||||
ctx,
|
|
||||||
self.bot.config,
|
|
||||||
).format(lang=f"`{get_locale_name(lang).lower()}`")
|
|
||||||
)
|
|
||||||
except NotImplementedError:
|
|
||||||
await self._lang_list(ctx)
|
|
||||||
|
|
||||||
@_lang.command(name="list", aliases=["liste", "all", "view"])
|
|
||||||
async def _lang_list(self, ctx: ContextPlus):
|
|
||||||
e = discord.Embed(
|
|
||||||
title=_("List of available locales: ", ctx, self.bot.config),
|
|
||||||
description=list_locales(),
|
|
||||||
color=0x36393E,
|
|
||||||
)
|
|
||||||
|
|
||||||
await ctx.send(embed=e)
|
|
||||||
|
|
||||||
# =========================================================================
|
|
||||||
|
|
||||||
@group_extra(
|
|
||||||
name="rule",
|
|
||||||
aliases=["rules", "regle", "regles"],
|
|
||||||
deletable=False,
|
|
||||||
invoke_without_command=True,
|
|
||||||
)
|
|
||||||
@commands.guild_only()
|
|
||||||
async def _rule(
|
|
||||||
self,
|
|
||||||
ctx: ContextPlus,
|
|
||||||
rule: RuleIDConverter,
|
|
||||||
members: commands.Greedy[discord.Member],
|
|
||||||
):
|
|
||||||
rule_row = await Rule.get(server_id=ctx.guild.id, rule_id=rule)
|
|
||||||
|
|
||||||
message = _(
|
|
||||||
"{}please read the following rule: \n{}", ctx, self.bot.config
|
|
||||||
)
|
|
||||||
authors = ""
|
|
||||||
|
|
||||||
for member in members:
|
|
||||||
if member in ctx.message.mentions:
|
|
||||||
authors += f"{member.name}#{member.discriminator}, "
|
|
||||||
else:
|
|
||||||
authors += f"{member.mention}, "
|
|
||||||
|
|
||||||
await ctx.send(message.format(authors, format_rule(rule_row)))
|
|
||||||
|
|
||||||
@_rule.command(name="list", aliases=["show", "all"])
|
|
||||||
async def _rule_list(self, ctx: ContextPlus):
|
|
||||||
rules = await get_server_rules(ctx.guild.id)
|
|
||||||
|
|
||||||
if not rules:
|
|
||||||
return await ctx.send(
|
|
||||||
_("No rules found for this server", ctx, self.bot.config)
|
|
||||||
)
|
|
||||||
|
|
||||||
embed = discord.Embed(
|
|
||||||
title=_("Rules for {}", ctx, self.bot.config).format(
|
|
||||||
ctx.guild.name
|
|
||||||
),
|
|
||||||
color=discord.Color.blue(),
|
|
||||||
)
|
|
||||||
embed.set_footer(
|
|
||||||
text=_("Latest change: {}", ctx, self.bot.config).format(
|
|
||||||
get_most_recent_server_rules(rules).updated_at.ctime()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
pages = paginate_server_rules(rules)
|
|
||||||
|
|
||||||
if len(pages) == 1:
|
|
||||||
embed.description = pages[0]
|
|
||||||
|
|
||||||
await ctx.send(embed=embed)
|
|
||||||
else:
|
|
||||||
for i, page in enumerate(pages):
|
|
||||||
embed.title = _(
|
|
||||||
"Rules for {} ({}/{})", ctx, self.bot.config
|
|
||||||
).format(ctx.guild.name, str(i + 1), str(len(pages)))
|
|
||||||
embed.description = page
|
|
||||||
|
|
||||||
await ctx.send(embed=embed)
|
|
||||||
|
|
||||||
@checks.is_admin()
|
|
||||||
@_rule.command(name="add")
|
|
||||||
async def _rule_add(self, ctx: ContextPlus, *, rule: RuleConverter):
|
|
||||||
rule_row = await Rule()
|
|
||||||
rule_row.server_id = ctx.guild.id
|
|
||||||
rule_row.author_id = ctx.message.author.id
|
|
||||||
|
|
||||||
rule_row.rule_id = (
|
|
||||||
len(await get_server_rules(ctx.guild.id)) + 1 # type: ignore
|
|
||||||
)
|
|
||||||
rule_row.content = str(rule) # type: ignore
|
|
||||||
|
|
||||||
await rule_row.save()
|
|
||||||
|
|
||||||
await ctx.send(
|
|
||||||
_("Following rule added: \n{}", ctx, self.bot.config).format(
|
|
||||||
format_rule(rule_row)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@checks.is_admin()
|
|
||||||
@_rule.command(name="edit")
|
|
||||||
async def _rule_edit(
|
|
||||||
self,
|
|
||||||
ctx: ContextPlus,
|
|
||||||
rule: RuleIDConverter,
|
|
||||||
*,
|
|
||||||
content: RuleConverter,
|
|
||||||
):
|
|
||||||
# noinspection PyTypeChecker
|
|
||||||
rule_row = await Rule.get(server_id=ctx.guild.id, rule_id=rule)
|
|
||||||
|
|
||||||
rule_row.content = str(content) # type: ignore
|
|
||||||
rule_row.updated_at = datetime.now() # type: ignore
|
|
||||||
|
|
||||||
await rule_row.save()
|
|
||||||
|
|
||||||
await ctx.send(
|
|
||||||
_("Following rule updated: \n{}", ctx, self.bot.config).format(
|
|
||||||
format_rule(rule_row)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@checks.is_admin()
|
|
||||||
@_rule.command(name="delete")
|
|
||||||
async def _rule_delete(
|
|
||||||
self,
|
|
||||||
ctx: ContextPlus,
|
|
||||||
rule: RuleIDConverter,
|
|
||||||
):
|
|
||||||
# noinspection PyTypeChecker
|
|
||||||
rule_row = await Rule.get(server_id=ctx.guild.id, rule_id=rule)
|
|
||||||
|
|
||||||
await rule_row.delete()
|
|
||||||
|
|
||||||
await ctx.send(
|
|
||||||
_("Following rule deleted: \n{}", ctx, self.bot.config).format(
|
|
||||||
format_rule(rule_row)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@checks.is_admin()
|
|
||||||
@_rule.command(name="update")
|
|
||||||
async def _rule_update(
|
|
||||||
self,
|
|
||||||
ctx: ContextPlus,
|
|
||||||
message: BotMessageConverter,
|
|
||||||
):
|
|
||||||
rules = await get_server_rules(ctx.guild.id)
|
|
||||||
|
|
||||||
if not rules:
|
|
||||||
return await ctx.send(
|
|
||||||
_("No rules found for this server", ctx, self.bot.config)
|
|
||||||
)
|
|
||||||
|
|
||||||
embed = discord.Embed(
|
|
||||||
title=_("Rules for {}", ctx, self.bot.config).format(
|
|
||||||
ctx.guild.name
|
|
||||||
),
|
|
||||||
color=discord.Color.blue(),
|
|
||||||
)
|
|
||||||
embed.set_footer(
|
|
||||||
text=_("Latest change: {}", ctx, self.bot.config).format(
|
|
||||||
get_most_recent_server_rules(rules).updated_at.ctime()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
pages = paginate_server_rules(rules)
|
|
||||||
|
|
||||||
# noinspection PyTypeChecker
|
|
||||||
to_edit: discord.Message = message
|
|
||||||
|
|
||||||
if len(pages) == 1:
|
|
||||||
embed.description = pages[0]
|
|
||||||
|
|
||||||
await to_edit.edit(content="", embed=embed)
|
|
||||||
else:
|
|
||||||
for i, page in enumerate(pages):
|
|
||||||
embed.title = _(
|
|
||||||
"Rules for {} ({}/{})", ctx, self.bot.config
|
|
||||||
).format(ctx.guild.name, str(i + 1), str(len(pages)))
|
|
||||||
embed.description = page
|
|
||||||
|
|
||||||
await to_edit.edit(content="", embed=embed)
|
|
||||||
|
|
||||||
# =========================================================================
|
|
||||||
|
|
||||||
@group_extra(
|
|
||||||
name="mute",
|
|
||||||
deletable=True,
|
|
||||||
invoke_without_command=True,
|
|
||||||
)
|
|
||||||
@commands.guild_only()
|
|
||||||
@checks.is_admin()
|
|
||||||
async def _mute(
|
|
||||||
self,
|
|
||||||
ctx: ContextPlus,
|
|
||||||
members: commands.Greedy[discord.Member],
|
|
||||||
*,
|
|
||||||
reason: Optional[ReasonConverter],
|
|
||||||
):
|
|
||||||
if not members:
|
|
||||||
return await ctx.send(_("Missing members", ctx, self.bot.config))
|
|
||||||
|
|
||||||
role_row = await get_mute_role(ctx.guild.id)
|
|
||||||
|
|
||||||
if role_row is None:
|
|
||||||
return await ctx.send(
|
|
||||||
_(
|
|
||||||
"No mute role has been specified for this guild",
|
|
||||||
ctx,
|
|
||||||
self.bot.config,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
for member in members:
|
|
||||||
await member.add_roles(
|
|
||||||
discord.Object(id=int(role_row.role_id)), reason=reason
|
|
||||||
)
|
|
||||||
|
|
||||||
await ctx.send("\N{THUMBS UP SIGN}")
|
|
||||||
|
|
||||||
@_mute.command(name="show", aliases=["role"])
|
|
||||||
async def _mute_show(
|
|
||||||
self,
|
|
||||||
ctx: ContextPlus,
|
|
||||||
):
|
|
||||||
role_row = await get_mute_role(ctx.guild.id)
|
|
||||||
|
|
||||||
if (
|
|
||||||
role_row is None
|
|
||||||
or (role := ctx.guild.get_role(int(role_row.role_id))) is None
|
|
||||||
):
|
|
||||||
return await ctx.send(
|
|
||||||
_(
|
|
||||||
"No mute role has been specified for this guild",
|
|
||||||
ctx,
|
|
||||||
self.bot.config,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
muted_members = [m for m in ctx.guild.members if role in m.roles]
|
|
||||||
|
|
||||||
e = discord.Embed(
|
|
||||||
title=f"Role: {role.name} (ID: {role.id})", color=role.color
|
|
||||||
)
|
|
||||||
e.add_field(name="Total mute:", value=len(muted_members))
|
|
||||||
|
|
||||||
await ctx.send(embed=e)
|
|
||||||
|
|
||||||
@_mute.command(name="set", aliases=["define"])
|
|
||||||
async def _mute_set(self, ctx: ContextPlus, role: discord.Role):
|
|
||||||
role_row = await get_mute_role(ctx.guild.id)
|
|
||||||
|
|
||||||
if role_row is None:
|
|
||||||
await create_mute_role(ctx.guild.id, role.id)
|
|
||||||
else:
|
|
||||||
role_row.role_id = role.id # type: ignore
|
|
||||||
await role_row.save()
|
|
||||||
|
|
||||||
await ctx.send(
|
|
||||||
_("Mute role successfully defined", ctx, self.bot.config)
|
|
||||||
)
|
|
||||||
|
|
||||||
# =========================================================================
|
|
||||||
|
|
||||||
@command_extra(
|
|
||||||
name="tempmute",
|
|
||||||
deletable=True,
|
|
||||||
)
|
|
||||||
@commands.guild_only()
|
|
||||||
@checks.is_admin()
|
|
||||||
async def _tempmute(
|
|
||||||
self,
|
|
||||||
ctx: ContextPlus,
|
|
||||||
time,
|
|
||||||
members: discord.Member,
|
|
||||||
*,
|
|
||||||
reason: Optional[ReasonConverter],
|
|
||||||
):
|
|
||||||
_, _, _, _ = ctx, time, members, reason
|
|
||||||
|
|
||||||
# =========================================================================
|
|
||||||
|
|
||||||
@command_extra(
|
|
||||||
name="unmute",
|
|
||||||
deletable=True,
|
|
||||||
)
|
|
||||||
@commands.guild_only()
|
|
||||||
@checks.is_admin()
|
|
||||||
async def _unmute(
|
|
||||||
self,
|
|
||||||
ctx: ContextPlus,
|
|
||||||
members: commands.Greedy[discord.Member],
|
|
||||||
*,
|
|
||||||
reason: Optional[ReasonConverter],
|
|
||||||
):
|
|
||||||
if not members:
|
|
||||||
return await ctx.send(_("Missing members", ctx, self.bot.config))
|
|
||||||
|
|
||||||
role_row = await get_mute_role(ctx.guild.id)
|
|
||||||
|
|
||||||
if role_row is None:
|
|
||||||
return await ctx.send(
|
|
||||||
_(
|
|
||||||
"No mute role has been specified for this guild",
|
|
||||||
ctx,
|
|
||||||
self.bot.config,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
for member in members:
|
|
||||||
await member.remove_roles(
|
|
||||||
discord.Object(id=int(role_row.role_id)), reason=reason
|
|
||||||
)
|
|
||||||
|
|
||||||
await ctx.send("\N{THUMBS UP SIGN}")
|
|
|
@ -1,3 +0,0 @@
|
||||||
from .rules import *
|
|
||||||
from .warns import *
|
|
||||||
from .mutes import *
|
|
|
@ -1,46 +0,0 @@
|
||||||
import tortoise
|
|
||||||
from tortoise import fields
|
|
||||||
|
|
||||||
|
|
||||||
class MuteRole(tortoise.Model):
|
|
||||||
id = fields.BigIntField(pk=True)
|
|
||||||
server_id = fields.BigIntField()
|
|
||||||
role_id = fields.BigIntField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
table = "mute_role"
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return (
|
|
||||||
f"<MuteRole id={self.id} "
|
|
||||||
f"server_id={self.server_id} "
|
|
||||||
f"role_id={self.role_id}>"
|
|
||||||
)
|
|
||||||
|
|
||||||
__repr__ = __str__
|
|
||||||
|
|
||||||
|
|
||||||
class Mute(tortoise.Model):
|
|
||||||
id = fields.BigIntField(pk=True)
|
|
||||||
server_id = fields.BigIntField()
|
|
||||||
author_id = fields.BigIntField()
|
|
||||||
reason = fields.TextField(max_length=300)
|
|
||||||
member_id = fields.BigIntField()
|
|
||||||
created_at = fields.DatetimeField(auto_now_add=True)
|
|
||||||
expire_at = fields.DatetimeField(null=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
table = "mutes"
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return (
|
|
||||||
f"<Mute id={self.id} "
|
|
||||||
f"server_id={self.server_id} "
|
|
||||||
f"author_id={self.author_id} "
|
|
||||||
f"reason='{self.reason}' "
|
|
||||||
f"member_id={self.member_id} "
|
|
||||||
f"created_at={self.created_at} "
|
|
||||||
f"expire_at={self.expire_at}>"
|
|
||||||
)
|
|
||||||
|
|
||||||
__repr__ = __str__
|
|
|
@ -1,28 +0,0 @@
|
||||||
import tortoise
|
|
||||||
from tortoise import fields
|
|
||||||
|
|
||||||
|
|
||||||
class Rule(tortoise.Model):
|
|
||||||
id = fields.BigIntField(pk=True)
|
|
||||||
server_id = fields.BigIntField()
|
|
||||||
author_id = fields.BigIntField()
|
|
||||||
rule_id = fields.IntField()
|
|
||||||
content = fields.TextField(max_length=300)
|
|
||||||
created_at = fields.DatetimeField(auto_now_add=True)
|
|
||||||
updated_at = fields.DatetimeField(auto_now_add=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
table = "rules"
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return (
|
|
||||||
f"<Rule id={self.id} "
|
|
||||||
f"server_id={self.server_id} "
|
|
||||||
f"author_id={self.author_id} "
|
|
||||||
f"rule_id={self.rule_id} "
|
|
||||||
f"content='{self.content}' "
|
|
||||||
f"created_at={self.created_at} "
|
|
||||||
f"updated_at={self.updated_at}>"
|
|
||||||
)
|
|
||||||
|
|
||||||
__repr__ = __str__
|
|
|
@ -1,24 +0,0 @@
|
||||||
import tortoise
|
|
||||||
from tortoise import fields
|
|
||||||
|
|
||||||
|
|
||||||
class Warn(tortoise.Model):
|
|
||||||
id = fields.BigIntField(pk=True)
|
|
||||||
server_id = fields.BigIntField()
|
|
||||||
user_id = fields.BigIntField()
|
|
||||||
reason = fields.TextField(max_length=255)
|
|
||||||
created_at = fields.DatetimeField()
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
table = "warns"
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return (
|
|
||||||
f"<Warn id={self.id} "
|
|
||||||
f"server_id={self.server_id} "
|
|
||||||
f"user_id={self.user_id} "
|
|
||||||
f"reason='{self.reason}' "
|
|
||||||
f"created_at={self.created_at}>"
|
|
||||||
)
|
|
||||||
|
|
||||||
__repr__ = __str__
|
|
|
@ -1,19 +0,0 @@
|
||||||
from collections import namedtuple
|
|
||||||
|
|
||||||
from tuxbot.core.bot import Tux
|
|
||||||
from .network import Network
|
|
||||||
from .config import NetworkConfig, HAS_MODELS
|
|
||||||
|
|
||||||
VersionInfo = namedtuple("VersionInfo", "major minor micro release_level")
|
|
||||||
version_info = VersionInfo(major=1, minor=0, micro=0, release_level="alpha")
|
|
||||||
|
|
||||||
__version__ = "v{}.{}.{}-{}".format(
|
|
||||||
version_info.major,
|
|
||||||
version_info.minor,
|
|
||||||
version_info.micro,
|
|
||||||
version_info.release_level,
|
|
||||||
).replace("\n", "")
|
|
||||||
|
|
||||||
|
|
||||||
def setup(bot: Tux):
|
|
||||||
bot.add_cog(Network(bot))
|
|
|
@ -1,22 +0,0 @@
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from structured_config import Structure, StrField
|
|
||||||
|
|
||||||
HAS_MODELS = False
|
|
||||||
|
|
||||||
|
|
||||||
class NetworkConfig(Structure):
|
|
||||||
ipinfoKey: str = StrField("")
|
|
||||||
geoapifyKey: str = StrField("")
|
|
||||||
|
|
||||||
|
|
||||||
extra: Dict[str, Dict] = {
|
|
||||||
"ipinfoKey": {
|
|
||||||
"type": str,
|
|
||||||
"description": "API Key for ipinfo.io (.iplocalise command)",
|
|
||||||
},
|
|
||||||
"geoapifyKey": {
|
|
||||||
"type": str,
|
|
||||||
"description": "API Key for geoapify.com (.iplocalise command)",
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -1,55 +0,0 @@
|
||||||
from discord.ext import commands
|
|
||||||
from discord.ext.commands import Context
|
|
||||||
|
|
||||||
|
|
||||||
def _(x):
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
class IPConverter(commands.Converter):
|
|
||||||
async def convert(self, ctx: Context, argument: str): # skipcq: PYL-W0613
|
|
||||||
argument = argument.replace("http://", "").replace("https://", "")
|
|
||||||
argument = argument.rstrip("/")
|
|
||||||
|
|
||||||
if argument.startswith("`") and argument.endswith("`"):
|
|
||||||
argument = argument.lstrip("`").rstrip("`")
|
|
||||||
|
|
||||||
return argument.lower()
|
|
||||||
|
|
||||||
|
|
||||||
class DomainConverter(commands.Converter):
|
|
||||||
async def convert(self, ctx: Context, argument: str): # skipcq: PYL-W0613
|
|
||||||
if not argument.startswith("http"):
|
|
||||||
return f"http://{argument}"
|
|
||||||
|
|
||||||
return argument
|
|
||||||
|
|
||||||
|
|
||||||
class QueryTypeConverter(commands.Converter):
|
|
||||||
async def convert(self, ctx: Context, argument: str): # skipcq: PYL-W0613
|
|
||||||
return argument.lower()
|
|
||||||
|
|
||||||
|
|
||||||
class IPParamsConverter(commands.Converter):
|
|
||||||
async def convert(self, ctx: Context, argument: str): # skipcq: PYL-W0613
|
|
||||||
if not argument:
|
|
||||||
return None
|
|
||||||
|
|
||||||
params = {
|
|
||||||
"inet": "",
|
|
||||||
"map": "map" in argument.lower(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if "4" in argument:
|
|
||||||
params["inet"] = "4"
|
|
||||||
elif "6" in argument:
|
|
||||||
params["inet"] = "6"
|
|
||||||
elif len(arg := argument.split(" ")) >= 2:
|
|
||||||
params["inet"] = arg[0]
|
|
||||||
|
|
||||||
return params
|
|
||||||
|
|
||||||
|
|
||||||
class ASConverter(commands.Converter):
|
|
||||||
async def convert(self, ctx: Context, argument: str): # skipcq: PYL-W0613
|
|
||||||
return argument.lower().lstrip("as")
|
|
|
@ -1,29 +0,0 @@
|
||||||
from discord.ext import commands
|
|
||||||
|
|
||||||
|
|
||||||
class NetworkException(commands.BadArgument):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class RFC18(NetworkException):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidIp(NetworkException):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidDomain(NetworkException):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidQueryType(NetworkException):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class VersionNotFound(NetworkException):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidAsn(NetworkException):
|
|
||||||
pass
|
|
|
@ -1,315 +0,0 @@
|
||||||
import io
|
|
||||||
import socket
|
|
||||||
from typing import NoReturn, Optional, Union
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import re
|
|
||||||
import ipinfo
|
|
||||||
import ipwhois
|
|
||||||
import pydig
|
|
||||||
import aiohttp
|
|
||||||
|
|
||||||
from ipinfo.exceptions import RequestQuotaExceededError
|
|
||||||
|
|
||||||
from ipwhois import Net
|
|
||||||
from ipwhois.asn import IPASN
|
|
||||||
|
|
||||||
from aiocache import cached, Cache
|
|
||||||
from aiocache.serializers import PickleSerializer
|
|
||||||
|
|
||||||
from tuxbot.cogs.Network.functions.exceptions import (
|
|
||||||
VersionNotFound,
|
|
||||||
RFC18,
|
|
||||||
InvalidIp,
|
|
||||||
InvalidQueryType,
|
|
||||||
InvalidAsn,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _(x):
|
|
||||||
return x
|
|
||||||
|
|
||||||
|
|
||||||
@cached(
|
|
||||||
ttl=24 * 3600,
|
|
||||||
serializer=PickleSerializer(),
|
|
||||||
cache=Cache.MEMORY,
|
|
||||||
namespace="network",
|
|
||||||
)
|
|
||||||
async def get_ip(loop, ip: str, inet: Optional[dict]) -> str:
|
|
||||||
_inet: Union[socket.AddressFamily, int] = 0 # pylint: disable=no-member
|
|
||||||
|
|
||||||
if inet:
|
|
||||||
if inet["inet"] == "6":
|
|
||||||
_inet = socket.AF_INET6
|
|
||||||
elif inet["inet"] == "4":
|
|
||||||
_inet = socket.AF_INET
|
|
||||||
|
|
||||||
def _get_ip(_ip: str):
|
|
||||||
try:
|
|
||||||
return socket.getaddrinfo(_ip, None, _inet)[1][4][0]
|
|
||||||
except socket.gaierror as e:
|
|
||||||
raise VersionNotFound(
|
|
||||||
_(
|
|
||||||
"Unable to collect information on this in the given "
|
|
||||||
"version",
|
|
||||||
)
|
|
||||||
) from e
|
|
||||||
|
|
||||||
return await loop.run_in_executor(None, _get_ip, str(ip))
|
|
||||||
|
|
||||||
|
|
||||||
@cached(
|
|
||||||
ttl=24 * 3600,
|
|
||||||
serializer=PickleSerializer(),
|
|
||||||
cache=Cache.MEMORY,
|
|
||||||
namespace="network",
|
|
||||||
)
|
|
||||||
async def get_hostname(loop, ip: str) -> str:
|
|
||||||
def _get_hostname(_ip: str):
|
|
||||||
try:
|
|
||||||
return socket.gethostbyaddr(ip)[0]
|
|
||||||
except socket.herror:
|
|
||||||
return "N/A"
|
|
||||||
|
|
||||||
try:
|
|
||||||
return await asyncio.wait_for(
|
|
||||||
loop.run_in_executor(None, _get_hostname, str(ip)),
|
|
||||||
timeout=0.200,
|
|
||||||
)
|
|
||||||
# assuming that if the hostname isn't retrieved in first .3sec,
|
|
||||||
# it doesn't exists
|
|
||||||
except asyncio.exceptions.TimeoutError:
|
|
||||||
return "N/A"
|
|
||||||
|
|
||||||
|
|
||||||
@cached(
|
|
||||||
ttl=24 * 3600,
|
|
||||||
serializer=PickleSerializer(),
|
|
||||||
cache=Cache.MEMORY,
|
|
||||||
namespace="network",
|
|
||||||
)
|
|
||||||
async def get_ipwhois_result(loop, ip: str) -> Union[NoReturn, dict]:
|
|
||||||
def _get_ipwhois_result(_ip: str) -> Union[NoReturn, dict]:
|
|
||||||
try:
|
|
||||||
net = Net(ip)
|
|
||||||
obj = IPASN(net)
|
|
||||||
return obj.lookup()
|
|
||||||
except ipwhois.exceptions.ASNRegistryError:
|
|
||||||
return {}
|
|
||||||
except ipwhois.exceptions.IPDefinedError as e:
|
|
||||||
raise RFC18(
|
|
||||||
_(
|
|
||||||
"IP address {ip_address} is already defined as Private-Use"
|
|
||||||
" Networks via RFC 1918."
|
|
||||||
)
|
|
||||||
) from e
|
|
||||||
|
|
||||||
try:
|
|
||||||
return await asyncio.wait_for(
|
|
||||||
loop.run_in_executor(None, _get_ipwhois_result, str(ip)),
|
|
||||||
timeout=0.200,
|
|
||||||
)
|
|
||||||
except asyncio.exceptions.TimeoutError:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
@cached(
|
|
||||||
ttl=24 * 3600,
|
|
||||||
serializer=PickleSerializer(),
|
|
||||||
cache=Cache.MEMORY,
|
|
||||||
namespace="network",
|
|
||||||
)
|
|
||||||
async def get_ipinfo_result(loop, apikey: str, ip: str) -> dict:
|
|
||||||
def _get_ipinfo_result(_ip: str) -> Union[NoReturn, dict]:
|
|
||||||
"""
|
|
||||||
Q. Why no getHandlerAsync ?
|
|
||||||
A. Use of this return "Unclosed client session" and "Unclosed connector"
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
handler = ipinfo.getHandler(apikey, request_options={"timeout": 7})
|
|
||||||
return (handler.getDetails(ip)).all
|
|
||||||
except RequestQuotaExceededError:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
try:
|
|
||||||
return await asyncio.wait_for(
|
|
||||||
loop.run_in_executor(None, _get_ipinfo_result, str(ip)),
|
|
||||||
timeout=8,
|
|
||||||
)
|
|
||||||
except asyncio.exceptions.TimeoutError:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
|
|
||||||
@cached(
|
|
||||||
ttl=24 * 3600,
|
|
||||||
serializer=PickleSerializer(),
|
|
||||||
cache=Cache.MEMORY,
|
|
||||||
namespace="network",
|
|
||||||
)
|
|
||||||
async def get_crimeflare_result(ip: str) -> Optional[str]:
|
|
||||||
try:
|
|
||||||
async with aiohttp.ClientSession() as cs:
|
|
||||||
async with cs.post(
|
|
||||||
"http://www.crimeflare.org:82/cgi-bin/cfsearch.cgi",
|
|
||||||
data=f"cfS={ip}",
|
|
||||||
timeout=aiohttp.ClientTimeout(total=21),
|
|
||||||
) as s:
|
|
||||||
result = re.search(r"(\d*\.\d*\.\d*\.\d*)", await s.text())
|
|
||||||
|
|
||||||
if result:
|
|
||||||
return result.group()
|
|
||||||
except (aiohttp.ClientError, asyncio.exceptions.TimeoutError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def merge_ipinfo_ipwhois(ipinfo_result: dict, ipwhois_result: dict) -> dict:
|
|
||||||
output = {
|
|
||||||
"belongs": "N/A",
|
|
||||||
"rir": "N/A",
|
|
||||||
"region": "N/A",
|
|
||||||
"flag": "N/A",
|
|
||||||
"map": "N/A",
|
|
||||||
}
|
|
||||||
|
|
||||||
if ipinfo_result:
|
|
||||||
org = ipinfo_result.get("org", "N/A")
|
|
||||||
asn = org.split()[0] if len(org.split()) > 1 else "N/A"
|
|
||||||
|
|
||||||
output["belongs"] = f"[{org}](https://bgp.he.net/{asn})"
|
|
||||||
output["rir"] = f"```{ipwhois_result.get('asn_registry', 'N/A')}```"
|
|
||||||
output["region"] = (
|
|
||||||
f"```{ipinfo_result.get('city', 'N/A')} - "
|
|
||||||
f"{ipinfo_result.get('region', 'N/A')} "
|
|
||||||
f"({ipinfo_result.get('country', 'N/A')})```"
|
|
||||||
)
|
|
||||||
output[
|
|
||||||
"flag"
|
|
||||||
] = f"https://flagcdn.com/144x108/{ipinfo_result['country'].lower()}.png"
|
|
||||||
output["map"] = ipinfo_result["loc"]
|
|
||||||
elif ipwhois_result:
|
|
||||||
org = ipwhois_result.get("asn_description", "N/A")
|
|
||||||
asn = ipwhois_result.get("asn", "N/A")
|
|
||||||
asn_country = ipwhois_result.get("asn_country_code", "N/A")
|
|
||||||
|
|
||||||
output["belongs"] = f"{org} ([AS{asn}](https://bgp.he.net/{asn}))"
|
|
||||||
output["rir"] = f"```{ipwhois_result['asn_registry']}```"
|
|
||||||
output["region"] = f"```{asn_country}```"
|
|
||||||
output[
|
|
||||||
"flag"
|
|
||||||
] = f"https://flagcdn.com/144x108/{asn_country.lower()}.png"
|
|
||||||
|
|
||||||
return output
|
|
||||||
|
|
||||||
|
|
||||||
@cached(
|
|
||||||
ttl=24 * 3600,
|
|
||||||
serializer=PickleSerializer(),
|
|
||||||
cache=Cache.MEMORY,
|
|
||||||
namespace="network",
|
|
||||||
)
|
|
||||||
async def get_map_bytes(apikey: str, latlon: str) -> Optional[io.BytesIO]:
|
|
||||||
if latlon == "N/A":
|
|
||||||
return None
|
|
||||||
|
|
||||||
url = (
|
|
||||||
"https://maps.geoapify.com/v1/staticmap"
|
|
||||||
"?style=osm-carto"
|
|
||||||
"&width=333"
|
|
||||||
"&height=250"
|
|
||||||
"¢er=lonlat:{lonlat}"
|
|
||||||
"&zoom=12"
|
|
||||||
"&marker=lonlat:{lonlat};color:%23ff0000;size:small"
|
|
||||||
"&apiKey={apikey}"
|
|
||||||
)
|
|
||||||
|
|
||||||
lonlat = ",".join(latlon.split(",")[::-1])
|
|
||||||
|
|
||||||
url = url.format(lonlat=lonlat, apikey=apikey)
|
|
||||||
|
|
||||||
try:
|
|
||||||
async with aiohttp.ClientSession(
|
|
||||||
timeout=aiohttp.ClientTimeout(total=5)
|
|
||||||
) as cs:
|
|
||||||
async with cs.get(url) as s:
|
|
||||||
if s.status != 200:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return io.BytesIO(await s.read())
|
|
||||||
except asyncio.exceptions.TimeoutError:
|
|
||||||
from ..images.load_fail import value
|
|
||||||
|
|
||||||
return io.BytesIO(value)
|
|
||||||
|
|
||||||
|
|
||||||
@cached(
|
|
||||||
ttl=24 * 3600,
|
|
||||||
serializer=PickleSerializer(),
|
|
||||||
cache=Cache.MEMORY,
|
|
||||||
namespace="network",
|
|
||||||
)
|
|
||||||
async def get_pydig_result(
|
|
||||||
loop, domain: str, query_type: str, dnssec: Union[str, bool]
|
|
||||||
) -> list:
|
|
||||||
additional_args = [] if dnssec is False else ["+dnssec"]
|
|
||||||
|
|
||||||
def _get_pydig_result(_domain: str) -> Union[NoReturn, dict]:
|
|
||||||
resolver = pydig.Resolver(
|
|
||||||
nameservers=[
|
|
||||||
"80.67.169.40",
|
|
||||||
"80.67.169.12",
|
|
||||||
],
|
|
||||||
additional_args=additional_args,
|
|
||||||
)
|
|
||||||
|
|
||||||
return resolver.query(_domain, query_type)
|
|
||||||
|
|
||||||
try:
|
|
||||||
return await asyncio.wait_for(
|
|
||||||
loop.run_in_executor(None, _get_pydig_result, str(domain)),
|
|
||||||
timeout=0.500,
|
|
||||||
)
|
|
||||||
except asyncio.exceptions.TimeoutError:
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def check_ip_version_or_raise(
|
|
||||||
version: Optional[dict],
|
|
||||||
) -> Union[bool, NoReturn]:
|
|
||||||
if version is None or version["inet"] in ("4", "6", ""):
|
|
||||||
return True
|
|
||||||
|
|
||||||
raise InvalidIp(_("Invalid ip version"))
|
|
||||||
|
|
||||||
|
|
||||||
def check_query_type_or_raise(query_type: str) -> Union[bool, NoReturn]:
|
|
||||||
query_types = (
|
|
||||||
"a",
|
|
||||||
"aaaa",
|
|
||||||
"cname",
|
|
||||||
"ns",
|
|
||||||
"ds",
|
|
||||||
"dnskey",
|
|
||||||
"soa",
|
|
||||||
"txt",
|
|
||||||
"ptr",
|
|
||||||
"mx",
|
|
||||||
)
|
|
||||||
|
|
||||||
if query_type in query_types:
|
|
||||||
return True
|
|
||||||
|
|
||||||
raise InvalidQueryType(
|
|
||||||
_(
|
|
||||||
"Supported queries : A, AAAA, CNAME, NS, DS, DNSKEY, SOA, TXT, PTR, MX"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def check_asn_or_raise(asn: str) -> Union[bool, NoReturn]:
|
|
||||||
if asn.isdigit() and int(asn) < 4_294_967_295:
|
|
||||||
return True
|
|
||||||
|
|
||||||
raise InvalidAsn(_("Invalid ASN provided"))
|
|
Binary file not shown.
Before Width: | Height: | Size: 3.4 KiB |
File diff suppressed because one or more lines are too long
|
@ -1,79 +0,0 @@
|
||||||
# French translations for Tuxbot-bot package
|
|
||||||
# Traductions françaises du paquet Tuxbot-bot.
|
|
||||||
# Copyright (C) 2020 THE Tuxbot-bot'S COPYRIGHT HOLDER
|
|
||||||
# This file is distributed under the same license as the Tuxbot-bot package.
|
|
||||||
# Automatically generated, 2020.
|
|
||||||
#
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: Tuxbot-bot\n"
|
|
||||||
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
|
|
||||||
"POT-Creation-Date: 2021-01-26 16:12+0100\n"
|
|
||||||
"PO-Revision-Date: 2021-01-19 14:39+0100\n"
|
|
||||||
"Last-Translator: Automatically generated\n"
|
|
||||||
"Language-Team: none\n"
|
|
||||||
"Language: fr\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/functions/converters.py:32
|
|
||||||
#: tuxbot/cogs/Network/functions/converters.py:52
|
|
||||||
msgid "Invalid ip or domain"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/functions/converters.py:64
|
|
||||||
msgid "Invalid domain"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/functions/converters.py:88
|
|
||||||
msgid "Supported queries : A, AAAA, CNAME, NS, DS, DNSKEY, SOA, TXT, PTR, MX"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/functions/converters.py:101
|
|
||||||
msgid "Invalid ip version"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/functions/utils.py:32
|
|
||||||
msgid "Impossible to collect information on this in the given version"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/functions/utils.py:55
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "IP address {ip_address} is already defined as Private-Use Networks via RFC 1918."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/network.py:89
|
|
||||||
msgid "*Retrieving information...*"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/network.py:107
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Information for ``{ip} ({ip_address})``"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/network.py:113
|
|
||||||
msgid "Belongs to:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/network.py:123
|
|
||||||
msgid "Region:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/network.py:131
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Hostname: {hostname}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/network.py:161
|
|
||||||
msgid "[show all]({})"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/network.py:171
|
|
||||||
msgid "Cannot connect to host {}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/network.py:195
|
|
||||||
msgid "No result..."
|
|
||||||
msgstr ""
|
|
|
@ -1,78 +0,0 @@
|
||||||
# French translations for Tuxbot-bot package
|
|
||||||
# Traductions françaises du paquet Tuxbot-bot.
|
|
||||||
# Copyright (C) 2020 THE Tuxbot-bot'S COPYRIGHT HOLDER
|
|
||||||
# This file is distributed under the same license as the Tuxbot-bot package.
|
|
||||||
# Automatically generated, 2020.
|
|
||||||
#
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: Tuxbot-bot\n"
|
|
||||||
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
|
|
||||||
"POT-Creation-Date: 2021-01-26 15:18+0100\n"
|
|
||||||
"PO-Revision-Date: 2021-01-19 14:39+0100\n"
|
|
||||||
"Last-Translator: Automatically generated\n"
|
|
||||||
"Language-Team: none\n"
|
|
||||||
"Language: fr\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/functions/utils.py:39
|
|
||||||
msgid "Invalid ip or domain"
|
|
||||||
msgstr "Nom de domaine ou adresse IP invalide"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/functions/utils.py:62
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Invalid domain"
|
|
||||||
msgstr "Domaine invalide"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/functions/utils.py:135
|
|
||||||
msgid "Supported queries : A, AAAA, CNAME, NS, DS, DNSKEY, SOA, TXT, PTR, MX"
|
|
||||||
msgstr "Requêtes supportées : A, AAAA, CNAME, NS, DS, DNSKEY, SOA, TXT, PTR, MX"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/functions/utils.py:142
|
|
||||||
msgid "Invalid ip version"
|
|
||||||
msgstr "Version d'adresse IP invalide"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/functions/utils.py:164
|
|
||||||
msgid "Impossible to collect information on this in the given version"
|
|
||||||
msgstr "Impossible de collecter des informations pour cette IP avec la version donnée"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/functions/utils.py:164
|
|
||||||
msgid "IP address {ip_address} is already defined as Private-Use Networks via RFC 1918."
|
|
||||||
msgstr "L'adresse ip {ip_address} est est reservée à un usage local selon la RFC 1918"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/network.py:94
|
|
||||||
msgid "*Retrieving information...*"
|
|
||||||
msgstr "*Récupération des informations...*"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/network.py:112
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Information for ``{ip} ({ip_address})``"
|
|
||||||
msgstr "Informations pour ``{ip} ({ip_address})``"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/network.py:118
|
|
||||||
msgid "Belongs to:"
|
|
||||||
msgstr "Appartient à :"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/network.py:128
|
|
||||||
msgid "Region:"
|
|
||||||
msgstr "Région :"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/network.py:136
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Hostname: {hostname}"
|
|
||||||
msgstr "Nom d'hôte : {hostname}"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/network.py:180
|
|
||||||
msgid "[show all]({})"
|
|
||||||
msgstr "[tout afficher]({})"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/network.py:190
|
|
||||||
msgid "Cannot connect to host {}"
|
|
||||||
msgstr "Impossible de se connecter à l'hôte {}"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/network.py:218
|
|
||||||
msgid "No result..."
|
|
||||||
msgstr "Aucun résultat..."
|
|
|
@ -1,90 +0,0 @@
|
||||||
# SOME DESCRIPTIVE TITLE.
|
|
||||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
|
||||||
# This file is distributed under the same license as the Tuxbot-bot package.
|
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
||||||
#
|
|
||||||
#, fuzzy
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: Tuxbot-bot\n"
|
|
||||||
"Report-Msgid-Bugs-To: rick@gnous.eu\n"
|
|
||||||
"POT-Creation-Date: 2021-05-17 00:04+0200\n"
|
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
||||||
"Language: \n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=CHARSET\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/functions/utils.py:54
|
|
||||||
msgid "Unable to collect information on this in the given version"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/functions/utils.py:103
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "IP address {ip_address} is already defined as Private-Use Networks via RFC 1918."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/functions/utils.py:284
|
|
||||||
msgid "Invalid ip version"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/functions/utils.py:306
|
|
||||||
msgid "Supported queries : A, AAAA, CNAME, NS, DS, DNSKEY, SOA, TXT, PTR, MX"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/functions/utils.py:315
|
|
||||||
msgid "Invalid ASN provided"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/network.py:139
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Information for ``{ip} ({ip_address})``"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/network.py:145
|
|
||||||
msgid "Belongs to:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/network.py:155
|
|
||||||
msgid "Region:"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/network.py:163
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "Hostname: {hostname}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/network.py:207
|
|
||||||
msgid "Unable to collect information through CloudFlare"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/network.py:252
|
|
||||||
msgid "[show all]({})"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/network.py:266 tuxbot/cogs/Network/network.py:339
|
|
||||||
msgid "Cannot connect to host {}"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/network.py:291
|
|
||||||
msgid "No result..."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/network.py:323
|
|
||||||
msgid "Up!"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/network.py:326
|
|
||||||
msgid "Down..."
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/network.py:355
|
|
||||||
msgid "Please retry in few minutes"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: tuxbot/cogs/Network/network.py:369
|
|
||||||
#, python-brace-format
|
|
||||||
msgid "AS{asn} could not be found in PeeringDB's database."
|
|
||||||
msgstr ""
|
|
|
@ -1,414 +0,0 @@
|
||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import Optional, Union
|
|
||||||
|
|
||||||
import aiohttp
|
|
||||||
import discord
|
|
||||||
from aiohttp import ClientConnectorError, InvalidURL, TCPConnector
|
|
||||||
from jishaku.models import copy_context_with
|
|
||||||
from discord.ext import commands, tasks
|
|
||||||
from ipinfo.exceptions import RequestQuotaExceededError
|
|
||||||
from structured_config import ConfigFile
|
|
||||||
from tuxbot.cogs.Network.functions.converters import (
|
|
||||||
IPConverter,
|
|
||||||
IPParamsConverter,
|
|
||||||
DomainConverter,
|
|
||||||
QueryTypeConverter,
|
|
||||||
ASConverter,
|
|
||||||
)
|
|
||||||
from tuxbot.cogs.Network.functions.exceptions import (
|
|
||||||
RFC18,
|
|
||||||
InvalidIp,
|
|
||||||
VersionNotFound,
|
|
||||||
InvalidDomain,
|
|
||||||
InvalidQueryType,
|
|
||||||
InvalidAsn,
|
|
||||||
)
|
|
||||||
from tuxbot.core.bot import Tux
|
|
||||||
from tuxbot.core.i18n import (
|
|
||||||
Translator,
|
|
||||||
)
|
|
||||||
from tuxbot.core.utils.data_manager import cogs_data_path
|
|
||||||
from tuxbot.core.utils.functions.extra import (
|
|
||||||
ContextPlus,
|
|
||||||
command_extra,
|
|
||||||
)
|
|
||||||
from tuxbot.core.utils.functions.utils import shorten, str_if_empty
|
|
||||||
from .config import NetworkConfig
|
|
||||||
from .functions.utils import (
|
|
||||||
get_ip,
|
|
||||||
get_hostname,
|
|
||||||
get_crimeflare_result,
|
|
||||||
get_ipinfo_result,
|
|
||||||
get_ipwhois_result,
|
|
||||||
get_map_bytes,
|
|
||||||
get_pydig_result,
|
|
||||||
merge_ipinfo_ipwhois,
|
|
||||||
check_query_type_or_raise,
|
|
||||||
check_ip_version_or_raise,
|
|
||||||
check_asn_or_raise,
|
|
||||||
)
|
|
||||||
|
|
||||||
log = logging.getLogger("tuxbot.cogs.Network")
|
|
||||||
_ = Translator("Network", __file__)
|
|
||||||
|
|
||||||
|
|
||||||
class Network(commands.Cog):
|
|
||||||
_peeringdb_net: Optional[dict]
|
|
||||||
|
|
||||||
def __init__(self, bot: Tux):
|
|
||||||
self.bot = bot
|
|
||||||
self.__config: NetworkConfig = ConfigFile(
|
|
||||||
str(cogs_data_path("Network") / "config.yaml"),
|
|
||||||
NetworkConfig,
|
|
||||||
).config
|
|
||||||
|
|
||||||
self._peeringdb_net = None
|
|
||||||
|
|
||||||
self._update_peering_db.start() # pylint: disable=no-member
|
|
||||||
|
|
||||||
async def cog_command_error(self, ctx: ContextPlus, error):
|
|
||||||
if isinstance(
|
|
||||||
error,
|
|
||||||
(
|
|
||||||
RequestQuotaExceededError,
|
|
||||||
RFC18,
|
|
||||||
InvalidIp,
|
|
||||||
InvalidDomain,
|
|
||||||
InvalidQueryType,
|
|
||||||
VersionNotFound,
|
|
||||||
InvalidAsn,
|
|
||||||
),
|
|
||||||
):
|
|
||||||
await ctx.send(_(str(error), ctx, self.bot.config))
|
|
||||||
|
|
||||||
async def cog_before_invoke(self, ctx: ContextPlus):
|
|
||||||
await ctx.trigger_typing()
|
|
||||||
|
|
||||||
def cog_unload(self):
|
|
||||||
self._update_peering_db.cancel() # pylint: disable=no-member
|
|
||||||
|
|
||||||
@tasks.loop(hours=1.0)
|
|
||||||
async def _update_peering_db(self):
|
|
||||||
try:
|
|
||||||
async with aiohttp.ClientSession(
|
|
||||||
connector=TCPConnector(verify_ssl=False)
|
|
||||||
) as cs:
|
|
||||||
async with cs.get(
|
|
||||||
"https://3.233.208.117/api/net",
|
|
||||||
timeout=aiohttp.ClientTimeout(total=60),
|
|
||||||
) as s:
|
|
||||||
self._peeringdb_net = await s.json()
|
|
||||||
except asyncio.exceptions.TimeoutError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
log.log(logging.INFO, "_update_peering_db")
|
|
||||||
|
|
||||||
# =========================================================================
|
|
||||||
# =========================================================================
|
|
||||||
|
|
||||||
@command_extra(name="iplocalise", aliases=["localiseip"], deletable=True)
|
|
||||||
async def _iplocalise(
|
|
||||||
self,
|
|
||||||
ctx: ContextPlus,
|
|
||||||
ip: IPConverter,
|
|
||||||
*,
|
|
||||||
params: Optional[IPParamsConverter] = None,
|
|
||||||
):
|
|
||||||
# noinspection PyUnresolvedReferences
|
|
||||||
check_ip_version_or_raise(params) # type: ignore
|
|
||||||
|
|
||||||
# noinspection PyUnresolvedReferences
|
|
||||||
ip_address = await get_ip(
|
|
||||||
self.bot.loop, str(ip), params # type: ignore
|
|
||||||
)
|
|
||||||
|
|
||||||
ip_hostname = await get_hostname(self.bot.loop, str(ip_address))
|
|
||||||
|
|
||||||
ipinfo_result = await get_ipinfo_result(
|
|
||||||
self.bot.loop, self.__config.ipinfoKey, ip_address
|
|
||||||
)
|
|
||||||
ipwhois_result = await get_ipwhois_result(self.bot.loop, ip_address)
|
|
||||||
|
|
||||||
merged_results = merge_ipinfo_ipwhois(ipinfo_result, ipwhois_result)
|
|
||||||
|
|
||||||
e = discord.Embed(
|
|
||||||
title=_(
|
|
||||||
"Information for ``{ip} ({ip_address})``", ctx, self.bot.config
|
|
||||||
).format(ip=ip, ip_address=ip_address),
|
|
||||||
color=0x5858D7,
|
|
||||||
)
|
|
||||||
|
|
||||||
e.add_field(
|
|
||||||
name=_("Belongs to:", ctx, self.bot.config),
|
|
||||||
value=merged_results["belongs"],
|
|
||||||
inline=True,
|
|
||||||
)
|
|
||||||
e.add_field(
|
|
||||||
name="RIR :",
|
|
||||||
value=merged_results["rir"],
|
|
||||||
inline=True,
|
|
||||||
)
|
|
||||||
e.add_field(
|
|
||||||
name=_("Region:", ctx, self.bot.config),
|
|
||||||
value=merged_results["region"],
|
|
||||||
inline=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
e.set_thumbnail(url=merged_results["flag"])
|
|
||||||
|
|
||||||
e.set_footer(
|
|
||||||
text=_("Hostname: {hostname}", ctx, self.bot.config).format(
|
|
||||||
hostname=ip_hostname
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
kwargs: dict = {}
|
|
||||||
|
|
||||||
# noinspection PyUnresolvedReferences
|
|
||||||
if (
|
|
||||||
params is not None
|
|
||||||
and params["map"]
|
|
||||||
and ( # type: ignore
|
|
||||||
map_bytes := await get_map_bytes(
|
|
||||||
self.__config.geoapifyKey, merged_results["map"]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
):
|
|
||||||
file = discord.File(map_bytes, "map.png")
|
|
||||||
e.set_image(url="attachment://map.png")
|
|
||||||
|
|
||||||
kwargs["file"] = file
|
|
||||||
|
|
||||||
kwargs["embed"] = e
|
|
||||||
|
|
||||||
return await ctx.send(f"https://ipinfo.io/{ip_address}#", **kwargs)
|
|
||||||
|
|
||||||
@command_extra(
|
|
||||||
name="cloudflare", aliases=["cf", "crimeflare"], deletable=True
|
|
||||||
)
|
|
||||||
async def _cloudflare(
|
|
||||||
self,
|
|
||||||
ctx: ContextPlus,
|
|
||||||
ip: DomainConverter,
|
|
||||||
):
|
|
||||||
crimeflare_result = await get_crimeflare_result(str(ip))
|
|
||||||
|
|
||||||
if crimeflare_result:
|
|
||||||
alt_ctx = await copy_context_with(
|
|
||||||
ctx, content=f"{ctx.prefix}iplocalise {crimeflare_result}"
|
|
||||||
)
|
|
||||||
return await alt_ctx.command.reinvoke(alt_ctx)
|
|
||||||
|
|
||||||
await ctx.send(
|
|
||||||
_(
|
|
||||||
"Unable to collect information through CloudFlare",
|
|
||||||
ctx,
|
|
||||||
self.bot.config,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@command_extra(name="getheaders", aliases=["headers"], deletable=True)
|
|
||||||
async def _getheaders(
|
|
||||||
self, ctx: ContextPlus, ip: DomainConverter, *, user_agent: str = ""
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
headers = {"User-Agent": user_agent}
|
|
||||||
colors = {
|
|
||||||
"1": 0x17A2B8,
|
|
||||||
"2": 0x28A745,
|
|
||||||
"3": 0xFFC107,
|
|
||||||
"4": 0xDC3545,
|
|
||||||
"5": 0x343A40,
|
|
||||||
}
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as cs:
|
|
||||||
async with cs.get(
|
|
||||||
str(ip),
|
|
||||||
headers=headers,
|
|
||||||
timeout=aiohttp.ClientTimeout(total=8),
|
|
||||||
) as s:
|
|
||||||
e = discord.Embed(
|
|
||||||
title=f"Headers : {ip}",
|
|
||||||
color=colors.get(str(s.status)[0], 0x6C757D),
|
|
||||||
)
|
|
||||||
e.add_field(
|
|
||||||
name="Status", value=f"```{s.status}```", inline=True
|
|
||||||
)
|
|
||||||
e.set_thumbnail(url=f"https://http.cat/{s.status}")
|
|
||||||
|
|
||||||
headers = dict(s.headers.items())
|
|
||||||
headers.pop("Set-Cookie", headers)
|
|
||||||
|
|
||||||
fail = False
|
|
||||||
|
|
||||||
for key, value in headers.items():
|
|
||||||
fail, output = await shorten(value, 50, fail)
|
|
||||||
|
|
||||||
if output["link"]:
|
|
||||||
value = _(
|
|
||||||
"[show all]({})", ctx, self.bot.config
|
|
||||||
).format(output["link"])
|
|
||||||
else:
|
|
||||||
value = f"```\n{output['text']}```"
|
|
||||||
|
|
||||||
e.add_field(name=key, value=value, inline=True)
|
|
||||||
|
|
||||||
await ctx.send(embed=e)
|
|
||||||
except (
|
|
||||||
ClientConnectorError,
|
|
||||||
InvalidURL,
|
|
||||||
asyncio.exceptions.TimeoutError,
|
|
||||||
):
|
|
||||||
await ctx.send(
|
|
||||||
_("Cannot connect to host {}", ctx, self.bot.config).format(ip)
|
|
||||||
)
|
|
||||||
|
|
||||||
@command_extra(name="dig", deletable=True)
|
|
||||||
async def _dig(
|
|
||||||
self,
|
|
||||||
ctx: ContextPlus,
|
|
||||||
domain: IPConverter,
|
|
||||||
query_type: QueryTypeConverter,
|
|
||||||
dnssec: Union[str, bool] = False,
|
|
||||||
):
|
|
||||||
check_query_type_or_raise(str(query_type))
|
|
||||||
|
|
||||||
pydig_result = await get_pydig_result(
|
|
||||||
self.bot.loop, str(domain), str(query_type), dnssec
|
|
||||||
)
|
|
||||||
|
|
||||||
e = discord.Embed(title=f"DIG {domain} {query_type}", color=0x5858D7)
|
|
||||||
|
|
||||||
for i, value in enumerate(pydig_result):
|
|
||||||
e.add_field(name=f"#{i}", value=f"```{value}```")
|
|
||||||
|
|
||||||
if not pydig_result:
|
|
||||||
e.add_field(
|
|
||||||
name=f"DIG {domain} IN {query_type}",
|
|
||||||
value=_("No result...", ctx, self.bot.config),
|
|
||||||
)
|
|
||||||
|
|
||||||
await ctx.send(embed=e)
|
|
||||||
|
|
||||||
@command_extra(name="ping", deletable=True)
|
|
||||||
async def _ping(self, ctx: ContextPlus):
|
|
||||||
start = time.perf_counter()
|
|
||||||
await ctx.trigger_typing()
|
|
||||||
end = time.perf_counter()
|
|
||||||
|
|
||||||
latency = round(self.bot.latency * 1000, 2)
|
|
||||||
typing = round((end - start) * 1000, 2)
|
|
||||||
|
|
||||||
e = discord.Embed(title="Ping", color=discord.Color.teal())
|
|
||||||
e.add_field(name="Websocket", value=f"{latency}ms")
|
|
||||||
e.add_field(name="Typing", value=f"{typing}ms")
|
|
||||||
await ctx.send(embed=e)
|
|
||||||
|
|
||||||
@command_extra(name="isdown", aliases=["is_down", "down?"], deletable=True)
|
|
||||||
async def _isdown(self, ctx: ContextPlus, domain: IPConverter):
|
|
||||||
try:
|
|
||||||
url = f"https://www.isthissitedown.org/site/{domain}"
|
|
||||||
|
|
||||||
async with aiohttp.ClientSession() as cs:
|
|
||||||
async with cs.get(
|
|
||||||
url,
|
|
||||||
timeout=aiohttp.ClientTimeout(total=8),
|
|
||||||
) as s:
|
|
||||||
text = await s.text()
|
|
||||||
|
|
||||||
if "is up!" in text:
|
|
||||||
title = _("Up!", ctx, self.bot.config)
|
|
||||||
color = 0x28A745
|
|
||||||
else:
|
|
||||||
title = _("Down...", ctx, self.bot.config)
|
|
||||||
color = 0xDC3545
|
|
||||||
|
|
||||||
e = discord.Embed(title=title, color=color)
|
|
||||||
|
|
||||||
await ctx.send(url, embed=e)
|
|
||||||
|
|
||||||
except (
|
|
||||||
ClientConnectorError,
|
|
||||||
InvalidURL,
|
|
||||||
asyncio.exceptions.TimeoutError,
|
|
||||||
):
|
|
||||||
await ctx.send(
|
|
||||||
_("Cannot connect to host {}", ctx, self.bot.config).format(
|
|
||||||
domain
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@command_extra(
|
|
||||||
name="peeringdb", aliases=["peer", "peering"], deletable=True
|
|
||||||
)
|
|
||||||
async def _peeringdb(self, ctx: ContextPlus, asn: ASConverter):
|
|
||||||
check_asn_or_raise(str(asn))
|
|
||||||
|
|
||||||
data = {}
|
|
||||||
|
|
||||||
if self._peeringdb_net is None:
|
|
||||||
return await ctx.send(
|
|
||||||
_(
|
|
||||||
"Please retry in few minutes",
|
|
||||||
ctx,
|
|
||||||
self.bot.config,
|
|
||||||
).format(asn=asn)
|
|
||||||
)
|
|
||||||
|
|
||||||
for _data in self._peeringdb_net["data"]:
|
|
||||||
if _data.get("asn", None) == int(str(asn)):
|
|
||||||
data = _data
|
|
||||||
break
|
|
||||||
|
|
||||||
if not data:
|
|
||||||
return await ctx.send(
|
|
||||||
_(
|
|
||||||
"AS{asn} could not be found in PeeringDB's database.",
|
|
||||||
ctx,
|
|
||||||
self.bot.config,
|
|
||||||
).format(asn=asn)
|
|
||||||
)
|
|
||||||
|
|
||||||
filtered = {
|
|
||||||
"info_type": "Type",
|
|
||||||
"info_traffic": "Traffic",
|
|
||||||
"info_ratio": "Ratio",
|
|
||||||
"info_prefixes4": "Prefixes IPv4",
|
|
||||||
"info_prefixes6": "Prefixes IPv6",
|
|
||||||
}
|
|
||||||
filtered_link = {
|
|
||||||
"website": ("Site", "website"),
|
|
||||||
"looking_glass": ("Looking Glass", "looking_glass"),
|
|
||||||
"policy_general": ("Peering", "policy_url"),
|
|
||||||
}
|
|
||||||
|
|
||||||
e = discord.Embed(
|
|
||||||
title=f"{data['name']} ({str_if_empty(data['aka'], f'AS{asn}')})",
|
|
||||||
color=0x5858D7,
|
|
||||||
)
|
|
||||||
|
|
||||||
for key, name in filtered.items():
|
|
||||||
e.add_field(
|
|
||||||
name=name, value=f"```{str_if_empty(data.get(key), 'N/A')}```"
|
|
||||||
)
|
|
||||||
|
|
||||||
for key, names in filtered_link.items():
|
|
||||||
if data.get(key):
|
|
||||||
e.add_field(
|
|
||||||
name=names[0],
|
|
||||||
value=f"[{str_if_empty(data.get(key), 'N/A')}]"
|
|
||||||
f"({str_if_empty(data.get(names[1]), 'N/A')})",
|
|
||||||
)
|
|
||||||
|
|
||||||
if data["notes"]:
|
|
||||||
output = (await shorten(data["notes"], 550))[1]
|
|
||||||
e.description = output["text"]
|
|
||||||
if data["created"]:
|
|
||||||
e.timestamp = datetime.strptime(
|
|
||||||
data["created"], "%Y-%m-%dT%H:%M:%SZ"
|
|
||||||
)
|
|
||||||
|
|
||||||
await ctx.send(f"https://www.peeringdb.com/net/{data['id']}", embed=e)
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue