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
|
15
.gitignore
vendored
15
.gitignore
vendored
|
@ -33,20 +33,7 @@ __pycache__/
|
|||
__pypackages__/
|
||||
|
||||
venv
|
||||
venv3.8
|
||||
venv3.9
|
||||
venv3.11
|
||||
dist
|
||||
build
|
||||
*.egg
|
||||
*.egg-info
|
||||
|
||||
|
||||
.ipython/
|
||||
.env
|
||||
.envs/*
|
||||
!.envs/.local/
|
||||
|
||||
|
||||
data/settings/
|
||||
dump.rdb
|
||||
*.egg-info
|
|
@ -1,70 +1,44 @@
|
|||
<component name="ProjectDictionaryState">
|
||||
<dictionary name="romain">
|
||||
<words>
|
||||
<w>aaaa</w>
|
||||
<w>ajout</w>
|
||||
<w>anglais</w>
|
||||
<w>anonyme</w>
|
||||
<w>appdirs</w>
|
||||
<w>apres</w>
|
||||
<w>asctime</w>
|
||||
<w>commandstats</w>
|
||||
<w>crimeflare</w>
|
||||
<w>ctype</w>
|
||||
<w>debian</w>
|
||||
<w>dnskey</w>
|
||||
<w>découverte</w>
|
||||
<w>ffff</w>
|
||||
<w>fonction</w>
|
||||
<w>francais</w>
|
||||
<w>français</w>
|
||||
<w>gitea</w>
|
||||
<w>gnous</w>
|
||||
<w>ipinfo</w>
|
||||
<w>iplocalise</w>
|
||||
<w>ipwhois</w>
|
||||
<w>jishaku</w>
|
||||
<w>langue</w>
|
||||
<w>latlon</w>
|
||||
<w>levelname</w>
|
||||
<w>liste</w>
|
||||
<w>localiseip</w>
|
||||
<w>lundi</w>
|
||||
<w>octobre</w>
|
||||
<w>outout</w>
|
||||
<w>outoutxyz</w>
|
||||
<w>outouxyz</w>
|
||||
<w>pacman</w>
|
||||
<w>peeringdb</w>
|
||||
<w>perso</w>
|
||||
<w>postgre</w>
|
||||
<w>postgresql</w>
|
||||
<w>pred</w>
|
||||
<w>pydig</w>
|
||||
<w>pylint</w>
|
||||
<w>regle</w>
|
||||
<w>regles</w>
|
||||
<w>releaselevel</w>
|
||||
<w>rprint</w>
|
||||
<w>skipcq</w>
|
||||
<w>socketstats</w>
|
||||
<w>soit</w>
|
||||
<w>sondage</w>
|
||||
<w>sondages</w>
|
||||
<w>splt</w>
|
||||
<w>suivante</w>
|
||||
<w>systemd</w>
|
||||
<w>tablename</w>
|
||||
<w>tempmute</w>
|
||||
<w>tldr</w>
|
||||
<w>tutux</w>
|
||||
<w>tuxbot</w>
|
||||
<w>tuxbot's</w>
|
||||
<w>tuxvenv</w>
|
||||
<w>venv</w>
|
||||
<w>webhook</w>
|
||||
<w>webhooks</w>
|
||||
<w>youtrack</w>
|
||||
<w>écrite</w>
|
||||
</words>
|
||||
</dictionary>
|
||||
|
|
|
@ -3,5 +3,5 @@
|
|||
<component name="JavaScriptSettings">
|
||||
<option name="languageLevel" value="ES6" />
|
||||
</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>
|
|
@ -2,7 +2,7 @@
|
|||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<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>
|
||||
</component>
|
||||
</project>
|
|
@ -5,13 +5,8 @@
|
|||
<excludeFolder url="file://$MODULE_DIR$/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/dist" />
|
||||
<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>
|
||||
<orderEntry type="jdk" jdkName="Python 3.10 (tuxbot_bot)" jdkType="Python SDK" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<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
|
||||
k, # for k, v in
|
||||
v, # for k, v in
|
||||
dt, # datetime
|
||||
|
||||
[MASTER]
|
||||
disable=
|
||||
C0103, # invalid-name
|
||||
C0114, # missing-module-docstring
|
||||
C0115, # missing-class-docstring
|
||||
C0116, # missing-function-docstring
|
||||
C0415, # import-outside-toplevel
|
||||
W0703, # broad-except
|
||||
W0707, # raise-missing-from
|
||||
R0801, # duplicate-code
|
||||
R0901, # too-many-ancestors
|
||||
R0902, # too-many-instance-attributes
|
||||
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)
|
||||
DOCKER_LOCAL := docker-compose -f production.yml
|
||||
else
|
||||
DOCKER_LOCAL := docker-compose -f local.yml
|
||||
endif
|
||||
PYTHON = python
|
||||
VENV = venv
|
||||
|
||||
INSTANCE := preprod
|
||||
|
||||
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'
|
||||
XGETTEXT_FLAGS = --no-wrap --language='python' --keyword=_ --from-code='UTF-8' --msgid-bugs-address='rick@gnous.eu' --width=79 --package-name='Tuxbot-bot'
|
||||
|
||||
# Init
|
||||
.PHONY: main
|
||||
main:
|
||||
$(VIRTUAL_ENV)/bin/pip install -U pip setuptools
|
||||
|
||||
.PHONY: install
|
||||
$(PYTHON) -m venv --clear $(VENV)
|
||||
$(VENV)/bin/pip install -U pip setuptools
|
||||
install:
|
||||
$(VIRTUAL_ENV)/bin/pip install .
|
||||
|
||||
.PHONY: install-dev
|
||||
install-dev:
|
||||
$(VIRTUAL_ENV)/bin/pip install -r dev.requirements.txt
|
||||
|
||||
.PHONY: update
|
||||
$(VENV)/bin/pip install .
|
||||
update:
|
||||
$(VIRTUAL_ENV)/bin/pip install --upgrade .
|
||||
|
||||
.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
|
||||
|
||||
$(VENV)/bin/pip install -U .
|
||||
|
||||
# Blackify code
|
||||
.PHONY: black
|
||||
black:
|
||||
$(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
|
||||
reformat:
|
||||
$(PYTHON) -m black `git ls-files "*.py"` --line-length=79 && pylint tuxbot
|
||||
|
||||
# Translations
|
||||
.PHONY: xgettext
|
||||
xgettext:
|
||||
for cog in tuxbot/cogs/*/; do \
|
||||
xgettext `find $$cog -type f -name '*.py'` --output=$$cog/locales/messages.pot $(XGETTEXT_FLAGS); \
|
||||
done
|
||||
|
||||
.PHONY: msginit
|
||||
msginit:
|
||||
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/en-US.po --locale=en_US.UTF-8 --no-translator; \
|
||||
done
|
||||
|
||||
.PHONY: msgmerge
|
||||
msgmerge:
|
||||
for cog in tuxbot/cogs/*/; do \
|
||||
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)
|
||||
:language: bash
|
||||
|
@ -14,7 +14,7 @@ Installing the pre-requirements
|
|||
|
||||
- The pre-requirements are:
|
||||
|
||||
- Python 3.8 or greater
|
||||
- Python 3.7 or greater
|
||||
- Pip
|
||||
- Git
|
||||
|
||||
|
@ -26,9 +26,9 @@ Arch Linux
|
|||
|
||||
.. 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
|
||||
|
||||
$ 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>`__.
|
||||
|
||||
--------------
|
||||
|
||||
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>`__.
|
||||
Continue to `create the venv <#creating-the-virtual-environment>`__.
|
||||
|
||||
--------------
|
||||
|
||||
|
@ -61,43 +49,6 @@ Windows
|
|||
|
||||
*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
|
||||
--------------------------------
|
||||
|
||||
|
@ -110,7 +61,7 @@ two commands:
|
|||
$ make install
|
||||
|
||||
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
|
||||
-------------
|
||||
|
@ -118,12 +69,12 @@ Configuration
|
|||
It's time to set up your first instance, to do this, you can simply
|
||||
execute this command:
|
||||
|
||||
:bash:`tuxbot-setup`
|
||||
:bash:`tuxbot-setup [your instance name]`
|
||||
|
||||
After following the instructions, you can run your instance by executing
|
||||
this command:
|
||||
|
||||
:bash:`tuxbot`
|
||||
:bash:`tuxbot [your instance name]`
|
||||
|
||||
Update
|
||||
------
|
||||
|
@ -134,8 +85,5 @@ To update the whole bot after a :bash:`git pull`, just execute
|
|||
|
||||
$ make update
|
||||
|
||||
.. |image0| image:: https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10-%23007ec6
|
||||
.. |image1| image:: https://img.shields.io/github/issues/Rom1-J/tuxbot-bot
|
||||
.. |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
|
||||
.. |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/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
|
|
@ -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
|
27
setup.cfg
27
setup.cfg
|
@ -1,7 +1,7 @@
|
|||
[metadata]
|
||||
name = Tuxbot-bot
|
||||
version = attr: tuxbot.__version__
|
||||
url = https://github.com/Rom1-J/tuxbot-bot/
|
||||
url = https://git.gnous.eu/gnouseu/tuxbot-bot/
|
||||
author = Romain J.
|
||||
author_email = romain@gnous.eu
|
||||
maintainer = Romain J.
|
||||
|
@ -13,25 +13,18 @@ platforms = linux
|
|||
|
||||
[options]
|
||||
packages = find_namespace:
|
||||
python_requires = >=3.8
|
||||
python_requires = >=3.7
|
||||
install_requires =
|
||||
aiocache>=0.11.1
|
||||
asyncpg>=0.21.0
|
||||
appdirs>=1.4.4
|
||||
Babel>=2.8.0
|
||||
beautifulsoup4>=4.9.3
|
||||
discord.py @ git+https://github.com/Rapptz/discord.py
|
||||
discord-ext-menus
|
||||
humanize>=2.6.0
|
||||
ipinfo>=4.1.0
|
||||
ipwhois>=1.2.0
|
||||
jishaku @ git+https://github.com/Gorialis/jishaku
|
||||
black==20.8b1
|
||||
discord.py==1.5.0
|
||||
discord_flags==2.1.1
|
||||
humanize==2.6.0
|
||||
jishaku>=1.19.1.200
|
||||
psutil>=5.7.2
|
||||
pydig>=0.3.0
|
||||
; ralgo @ git+https://github.com/Rom1-J/ralgo
|
||||
rich>=9.10.0
|
||||
sentry_sdk>=0.20.2
|
||||
rich>=6.0.0
|
||||
structured_config>=4.12
|
||||
tortoise-orm>=0.16.17
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
|
@ -48,4 +41,4 @@ include =
|
|||
locales/*.po
|
||||
**/locales/*.po
|
||||
data/*
|
||||
data/**/*
|
||||
data/**/*
|
2
setup.py
2
setup.py
|
@ -1,5 +1,5 @@
|
|||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
python_requires=">=3.8",
|
||||
python_requires=">=3.7",
|
||||
)
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import os
|
||||
from collections import namedtuple
|
||||
|
||||
build = os.popen("/usr/bin/git rev-parse --short HEAD").read().strip()
|
||||
info = os.popen('/usr/bin/git log -n 3 -s --format="%s"').read().strip()
|
||||
build = os.popen("git rev-parse --short HEAD").read().strip()
|
||||
info = os.popen('git log -n 1 -s --format="%s"').read().strip()
|
||||
|
||||
VersionInfo = namedtuple(
|
||||
"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.core.utils.console import console
|
||||
|
||||
console = Console()
|
||||
install(console=console)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
def main() -> NoReturn:
|
||||
try:
|
||||
from .__run__ import run # pylint: disable=import-outside-toplevel
|
||||
|
||||
run()
|
||||
except SystemExit as exc:
|
||||
if exc.code == ExitCodes.RESTART:
|
||||
sys.exit(exc.code)
|
||||
# reimport to load changes
|
||||
from .__run__ import run # pylint: disable=import-outside-toplevel
|
||||
|
||||
run()
|
||||
else:
|
||||
raise exc
|
||||
except Exception:
|
||||
console.print_exception(
|
||||
show_locals=True, word_wrap=True, extra_lines=5
|
||||
)
|
||||
console.print_exception()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except Exception:
|
||||
console.print_exception(
|
||||
show_locals=True, word_wrap=True, extra_lines=5
|
||||
)
|
||||
console.print_exception()
|
||||
|
|
|
@ -4,34 +4,80 @@ import logging
|
|||
import signal
|
||||
import sys
|
||||
import os
|
||||
import tracemalloc
|
||||
from argparse import Namespace
|
||||
from typing import NoReturn
|
||||
from datetime import datetime
|
||||
|
||||
import discord
|
||||
import humanize
|
||||
import pip
|
||||
from rich.columns import Columns
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.traceback import install
|
||||
from rich.table import Table, box
|
||||
from rich.text import Text
|
||||
from rich import print as rprint
|
||||
|
||||
import tuxbot.logging
|
||||
from tuxbot.core.bot import Tux
|
||||
from tuxbot.core.utils import data_manager
|
||||
from tuxbot.core.utils.console import console
|
||||
from tuxbot.core import data_manager
|
||||
from tuxbot.core import config
|
||||
from . import __version__, version_info, ExitCodes
|
||||
|
||||
log = logging.getLogger("tuxbot.main")
|
||||
|
||||
console = Console()
|
||||
install(console=console)
|
||||
tracemalloc.start()
|
||||
|
||||
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"""
|
||||
python_version = sys.version.replace("\n", "")
|
||||
pip_version = pip.__version__
|
||||
tuxbot_version = __version__
|
||||
dpy_version = discord.__version__
|
||||
|
||||
uptime = os.popen("/usr/bin/uptime").read().strip().split()
|
||||
uptime = os.popen("uptime").read().strip().split()
|
||||
|
||||
console.print(
|
||||
Panel("[bold blue]Debug Info", style="blue"), justify="center"
|
||||
|
@ -95,7 +141,7 @@ def parse_cli_flags(args: list) -> Namespace:
|
|||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Tuxbot - OpenSource bot",
|
||||
usage="tuxbot [arguments]",
|
||||
usage="tuxbot <instance_name> [arguments]",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
|
@ -106,14 +152,27 @@ def parse_cli_flags(args: list) -> Namespace:
|
|||
parser.add_argument(
|
||||
"--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(
|
||||
"--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
|
||||
|
||||
It cancels all running task.
|
||||
|
@ -154,7 +213,7 @@ async def run_bot(tux: Tux, cli_flags: Namespace) -> None:
|
|||
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")
|
||||
|
||||
|
@ -173,9 +232,9 @@ async def run_bot(tux: Tux, cli_flags: Namespace) -> None:
|
|||
try:
|
||||
await tux.load_packages()
|
||||
console.print()
|
||||
await tux.start(token=token)
|
||||
await tux.start(token=token, bot=True)
|
||||
except discord.LoginFailure:
|
||||
log.critical("This token appears to be invalid.")
|
||||
log.critical("This token appears to be valid.")
|
||||
console.print()
|
||||
console.print(
|
||||
"[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
|
||||
|
||||
|
||||
def run() -> None:
|
||||
def run() -> NoReturn:
|
||||
"""Main function"""
|
||||
tux = None
|
||||
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()
|
||||
elif cli_flags.version:
|
||||
rprint(f"Tuxbot V{version_info.major}")
|
||||
|
@ -205,6 +266,13 @@ def run() -> None:
|
|||
asyncio.set_event_loop(loop)
|
||||
|
||||
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(
|
||||
cli_flags=cli_flags,
|
||||
description="Tuxbot, made from and for OpenSource",
|
||||
|
@ -228,7 +296,7 @@ def run() -> None:
|
|||
raise
|
||||
except Exception as exc:
|
||||
log.error("Unexpected exception (%s): ", type(exc))
|
||||
console.print_exception(show_locals=True)
|
||||
console.print_exception()
|
||||
if tux is not None:
|
||||
loop.run_until_complete(shutdown_handler(tux, None, 1))
|
||||
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