Compare commits

..

No commits in common. "master" and "v3" have entirely different histories.
master ... v3

161 changed files with 672 additions and 7233 deletions

View file

@ -1,8 +0,0 @@
version = 1
[[analyzers]]
name = "python"
enabled = true
[analyzers.meta]
runtime_version = "3.x.x"

View file

@ -1,7 +0,0 @@
# PostgreSQL
# ------------------------------------------------------------------------------
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
POSTGRES_DB=tuxbot_bot
POSTGRES_USER=debug
POSTGRES_PASSWORD=debug

View file

@ -1,4 +0,0 @@
# General
# ------------------------------------------------------------------------------
USE_DOCKER=yes
IPYTHONDIR=/app/.ipython

15
.gitignore vendored
View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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">

View file

@ -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>

View file

@ -1,3 +0,0 @@
[mypy]
ignore_missing_imports = True
exclude = venv

View file

@ -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)

View file

@ -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; \

View file

@ -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

View file

@ -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"]

View file

@ -1,6 +0,0 @@
#!/bin/bash
set -o errexit
set -o pipefail
set -o nounset

View file

@ -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

View file

@ -1,5 +0,0 @@
#!/usr/bin/env bash
BACKUP_DIR_PATH='/backups'
BACKUP_FILE_PREFIX='backup'

View file

@ -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
}

View file

@ -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: ${@}"
}

View file

@ -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
}

View file

@ -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}'."

View file

@ -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}"

View file

@ -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."

View file

@ -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"]

View file

@ -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 "$@"

View file

@ -1,8 +0,0 @@
#!/bin/bash
set -o errexit
set -o pipefail
set -o nounset
tuxbot dev

View file

@ -1,3 +0,0 @@
pylint>=2.6.0
black>=20.8b1
mypy>=0.812

View file

@ -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

View file

@ -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

View file

@ -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/**/*

View file

@ -1,5 +1,5 @@
from setuptools import setup
setup(
python_requires=">=3.8",
python_requires=">=3.7",
)

View file

@ -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"

View file

@ -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()

View file

@ -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:

View file

@ -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)

View file

@ -1,12 +0,0 @@
from typing import Dict
from structured_config import Structure
HAS_MODELS = False
class AdminConfig(Structure):
pass
extra: Dict[str, Dict] = {}

View file

@ -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"

View file

@ -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))

View file

@ -1,12 +0,0 @@
from typing import Dict
from structured_config import Structure
HAS_MODELS = False
class CustomConfig(Structure):
pass
extra: Dict[str, Dict] = {}

View file

@ -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)
)

View file

@ -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

View file

@ -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 ""

View file

@ -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à"

View file

@ -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 ""

View file

@ -1 +0,0 @@
# pylint: disable=cyclic-import

View file

@ -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)

View file

@ -1,12 +0,0 @@
from typing import Dict
from structured_config import Structure
HAS_MODELS = False
class DevConfig(Structure):
pass
extra: Dict[str, Dict] = {}

View file

@ -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)

View file

@ -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)

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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))

View file

@ -1,12 +0,0 @@
from typing import Dict
from structured_config import Structure
HAS_MODELS = False
class LinuxConfig(Structure):
pass
extra: Dict[str, Dict] = {}

View file

@ -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,
}

View file

@ -1,9 +0,0 @@
from discord.ext import commands
class LinuxException(commands.BadArgument):
pass
class CNFException(LinuxException):
pass

View file

@ -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()

View file

@ -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))

View file

@ -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)

View file

@ -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

View file

@ -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 ""

View file

@ -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)"

View file

@ -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 ""

View file

@ -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}**")

View file

@ -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))

View file

@ -1,12 +0,0 @@
from typing import Dict
from structured_config import Structure
HAS_MODELS = True
class ModConfig(Structure):
pass
extra: Dict[str, Dict] = {}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 ""

View file

@ -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"

View file

@ -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 ""

View file

@ -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}")

View file

@ -1,3 +0,0 @@
from .rules import *
from .warns import *
from .mutes import *

View file

@ -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__

View file

@ -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__

View file

@ -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__

View file

@ -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))

View file

@ -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)",
},
}

View file

@ -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")

View file

@ -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

View file

@ -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"
"&center=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

View file

@ -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 ""

View file

@ -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..."

View file

@ -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 ""

View file

@ -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