Merge pull request #122 from stijndcl/didier-v3

Didier v3 - Rewrite from Scratch
pull/136/head
Stijn De Clercq 2022-09-25 14:52:12 +02:00 committed by GitHub
commit 98b18f7ee3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
350 changed files with 32343 additions and 358414 deletions

View File

@ -3,4 +3,4 @@ root = true
[{*.yml, *.yaml}]
indent_size = 2
indent_style = space
max_line_length=80
max_line_length=120

44
.flake8 100644
View File

@ -0,0 +1,44 @@
[flake8]
# Don't lint non-Python files
exclude =
.git,
.github,
.mypy_cache,
.pytest_cache,
__pycache__,
alembic,
htmlcov,
venv
# Disable rules that we don't care about (or conflict with others)
extend-ignore =
# Missing docstring in public module
D100, D104,
# Missing docstring in magic method
D105,
# Missing docstring in __init__
D107,
# First line of docstrings should end with a period
D400,
# First line of docstrings should be in imperative mood
D401,
# Whitespace before ":"
E203,
# Standard pseudo-random generators are not suitable for security/cryptographic purposes.
S311,
# Don't require docstrings when overriding a method,
# the base method should have a docstring but the rest not
ignore-decorators=overrides
max-line-length = 120
# Disable some rules for entire files
per-file-ignores =
# DALL000: Missing __all__, main isn't supposed to be imported
main.py: DALL000,
# DALL000: Missing __all__, Cogs aren't modules
./didier/cogs/*: DALL000,
# DALL000: Missing __all__, tests aren't supposed to be imported
# S101: Use of assert, this is the point of tests
./tests/*: DALL000 S101,
# D103: Missing docstring in public function
# All of the colours methods are just oneliners to create a colour,
# there's no point adding docstrings (function names are enough)
./didier/utils/discord/colours.py: D103,

123
.github/workflows/python.yml vendored 100644
View File

@ -0,0 +1,123 @@
name: Python CI
on:
push:
jobs:
dependencies:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: '3.9.5'
- name: Cache dependencies
uses: actions/cache@v2
with:
path: ~/.m2/repository
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt', '**/requirements-dev.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: pip3 install -r requirements.txt -r requirements-dev.txt
tests:
needs: [dependencies]
runs-on: ubuntu-latest
services:
postgres:
image: postgres:14
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5433:5432
env:
POSTGRES_DB: didier_pytest
POSTGRES_USER: pytest
POSTGRES_PASSWORD: pytest
steps:
- uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: '3.9.5'
- name: Cache dependencies
uses: actions/cache@v2
with:
path: ~/.m2/repository
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt', '**/requirements-dev.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: pip3 install -r requirements.txt -r requirements-dev.txt
- name: Run Pytest
run: |
coverage run -m pytest
coverage xml
- name: Upload coverage report to CodeCov
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV }}
linting:
needs: [tests]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: '3.9.5'
- name: Cache dependencies
uses: actions/cache@v2
with:
path: ~/.m2/repository
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt', '**/requirements-dev.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: pip3 install -r requirements.txt -r requirements-dev.txt
- name: Linting
run: flake8
typing:
needs: [tests]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: '3.9.5'
- name: Cache dependencies
uses: actions/cache@v2
with:
path: ~/.m2/repository
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt', '**/requirements-dev.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: pip3 install -r requirements.txt -r requirements-dev.txt
- name: Typing
run: mypy
formatting:
needs: [tests]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: '3.9.5'
- name: Cache dependencies
uses: actions/cache@v2
with:
path: ~/.m2/repository
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt', '**/requirements-dev.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: pip3 install -r requirements.txt -r requirements-dev.txt
- name: Formatting
run: black --check didier database

View File

@ -1,15 +0,0 @@
name: Run Tests
on:
push:
jobs:
python-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.9.5'
- run: pip3 install -r requirements.txt
- run: pytest tests

174
.gitignore vendored
View File

@ -1,14 +1,162 @@
files/lastTasks.json
files/c4.json
files/hangman.json
files/stats.json
files/lost.json
files/locked.json
files/ufora_notifications.json
files/compbio_benchmarks_2.json
files/compbio_benchmarks_4.json
.idea/
__pycache__
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
/venv/
.pytest_cache
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
.idea/
# Debugging files
debug.py
# Schedule .ics files
/files/schedules/

View File

@ -0,0 +1,46 @@
default_language_version:
python: python3.9.5
repos:
- repo: https://github.com/ambv/black
rev: 22.3.0
hooks:
- id: black
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
hooks:
- id: check-json
- id: end-of-file-fixer
- id: pretty-format-json
- id: trailing-whitespace
- repo: https://github.com/pycqa/isort
rev: 5.10.1
hooks:
- id: isort
- repo: https://github.com/PyCQA/autoflake
rev: v1.4
hooks:
- id: autoflake
name: autoflake (python)
args:
- "--remove-all-unused-imports"
- "--in-place"
- "--ignore-init-module-imports"
- repo: https://github.com/PyCQA/flake8
rev: 4.0.1
hooks:
- id: flake8
exclude: ^(alembic|.github)
args: [--config, .flake8]
additional_dependencies:
- "flake8-bandit"
- "flake8-bugbear"
- "flake8-docstrings"
- "flake8-dunder-all"
- "flake8-eradicate"
- "flake8-isort"
- "flake8-simplify"

105
alembic.ini 100644
View File

@ -0,0 +1,105 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = alembic
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python-dateutil library that can be
# installed by adding `alembic[tz]` to the pip requirements
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to alembic/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = driver://user:pass@localhost/dbname
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

1
alembic/README 100644
View File

@ -0,0 +1 @@
Generic single-database configuration.

51
alembic/env.py 100644
View File

@ -0,0 +1,51 @@
import asyncio
from logging.config import fileConfig
from sqlalchemy.ext.asyncio import AsyncEngine
from alembic import context
from database.engine import postgres_engine
from database.schemas import Base
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
target_metadata = Base.metadata
def do_run_migrations(connection):
context.configure(connection=connection, target_metadata=target_metadata, render_as_batch=True)
with context.begin_transaction():
context.run_migrations()
async def run_async_migrations(connectable: AsyncEngine):
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
await connectable.dispose()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = context.config.attributes.get("connection", None) or postgres_engine
if isinstance(connectable, AsyncEngine):
asyncio.run(run_async_migrations(connectable))
else:
do_run_migrations(connectable)
run_migrations_online()

View File

@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,32 @@
"""Add second role to ufora courses
Revision ID: 11388e39bb90
Revises: a64876b41af2
Create Date: 2022-09-25 00:09:06.625622
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "11388e39bb90"
down_revision = "a64876b41af2"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("ufora_courses", schema=None) as batch_op:
batch_op.add_column(sa.Column("alternative_overarching_role_id", sa.BigInteger(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("ufora_courses", schema=None) as batch_op:
batch_op.drop_column("alternative_overarching_role_id")
# ### end Alembic commands ###

View File

@ -0,0 +1,246 @@
"""Initial migration
Revision ID: 515dc3f52c6d
Revises:
Create Date: 2022-09-18 00:30:56.348634
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "515dc3f52c6d"
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"custom_commands",
sa.Column("command_id", sa.Integer(), nullable=False),
sa.Column("name", sa.Text(), nullable=False),
sa.Column("indexed_name", sa.Text(), nullable=False),
sa.Column("response", sa.Text(), nullable=False),
sa.PrimaryKeyConstraint("command_id"),
sa.UniqueConstraint("name"),
)
with op.batch_alter_table("custom_commands", schema=None) as batch_op:
batch_op.create_index(batch_op.f("ix_custom_commands_indexed_name"), ["indexed_name"], unique=False)
op.create_table(
"dad_jokes",
sa.Column("dad_joke_id", sa.Integer(), nullable=False),
sa.Column("joke", sa.Text(), nullable=False),
sa.PrimaryKeyConstraint("dad_joke_id"),
)
op.create_table(
"links",
sa.Column("link_id", sa.Integer(), nullable=False),
sa.Column("name", sa.Text(), nullable=False),
sa.Column("url", sa.Text(), nullable=False),
sa.PrimaryKeyConstraint("link_id"),
sa.UniqueConstraint("name"),
)
op.create_table(
"meme",
sa.Column("meme_id", sa.Integer(), nullable=False),
sa.Column("name", sa.Text(), nullable=False),
sa.Column("template_id", sa.Integer(), nullable=False),
sa.Column("field_count", sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint("meme_id"),
sa.UniqueConstraint("name"),
sa.UniqueConstraint("template_id"),
)
op.create_table(
"tasks",
sa.Column("task_id", sa.Integer(), nullable=False),
sa.Column("task", sa.Enum("BIRTHDAYS", "SCHEDULES", "UFORA_ANNOUNCEMENTS", name="tasktype"), nullable=False),
sa.Column("previous_run", sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint("task_id"),
sa.UniqueConstraint("task"),
)
op.create_table(
"ufora_courses",
sa.Column("course_id", sa.Integer(), nullable=False),
sa.Column("name", sa.Text(), nullable=False),
sa.Column("code", sa.Text(), nullable=False),
sa.Column("year", sa.Integer(), nullable=False),
sa.Column("compulsory", sa.Boolean(), server_default="1", nullable=False),
sa.Column("role_id", sa.BigInteger(), nullable=True),
sa.Column("overarching_role_id", sa.BigInteger(), nullable=True),
sa.Column("log_announcements", sa.Boolean(), server_default="0", nullable=False),
sa.PrimaryKeyConstraint("course_id"),
sa.UniqueConstraint("code"),
sa.UniqueConstraint("name"),
)
op.create_table("users", sa.Column("user_id", sa.BigInteger(), nullable=False), sa.PrimaryKeyConstraint("user_id"))
op.create_table(
"wordle_word",
sa.Column("word_id", sa.Integer(), nullable=False),
sa.Column("word", sa.Text(), nullable=False),
sa.Column("day", sa.Date(), nullable=False),
sa.PrimaryKeyConstraint("word_id"),
sa.UniqueConstraint("day"),
)
op.create_table(
"bank",
sa.Column("bank_id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.BigInteger(), nullable=True),
sa.Column("dinks", sa.BigInteger(), server_default="0", nullable=False),
sa.Column("invested", sa.BigInteger(), server_default="0", nullable=False),
sa.Column("interest_level", sa.Integer(), server_default="1", nullable=False),
sa.Column("capacity_level", sa.Integer(), server_default="1", nullable=False),
sa.Column("rob_level", sa.Integer(), server_default="1", nullable=False),
sa.ForeignKeyConstraint(
["user_id"],
["users.user_id"],
),
sa.PrimaryKeyConstraint("bank_id"),
)
op.create_table(
"birthdays",
sa.Column("birthday_id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.BigInteger(), nullable=True),
sa.Column("birthday", sa.Date(), nullable=False),
sa.ForeignKeyConstraint(
["user_id"],
["users.user_id"],
),
sa.PrimaryKeyConstraint("birthday_id"),
)
op.create_table(
"bookmarks",
sa.Column("bookmark_id", sa.Integer(), nullable=False),
sa.Column("label", sa.Text(), nullable=False),
sa.Column("jump_url", sa.Text(), nullable=False),
sa.Column("user_id", sa.BigInteger(), nullable=True),
sa.ForeignKeyConstraint(
["user_id"],
["users.user_id"],
),
sa.PrimaryKeyConstraint("bookmark_id"),
sa.UniqueConstraint("user_id", "label"),
)
op.create_table(
"custom_command_aliases",
sa.Column("alias_id", sa.Integer(), nullable=False),
sa.Column("alias", sa.Text(), nullable=False),
sa.Column("indexed_alias", sa.Text(), nullable=False),
sa.Column("command_id", sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(
["command_id"],
["custom_commands.command_id"],
),
sa.PrimaryKeyConstraint("alias_id"),
sa.UniqueConstraint("alias"),
)
with op.batch_alter_table("custom_command_aliases", schema=None) as batch_op:
batch_op.create_index(batch_op.f("ix_custom_command_aliases_indexed_alias"), ["indexed_alias"], unique=False)
op.create_table(
"deadlines",
sa.Column("deadline_id", sa.Integer(), nullable=False),
sa.Column("course_id", sa.Integer(), nullable=True),
sa.Column("name", sa.Text(), nullable=False),
sa.Column("deadline", sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(
["course_id"],
["ufora_courses.course_id"],
),
sa.PrimaryKeyConstraint("deadline_id"),
)
op.create_table(
"nightly_data",
sa.Column("nightly_id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.BigInteger(), nullable=True),
sa.Column("last_nightly", sa.Date(), nullable=True),
sa.Column("count", sa.Integer(), server_default="0", nullable=False),
sa.ForeignKeyConstraint(
["user_id"],
["users.user_id"],
),
sa.PrimaryKeyConstraint("nightly_id"),
)
op.create_table(
"ufora_announcements",
sa.Column("announcement_id", sa.Integer(), nullable=False),
sa.Column("course_id", sa.Integer(), nullable=True),
sa.Column("publication_date", sa.Date(), nullable=True),
sa.ForeignKeyConstraint(
["course_id"],
["ufora_courses.course_id"],
),
sa.PrimaryKeyConstraint("announcement_id"),
)
op.create_table(
"ufora_course_aliases",
sa.Column("alias_id", sa.Integer(), nullable=False),
sa.Column("alias", sa.Text(), nullable=False),
sa.Column("course_id", sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(
["course_id"],
["ufora_courses.course_id"],
),
sa.PrimaryKeyConstraint("alias_id"),
sa.UniqueConstraint("alias"),
)
op.create_table(
"wordle_guesses",
sa.Column("wordle_guess_id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.BigInteger(), nullable=True),
sa.Column("guess", sa.Text(), nullable=False),
sa.ForeignKeyConstraint(
["user_id"],
["users.user_id"],
),
sa.PrimaryKeyConstraint("wordle_guess_id"),
)
op.create_table(
"wordle_stats",
sa.Column("wordle_stats_id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.BigInteger(), nullable=True),
sa.Column("last_win", sa.Date(), nullable=True),
sa.Column("games", sa.Integer(), server_default="0", nullable=False),
sa.Column("wins", sa.Integer(), server_default="0", nullable=False),
sa.Column("current_streak", sa.Integer(), server_default="0", nullable=False),
sa.Column("highest_streak", sa.Integer(), server_default="0", nullable=False),
sa.ForeignKeyConstraint(
["user_id"],
["users.user_id"],
),
sa.PrimaryKeyConstraint("wordle_stats_id"),
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("wordle_stats")
op.drop_table("wordle_guesses")
op.drop_table("ufora_course_aliases")
op.drop_table("ufora_announcements")
op.drop_table("nightly_data")
op.drop_table("deadlines")
with op.batch_alter_table("custom_command_aliases", schema=None) as batch_op:
batch_op.drop_index(batch_op.f("ix_custom_command_aliases_indexed_alias"))
op.drop_table("custom_command_aliases")
op.drop_table("bookmarks")
op.drop_table("birthdays")
op.drop_table("bank")
op.drop_table("wordle_word")
op.drop_table("users")
op.drop_table("ufora_courses")
op.drop_table("tasks")
op.drop_table("meme")
op.drop_table("links")
op.drop_table("dad_jokes")
with op.batch_alter_table("custom_commands", schema=None) as batch_op:
batch_op.drop_index(batch_op.f("ix_custom_commands_indexed_name"))
op.drop_table("custom_commands")
sa.Enum("BIRTHDAYS", "SCHEDULES", "UFORA_ANNOUNCEMENTS", name="tasktype").drop(op.get_bind())
# ### end Alembic commands ###

View File

@ -0,0 +1,39 @@
"""Add reminders
Revision ID: a64876b41af2
Revises: c1f9ee875616
Create Date: 2022-09-23 13:37:10.331840
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "a64876b41af2"
down_revision = "c1f9ee875616"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"reminders",
sa.Column("reminder_id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.BigInteger(), nullable=True),
sa.Column("category", sa.Enum("LES", name="remindercategory"), nullable=False),
sa.ForeignKeyConstraint(
["user_id"],
["users.user_id"],
),
sa.PrimaryKeyConstraint("reminder_id"),
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("reminders")
sa.Enum("LES", name="remindercategory").drop(op.get_bind())
# ### end Alembic commands ###

View File

@ -0,0 +1,36 @@
"""Easter eggs
Revision ID: b84bb10fb8de
Revises: 515dc3f52c6d
Create Date: 2022-09-20 00:23:53.160168
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "b84bb10fb8de"
down_revision = "515dc3f52c6d"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"easter_eggs",
sa.Column("easter_egg_id", sa.Integer(), nullable=False),
sa.Column("match", sa.Text(), nullable=False),
sa.Column("response", sa.Text(), nullable=False),
sa.Column("exact", sa.Boolean(), server_default="1", nullable=False),
sa.Column("startswith", sa.Boolean(), server_default="1", nullable=False),
sa.PrimaryKeyConstraint("easter_egg_id"),
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("easter_eggs")
# ### end Alembic commands ###

View File

@ -0,0 +1,54 @@
"""Command stats & GitHub links
Revision ID: c1f9ee875616
Revises: b84bb10fb8de
Create Date: 2022-09-20 17:18:02.289593
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "c1f9ee875616"
down_revision = "b84bb10fb8de"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"command_stats",
sa.Column("command_stats_id", sa.Integer(), nullable=False),
sa.Column("command", sa.Text(), nullable=False),
sa.Column("timestamp", sa.DateTime(timezone=True), nullable=False),
sa.Column("user_id", sa.BigInteger(), nullable=True),
sa.Column("slash", sa.Boolean(), nullable=False),
sa.Column("context_menu", sa.Boolean(), nullable=False),
sa.ForeignKeyConstraint(
["user_id"],
["users.user_id"],
),
sa.PrimaryKeyConstraint("command_stats_id"),
)
op.create_table(
"github_links",
sa.Column("github_link_id", sa.Integer(), nullable=False),
sa.Column("url", sa.Text(), nullable=False),
sa.Column("user_id", sa.BigInteger(), nullable=True),
sa.ForeignKeyConstraint(
["user_id"],
["users.user_id"],
),
sa.PrimaryKeyConstraint("github_link_id"),
sa.UniqueConstraint("url"),
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("github_links")
op.drop_table("command_stats")
# ### end Alembic commands ###

14
codecov.yaml 100644
View File

@ -0,0 +1,14 @@
comment:
layout: "reach, diff, flags, files"
behavior: default
require_changes: false # if true: only post the comment if coverage changes
require_base: no # [yes :: must have a base report to post]
require_head: yes # [yes :: must have a head report to post]
coverage:
round: down
precision: 5
ignore:
- "./didier/cogs/*" # Cogs can't really be tested properly
- "./tests/*"

View File

@ -1,226 +0,0 @@
from data import constants
import datetime
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import timeFormatters, stringFormatters
from functions.database import birthdays
class Birthdays(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.group(name="Birthday", aliases=["Bd", "Birthdays"], case_insensitive=True, invoke_without_command=True)
@help.Category(Category.Other)
async def birthday(self, ctx, member: discord.Member = None):
"""
Command to check the birthday of yourself/another person.
:param ctx: Discord Context
:param member: The member to check
"""
if member is not None:
# A member was tagged
nameStr = "**{}**'s".format(member.display_name)
res = birthdays.get_user(member.id)
else:
# No member passed -> check the user's birthday
nameStr = "Jouw"
res = birthdays.get_user(ctx.author.id)
if not res:
# Nothing found in the db for this member
return await ctx.send("{} verjaardag zit nog niet in de database.".format(nameStr))
# Create a datetime object of the upcoming birthday,
# and a formatted string displaying the date
dayDatetime, timeString = self.dmToDatetime(res[0][0], res[0][1])
# Find the weekday related to this day
weekday = timeFormatters.intToWeekday(dayDatetime.weekday()).lower()
return await ctx.send("{} verjaardag staat ingesteld op **{} {}**.".format(
nameStr, weekday, timeString
))
@birthday.command(name="Today", aliases=["Now"])
async def today(self, ctx):
"""
Command that lists all birthdays of the day.
:param ctx: Discord Context
"""
# Create a datetime object for today
dt = timeFormatters.dateTimeNow()
await ctx.send(self.getBirthdayOnDate(dt))
@birthday.command(name="Tomorrow", aliases=["Tm", "Tmw"])
async def tomorrow(self, ctx):
"""
Command that lists all birthdays of tomorrow.
:param ctx: Discord Context
"""
# Create a datetime object for tomorrow
dt = timeFormatters.dateTimeNow() + datetime.timedelta(days=1)
await ctx.send(self.getBirthdayOnDate(dt).replace("Vandaag", "Morgen").replace("vandaag", "morgen"))
@birthday.command(name="Week")
async def week(self, ctx):
"""
Command that lists all birthdays for the coming week.
:param ctx: Discord Context
"""
# Dict of all birthdays this week
this_week = {}
# Create a datetime object starting yesterday so the first line
# of the loop can add a day every time,
# as premature returning would prevent this from happening
# & get the day stuck
dt = timeFormatters.dateTimeNow() - datetime.timedelta(days=1)
# Create an embed
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Verjaardagen deze week")
# Add all people of the coming week
for dayCounter in range(7):
dt += datetime.timedelta(days=1)
res = birthdays.get_users_on_date(dt.day, dt.month)
# No birthdays on this day
if not res:
continue
# Add everyone from this day into the dict
this_week[str(dayCounter)] = {"day": dt.day, "month": dt.month, "users": []}
for user in res:
this_week[str(dayCounter)]["users"].append(user[0])
# No one found
if not this_week:
embed.description = "Deze week is er niemand jarig."
return await ctx.send(embed=embed)
COC = self.client.get_guild(int(constants.CallOfCode))
# For every day, add the list of users into the embed
for day, value in this_week.items():
dayDatetime, timeString = self.dmToDatetime(int(value["day"]), int(value["month"]))
weekday = timeFormatters.intToWeekday(dayDatetime.weekday())
embed.add_field(name="{} {}".format(weekday, timeString),
value=", ".join(COC.get_member(user).mention for user in value["users"]),
inline=False)
await ctx.send(embed=embed)
def getBirthdayOnDate(self, dt):
"""
Function to get all birthdays on a certain date.
Returns a string right away to avoid more code duplication.
:param dt: the date (Python datetime instance)
:return: A formatted string containing all birthdays on [dt]
"""
res = birthdays.get_users_on_date(dt.day, dt.month)
# Nobody's birthday
if not res:
return "Vandaag is er niemand jarig."
COC = self.client.get_guild(int(constants.CallOfCode))
# Create a list of member objects of the people that have a birthday on this date
people = [COC.get_member(int(user[0])) for user in res]
if len(people) == 1:
return "Vandaag is **{}** jarig.".format(people[0].display_name)
return "Vandaag zijn {} en {} jarig.".format(
", ".join("**" + user.display_name + "**" for user in people[:-1]),
people[-1].display_name
)
def dmToDatetime(self, day, month):
"""
Converts a day + month to a datetime instance.
:param day: the day in the date
:param month: the month in the date
:return: a datetime instance representing the next time this date occurs,
and a formatted string for this date
"""
now = timeFormatters.dateTimeNow()
year = now.year
# Add an extra year to the date in case it has already passed
if month < now.month or (month == now.month and day < now.day):
year += 1
# Create a datetime object for this birthday
timeString = "{}/{}/{}".format(
stringFormatters.leading_zero(str(day)),
stringFormatters.leading_zero(str(month)),
year
)
dayDatetime = datetime.datetime.strptime(timeString, "%d/%m/%Y")
return dayDatetime, timeString
@birthday.command(name="Set", usage="[DD/MM/YYYY]")
async def set(self, ctx, date=None, member: discord.Member = None):
"""
Command to add your birthday into the database.
:param ctx: Discord Context
:param date: the date of your birthday
:param member: another member whose birthday has to be added/changed
"""
# No date passed
if date is None:
return await ctx.send("Geef een datum op.")
# Invalid format used
if date.count("/") != 2:
return await ctx.send("Ongeldig formaat (gebruik DD/MM/YYYY).")
# Check if anything is wrong with the date
try:
day = int(date.split("/")[0])
month = int(date.split("/")[1])
year = int(date.split("/")[2])
# This is not used, but creating an invalid datetime object throws a ValueError
# so it prevents invalid dates like 69/420/360
dt = datetime.datetime(year=year, month=month, day=day)
# Assume no one in the Discord is more than 5 years younger, or 10 years older
# (which are also virtually impossible, but just to be sure)
if year >= timeFormatters.dateTimeNow().year - 15 or year < 1990:
raise ValueError
except ValueError:
return await ctx.send("Dit is geen geldige datum.")
# A member was tagged, check if I did it
if member is not None:
if str(ctx.author.id) != str(constants.myId):
return await ctx.send("Je kan andere mensen hun verjaardag niet instellen, {}.".format(ctx.author.display_name))
else:
birthdays.add_user(member.id, day, month, year)
return await ctx.message.add_reaction("")
# Birthday is already added
if birthdays.get_user(ctx.author.id) and str(ctx.author.id) != constants.myId:
return await ctx.send("Je verjaardag zit al in de database.")
# Add into the db
birthdays.add_user(ctx.author.id, day, month, year)
return await ctx.send("Je verjaardag is toegevoegd aan de database.")
def setup(client):
client.add_cog(Birthdays(client))

View File

@ -1,149 +0,0 @@
from converters.numbers import Abbreviated
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import checks, timeFormatters
from functions.database import currency
import requests
class Bitcoin(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.group(name="Bitcoin", aliases=["Bc"], case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Currency)
async def bc(self, ctx):
"""
Command that shows your Bitcoin bank.
:param ctx: Discord Context
"""
price = self.getPrice()
bc = float(currency.getOrAddUser(ctx.author.id)[8])
currentTime = timeFormatters.dateTimeNow()
currentTimeFormatted = currentTime.strftime('%m/%d/%Y om %H:%M:%S')
# Create the embed
embed = discord.Embed(colour=discord.Colour.gold())
embed.set_author(name="Bitcoin Bank van {}".format(ctx.author.display_name))
embed.add_field(name="Aantal Bitcoins:", value="{:,}".format(round(bc, 8)), inline=False)
embed.add_field(name="Huidige waarde:", value="{:,} Didier Dink{}"
.format(round(bc * price, 8), checks.pluralS(bc * price)), inline=False)
embed.set_footer(text="Huidige Bitcoin prijs: €{:,} ({})".format(price, str(currentTimeFormatted)))
# Add the Bitcoin icon to the embed
file = discord.File("files/images/bitcoin.png", filename="icon.png")
embed.set_thumbnail(url="attachment://icon.png")
await ctx.send(embed=embed, file=file)
@bc.command(name="Price")
async def price(self, ctx):
"""
Command that shows the current Bitcoin price.
:param ctx: Discord Context
"""
price = self.getPrice()
currentTime = timeFormatters.dateTimeNow()
currentTimeFormatted = currentTime.strftime('%m/%d/%Y om %H:%M:%S')
await ctx.send(
"Huidige Bitcoin prijs: **€{:,}** ({}).".format(price, str(currentTimeFormatted)))
@bc.command(name="Buy", usage="[Aantal]")
async def buy(self, ctx, amount: Abbreviated):
"""
Command to buy Bitcoins.
:param ctx: Discord Context
:param amount: the amount of Bitcoins the user wants to buy
"""
resp = checks.isValidAmount(ctx, amount)
# Not a valid amount: send the appropriate error message
if not resp[0]:
return await ctx.send(resp[1])
if str(amount).lower() == "all":
amount = resp[1]
# Calculate the amount of Bitcoins the user can buy with [amount] of Didier Dinks
price = self.getPrice()
purchased = round(float(amount) / price, 8)
# Update the db
currency.update(ctx.author.id, "dinks", float(currency.dinks(ctx.author.id)) - float(amount))
currency.update(ctx.author.id, "bitcoins",
float(currency.getOrAddUser(ctx.author.id)[8]) + float(purchased))
await ctx.send("**{}** heeft **{:,}** Bitcoin{} gekocht voor **{:,}** Didier Dink{}!"
.format(ctx.author.display_name, purchased, checks.pluralS(purchased),
round(float(amount)), checks.pluralS(amount)))
@bc.command(name="Sell", usage="[Aantal]")
async def sell(self, ctx, amount: Abbreviated):
"""
Command to sell Bitcoins.
:param ctx: Discord Context
:param amount: the amount of Bitcoins the user wants to sell
"""
if str(amount).lower() == "all":
amount = float(currency.getOrAddUser(ctx.author.id)[8])
try:
amount = float(amount)
if amount <= 0:
raise ValueError
bc = float(currency.getOrAddUser(ctx.author.id)[8])
if bc == 0.0:
# User has no Bitcoins
await ctx.send("Je hebt geen Bitcoins, **{}**".format(ctx.author.display_name))
elif amount > bc:
# User is trying to sell more Bitcoins that he has
await ctx.send("Je hebt niet genoeg Bitcoins om dit te doen, **{}**"
.format(ctx.author.display_name))
else:
price = self.getPrice()
dinks = float(currency.dinks(ctx.author.id))
currency.update(ctx.author.id, "bitcoins", bc - amount)
currency.update(ctx.author.id, "dinks", dinks + (price * amount))
await ctx.send("**{}** heeft **{:,}** Bitcoin{} verkocht voor **{:,}** Didier Dink{}!"
.format(ctx.author.display_name, round(amount, 8), checks.pluralS(amount),
round((price * amount), 8), checks.pluralS(price * amount)))
except ValueError:
# Can't be parsed to float -> random string OR smaller than 0
await ctx.send("Geef een geldig bedrag op.")
@bc.command(aliases=["Lb", "Leaderboards"], hidden=True)
@help.Category(category=Category.Other)
async def leaderboard(self, ctx):
"""
Command that shows the Bitcoin Leaderboard.
Alias for Lb Bc.
:param ctx: Discord Context
"""
# Call the appropriate leaderboard function
await self.client.get_cog("Leaderboards").callLeaderboard("bitcoin", ctx)
def getPrice(self):
"""
Function to get the current Bitcoin price.
:return: the current Bitcoin price (float)
"""
result = requests.get("https://api.coindesk.com/v1/bpi/currentprice.json").json()
currentPrice = result["bpi"]["EUR"]["rate_float"]
return float(currentPrice)
def setup(client):
client.add_cog(Bitcoin(client))

View File

@ -1,32 +0,0 @@
import discord
from discord import ApplicationContext
from discord.ext import commands
from discord.commands import message_command
from startup.didier import Didier
class SchoolCM(commands.Cog):
def __init__(self, client: Didier):
self.client: Didier = client
@message_command(name="Pin")
async def _pin_cm(self, ctx: ApplicationContext, message: discord.Message):
# In case people abuse, check if they're blacklisted
blacklist = []
if ctx.user.id in blacklist:
return await ctx.respond(":angry:", ephemeral=True)
if message.is_system():
return await ctx.respond("Dus jij wil system messages pinnen?\nMag niet.", ephemeral=True)
await message.pin(reason=f"Didier Pin door {ctx.user.display_name}")
await ctx.respond("📌", ephemeral=True)
def setup(client: Didier):
# client.add_cog(SchoolCM(client))
# TODO wait for bug to be fixed in lib then uncomment this
# when used in dm, tries to create a DM with the bot?
pass

View File

@ -1,508 +0,0 @@
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import checks, timeFormatters
import requests
class Corona(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
# Gets the information & calls other functions if necessary
@commands.group(name="Corona", usage="[Land]*", case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Other)
async def corona(self, ctx, country: str = "Belgium"):
"""
Command that shows the corona stats for a certain country.
:param ctx: Discord Context
:param country: the country to show the stats for
"""
dic = await self.getCountryStats(country)
if dic is None:
# Country was not found
await self.sendError(ctx)
return
# Vaccination stats
try:
vaccine = self.getVaccineData(country, dic["today"]["population"], dic["yesterday"]["population"])
except KeyError:
vaccine = None
await self.sendEmbed(ctx, dic, vaccine)
@corona.command(name="Vaccinations", aliases=["V", "Vacc", "Vax", "Vaxx"])
async def vaccinations(self, ctx):
population = 11632326
res = requests.get("https://covid-vaccinatie.be/api/v1/administered.json")
updated = requests.get("https://covid-vaccinatie.be/api/v1/last-updated.json")
if res.status_code != 200:
return
res = res.json()
administered = res["result"]["administered"]
first_dose = 0
second_dose = 0
# Get dose counts
for entry in administered:
first_dose += entry["first_dose"]
second_dose += entry["second_dose"]
new_info = self.get_new_vaccinations(administered)
# % of population that has received their vaccine
first_dose_perc = round(first_dose * 100 / population, 2)
second_dose_perc = round(second_dose * 100 / population, 2)
# % of population that has received their vaccine TODAY
first_today_perc = round(new_info["today"]["first_dose"] * 100 / population, 2)
second_today_perc = round(new_info["today"]["second_dose"] * 100 / population, 2)
# Difference compared to the day before
first_trend = self.trend(new_info, "first_dose")
second_trend = self.trend(new_info, "second_dose")
embed = discord.Embed(colour=discord.Colour.red(), title="Vaccinatiecijfers")
embed.add_field(name="Eerste Dosis", value="{:,} ({}%)".format(first_dose, first_dose_perc))
embed.add_field(name="Eerste Dosis (Vandaag)",
value="{:,} ({}%)".format(new_info["today"]["first_dose"], first_today_perc))
embed.add_field(name="Eerste Dosis (Verschil)", value=first_trend)
embed.add_field(name="Tweede Dosis", value="{:,} ({}%)".format(second_dose, second_dose_perc))
embed.add_field(name="Tweede Dosis (Vandaag)",
value="{:,} ({}%)".format(new_info["today"]["second_dose"], second_today_perc))
embed.add_field(name="Tweede Dosis (Verschil)", value=second_trend)
# Only add updated timestamp if the request succeeded
# this isn't really a big deal so the command doesn't fail
# if it didn't
if updated.status_code == 200:
embed.set_footer(text="Laatste update: {}".format(updated.json()["result"]["last_updated"]["updated"]))
return await ctx.send(embed=embed)
def get_new_vaccinations(self, data):
"""
Finds the amount of new doses administered today & the day before
"""
reversed_data = list(reversed(data))
# Find the most recent date that was added
# (not necessarily today)
latest_date = reversed_data[0]["date"]
date_before = ""
info = {
"today": {
"first_dose": 0,
"second_dose": 0
},
"yesterday": {
"first_dose": 0,
"second_dose": 0
}
}
# Find first date doses
for entry in reversed_data:
if entry["date"] == latest_date:
info["today"]["first_dose"] += entry["first_dose"]
info["today"]["second_dose"] += entry["second_dose"]
else:
# Find the date before the most recent one
# to calculate differences
date_before = entry["date"]
break
# Find second date doses
for entry in reversed_data:
# Info on first date was added above
if entry["date"] == latest_date:
continue
elif entry["date"] == date_before:
info["yesterday"]["first_dose"] += entry["first_dose"]
info["yesterday"]["second_dose"] += entry["second_dose"]
else:
break
return info
@corona.command(aliases=["lb", "leaderboards"], hidden=True)
async def leaderboard(self, ctx):
"""
Command that shows the Corona Leaderboard.
Alias for Lb Corona.
:param ctx: Discord Context
:return: y
"""
await self.client.get_cog("Leaderboards").callLeaderboard("corona", ctx)
async def sendEmbed(self, ctx, dic, vaccines):
"""
Function that sends a Corona embed from a dictionary.
:param ctx: Discord Context
:param dic: the dictionary corresponding to this country
"""
embed = discord.Embed(colour=discord.Colour.red(), title="Coronatracker {}".format(dic["today"]["country"]))
embed.set_thumbnail(url="https://i.imgur.com/aWnDuBt.png")
# Total
embed.add_field(name="Totale Gevallen (Vandaag):",
value=self.createEmbedString(
dic["today"]["cases"],
dic["today"]["todayCases"],
self.trendIndicator(dic, "todayCases")
),
inline=False)
# Active
embed.add_field(name="Actieve Gevallen (Vandaag):",
value=self.createEmbedString(
dic["today"]["activeCases"],
dic["today"]["activeCases"] - dic["yesterday"]["activeCases"],
self.activeTrendIndicator(dic)
),
inline=False)
# Deaths
embed.add_field(name="Sterfgevallen (Vandaag):",
value=self.createEmbedString(
dic["today"]["deaths"],
dic["today"]["todayDeaths"],
self.trendIndicator(dic, "todayDeaths")
),
inline=False)
# Recovered
embed.add_field(name="Hersteld (Vandaag):",
value=self.createEmbedString(
dic["today"]["recovered"],
dic["today"]["todayRecovered"],
self.trendIndicator(dic, "todayRecovered")
),
inline=False)
# Test Cases
embed.add_field(name="Aantal uitgevoerde tests:",
value=self.createEmbedString(
dic["today"]["tests"],
dic["today"]["todayTests"]
),
inline=False)
# Vaccines
if vaccines is not None:
# embed.add_field(name="Aantal toegediende vaccins:",
# value=self.createEmbedString(
# vaccines["today"]["vaccines"],
# vaccines["today"]["todayVaccines"],
# self.trendIndicator(vaccines, "todayVaccines")
# ))
embed.add_field(name="Aantal toegediende vaccins:",
value="{:,}".format(vaccines["today"]["vaccines"]),
inline=False)
else:
# Vaccine data is missing
embed.add_field(name="Aantal toegediende vaccins:",
value="?",
inline=False)
# Timestamp of last update
timeFormatted = timeFormatters.epochToDate(int(dic["today"]["updated"]) / 1000)
embed.set_footer(text="Laatst geüpdatet op {} ({} geleden)".format(
timeFormatted["date"], timeFormatted["timeAgo"]))
await ctx.send(embed=embed)
def createEmbedString(self, total, today, indicator="", isPercentage=False):
"""
Function that formats a string to add covid data into the embed,
separate function to make code more readable
"""
# + or - symbol | minus is included in the number so no need
symbol = "+" if today >= 0 else ""
perc = "%" if isPercentage else ""
return "{:,}{} **({}{:,}{})** {}".format(
total, perc, symbol, today, perc, indicator
)
@commands.command(name="Trends", aliases=["Ct"], usage="[Land]*")
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Other)
async def trends(self, ctx, country: str = "Belgium"):
"""
Command that gives more precise stats & changes.
:param ctx: Discord Context
:param country: the country to get the stats for
"""
dic = await self.getCountryStats(country)
if dic is None:
await self.sendError(ctx)
return
# Get the distribution for this country
distribution = self.distribution(dic)
embed = discord.Embed(colour=discord.Colour.red(), title="Coronatrends {}".format(dic["today"]["country"]))
embed.set_thumbnail(url="https://i.imgur.com/aWnDuBt.png")
# Calculate the trends & add them into the fields
embed.add_field(name="Totale Gevallen\n({:,})".format(dic["today"]["cases"]),
value=self.trend(dic, "cases"),
inline=True)
embed.add_field(name="Sterfgevallen\n({:,})".format(dic["today"]["deaths"]),
value=self.trend(dic, "deaths"),
inline=True)
embed.add_field(name="Hersteld\n({:,})".format(dic["today"]["recovered"]),
value=self.trend(dic, "recovered"))
embed.add_field(name="Totale Gevallen\nVandaag ({:,})".format(dic["today"]["todayCases"]),
value=self.trend(dic, "todayCases"),
inline=True)
embed.add_field(name="Sterfgevallen\nVandaag ({:,})".format(dic["today"]["todayDeaths"]),
value=self.trend(dic, "todayDeaths"),
inline=True)
embed.add_field(name="Hersteld\nVandaag ({:,})".format(dic["today"]["todayRecovered"]),
value=self.trend(dic, "todayRecovered"))
embed.add_field(name="Verdeling", value="Actief: {} | Overleden: {} | Hersteld: {}".format(
distribution[0], distribution[1], distribution[2]), inline=False)
# Timestamp of last update
timeFormatted = timeFormatters.epochToDate(int(dic["today"]["updated"]) / 1000)
embed.set_footer(text="Laatst geüpdatet op {} ({} geleden)".format(
timeFormatted["date"], timeFormatted["timeAgo"]))
await ctx.send(embed=embed)
async def getCountryStats(self, country):
"""
Function that gets the stats for a specific country.
:param country: the country to get the stats for
:return: a dictionary containing the info for today & yesterday
"""
# Check if Global or a country was passed
if country.lower() == "global":
country = "all?"
else:
country = "countries/{}?strict=false&".format(country)
today = requests.get("https://disease.sh/v3/covid-19/{}yesterday=false&allowNull=false".format(country)).json()
# Send error message
if "message" in today:
return None
yesterday = requests.get("https://disease.sh/v3/covid-19/{}yesterday=true&allowNull=false".format(country)) \
.json()
# Divide into today & yesterday to be able to calculate the changes
dic = {
"today": {
"country": today["country"] if country != "all?" else "Global",
"cases": today["cases"],
"activeCases": today["active"],
"todayCases": today["todayCases"],
"deaths": today["deaths"],
"todayDeaths": today["todayDeaths"],
"recovered": today["recovered"],
"todayRecovered": today["todayRecovered"],
"tests": today["tests"],
"todayTests": today["tests"] - yesterday["tests"],
"updated": today["updated"],
"population": today["population"]
},
"yesterday": {
"cases": yesterday["cases"],
"activeCases": yesterday["active"],
"todayCases": yesterday["todayCases"],
"deaths": yesterday["deaths"],
"todayDeaths": yesterday["todayDeaths"],
"recovered": yesterday["recovered"],
"todayRecovered": yesterday["todayRecovered"],
"tests": yesterday["tests"],
"updated": yesterday["updated"],
"population": yesterday["population"]
}
}
return dic
def getVaccineData(self, country: str, todayPopulation: int, yesterdayPopulation: int):
"""
Function that gets vaccination stats for a specicic country.
This information is later added to the embed.
"""
if todayPopulation == 0:
todayPopulation = 1
if yesterdayPopulation == 0:
yesterdayPopulation = 1
# "all" has a different endpoint
if country == "global":
vaccine: dict = requests.get("https://disease.sh/v3/covid-19/vaccine/coverage?lastdays=3").json()
else:
vaccine: dict = requests.get("https://disease.sh/v3/covid-19/vaccine/coverage/countries/{}?lastdays=3"
.format(country)).json()
# Country-specific is structured differently
if "timeline" in vaccine:
vaccine = vaccine["timeline"]
# Error message returned or not enough data yet
if "message" in vaccine or len(vaccine.keys()) != 3:
return None
timeFormat = "%m/%d/%y"
def getDt(dt):
"""
Function that calls fromString with an argument
so it can be used in map
"""
return timeFormatters.fromString(dt, timeFormat)
def toString(dt):
"""
Function to cast datetime back to string
"""
st: str = dt.strftime(timeFormat)
# Api doesn't add leading zeroes so the keys don't match anymore ---'
if st.startswith("0"):
st = st[1:]
if st[2] == "0":
st = st[:2] + st[3:]
return st
# Order dates
ordered = sorted(map(getDt, vaccine.keys()))
# Datetime objects are only required for sorting, turn back into strings
ordered = list(map(toString, ordered))
info = {"today": {}, "yesterday": {}}
# Total vaccines
info["today"]["vaccines"] = vaccine[ordered[2]]
# New vaccines today
info["today"]["todayVaccines"] = vaccine[ordered[2]] - vaccine[ordered[1]]
# % of total population
info["today"]["perc"] = round(100 * vaccine[ordered[2]] / todayPopulation, 2)
info["yesterday"]["vaccines"] = vaccine[ordered[1]]
info["yesterday"]["todayVaccines"] = vaccine[ordered[1]] - vaccine[ordered[0]]
return info
def distribution(self, dic):
"""
Calculates the percentage distribution for every key & shows an indicator.
:param dic: the today/yesterday dictionary for this country
:return: a list containing the distribution + indicator for active, recovered & deaths
"""
totalToday = dic["today"]["cases"] if dic["today"]["cases"] != 0 else 1
totalYesterday = dic["yesterday"]["cases"] if dic["yesterday"]["cases"] != 0 else 1
tap = round(100 * dic["today"]["activeCases"] / totalToday, 2) # Today Active Percentage
trp = round(100 * dic["today"]["recovered"] / totalToday, 2) # Today Recovered Percentage
tdp = round(100 * dic["today"]["deaths"] / totalToday, 2) # Today Deaths Percentage
yap = round(100 * dic["yesterday"]["activeCases"] / totalYesterday, 2) # Yesterday Active Percentage
yrp = round(100 * dic["yesterday"]["recovered"] / totalYesterday, 2) # Yesterday Recovered Percentage
ydp = round(100 * dic["yesterday"]["deaths"] / totalYesterday, 2) # Yesterday Deaths Percentage
return ["{}% {}".format(tap, self.indicator(tap, yap)),
"{}% {}".format(tdp, self.indicator(tdp, ydp)),
"{}% {}".format(trp, self.indicator(trp, yrp))]
async def sendError(self, ctx):
"""
Function that sends an error embed when an invalid country was passed.
:param ctx: Discord Context
"""
embed = discord.Embed(colour=discord.Colour.red())
embed.add_field(name="Error", value="Dit land staat niet in de database.", inline=False)
await ctx.send(embed=embed)
# Returns a number and a percentage of rise/decline
def trend(self, dic, key):
"""
Function that creates a string representing a number & percentage of
rise & decline for a certain key of the dict.
:param dic: the today/yesterday dictionary for this country
:param key: the key to compare
:return: a string showing the increase in numbers & percentages
"""
# Difference vs yesterday
change = dic["today"][key] - dic["yesterday"][key]
# Don't divide by 0
yesterday = dic["yesterday"][key] if dic["yesterday"][key] != 0 else 1
# Percentage
perc = round(100 * change / yesterday, 2)
# Sign to add to the number
sign = "+" if change >= 0 else ""
return "{}{:,} ({}{:,}%)".format(sign, change, sign, perc)
# Requires a bit of math so this is a separate function
def activeTrendIndicator(self, dic):
"""
Function that returns a rise/decline indicator for the active cases of the day.
This is a separate function as it requires some math to get right.
New cases have to take into account the deaths & recovered cases being
subtracted as well.
:param dic: the today/yesterday dictionary for this country
:return: a triangle emoji or empty string
"""
todayNew = dic["today"]["todayCases"] - dic["today"]["todayDeaths"] - dic["today"]["todayRecovered"]
yesterdayNew = dic["yesterday"]["todayCases"] - dic["yesterday"]["todayDeaths"] - dic["yesterday"][
"todayRecovered"]
return ":small_red_triangle:" if todayNew > yesterdayNew else \
(":small_red_triangle_down:" if todayNew < yesterdayNew else "")
# Returns an arrow indicating rise or decline
def trendIndicator(self, dic, key):
"""
Function that returns a rise/decline indicator for the target key.
:param dic: the today/yesterday dictionary for this country
:param key: the key to get the indicator for
:return: a triangle emoji or empty string
"""
return ":small_red_triangle:" if dic["today"][key] > dic["yesterday"][key] else \
(":small_red_triangle_down:" if dic["today"][key] < dic["yesterday"][key] else "")
# Also returns an indicator, but compares instead of pulling it out of the dic (for custom numbers)
def indicator(self, today, yesterday):
"""
Function that also returns an indicator but for two numbers
instead of comparing values out of the dictionary.
:param today: the number representing today
:param yesterday: the number representing yesterday
:return: a triangle emoji or empty string
"""
return ":small_red_triangle:" if today > yesterday else \
(":small_red_triangle_down:" if yesterday > today else "")
def setup(client):
client.add_cog(Corona(client))

View File

@ -1,30 +0,0 @@
from data.embeds.urban_dictionary import Definition
from decorators import help
from discord.ext import commands
from enums.help_categories import Category
from functions import checks
class Define(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.command(name="Define", aliases=["UrbanDictionary", "Ud"], usage="[Woord]")
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Other)
async def define(self, ctx, *, query):
"""
Command that looks up the definition of a word in the Urban Dictionary.
:param ctx: Discord Context
:param query: Word(s) to look up
"""
embed = Definition(query).to_embed()
await ctx.send(embed=embed)
def setup(client):
client.add_cog(Define(client))

View File

@ -1,567 +0,0 @@
from converters.numbers import Abbreviated, abbreviated
from data import constants
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from enums.numbers import Numbers
from functions import checks
from functions.database import currency, prison, stats
from functions.numbers import getRep
import json
import math
import random
def calcCapacity(level):
"""
Function that calculates the rob capacity for a given level.
:param level: the level of the user
:return: the capacity the user can rob (float)
"""
cap = 200
for x in range(level):
cap *= (math.pow(1.03, x))
return round(cap)
class Dinks(commands.Cog):
def __init__(self, client):
self.client = client
self.utilsCog = self.client.get_cog("Utils")
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.command(name="Award", aliases=["Reward"], usage="[@Persoon] [Aantal]", hidden=True)
@commands.check(checks.isMe)
@help.Category(category=Category.Mod)
async def award(self, ctx, user: discord.User, amount: Abbreviated):
"""
Command that awards a user a certain amount of Didier Dinks.
:param ctx: Discord Context
:param user: the user to give the Didier Dinks to
:param amount: the amount of Didier Dinks to award [user]
"""
# No amount was passed
if amount is None:
return
# Update the db
currency.update(user.id, "dinks", float(currency.dinks(user.id)) + float(amount))
# Gets the abbreviated representation of the amount
rep = getRep(amount, Numbers.t.value)
await ctx.send("**{}** heeft **{}** zowaar **{}** Didier Dink{} beloond!"
.format(ctx.author.display_name, self.utilsCog.getDisplayName(ctx, user.id), rep, checks.pluralS(amount)))
@commands.group(name="Dinks", aliases=["Cash"], case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Currency)
async def dinks(self, ctx):
"""
Command that shows the user's Didier Dinks & Platinum Dinks
:param ctx: Discord Context
"""
dinks = currency.dinksAll(ctx.author.id)
answer = "**{}** heeft **{:,}** Didier Dink{}"\
.format(ctx.author.display_name, math.floor(dinks["dinks"]), checks.pluralS(dinks["dinks"]))
if dinks["platinum"] > 0:
answer += " en **{}** Platinum Dink{}".format(dinks["platinum"], checks.pluralS(dinks["platinum"]))
await ctx.send(answer + "!")
@dinks.command(aliases=["Lb", "Leaderboards"], hidden=True)
@commands.check(checks.allowedChannels)
async def leaderboard(self, ctx):
"""
Command that shows the Didier Dinks Leaderboard.
Alias for Lb Dinks.
:param ctx: Discord Context
"""
await self.client.get_cog("Leaderboards").callLeaderboard("dinks", ctx)
@commands.command(name="Nightly")
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Currency)
async def nightly(self, ctx):
"""
Command to claim daily Didier Dinks.
:param ctx: Discord Context
"""
response = currency.nightly(int(ctx.author.id))
if response[0]:
# Claim successful
await ctx.send("Je hebt je dagelijkse **{:,}** Didier Dinks geclaimt. :fire:**{}**".format(
response[1], response[2]))
else:
# Already claimed today, react PIPO
await ctx.send("Je kan dit niet meerdere keren per dag doen.")
reactCog = self.client.get_cog("ReactWord")
await reactCog.react(ctx, "pipo")
@commands.command(name="Give", aliases=["Gift"], usage="[@Persoon] [Aantal]")
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Currency)
async def give(self, ctx, person: discord.Member, amount: Abbreviated):
"""
Command that gives your Didier Dinks to another user.
:param ctx: Discord Context
:param person: user to give the Didier Dinks to
:param amount: the amount of Didier Dinks to give
"""
# Invalid amount
if amount is None:
return
# Disable DM abuse
if ctx.guild is None:
return await ctx.send("Muttn")
valid = checks.isValidAmount(ctx, amount)
if not valid[0]:
return await ctx.send(valid[1])
amount = float(valid[1])
currency.update(ctx.author.id, "dinks", float(currency.dinks(ctx.author.id)) - amount)
currency.update(person.id, "dinks", float(currency.dinks(person.id)) + amount)
rep = getRep(math.floor(amount), Numbers.t.value)
await ctx.send("**{}** heeft **{}** zowaar **{}** Didier Dink{} geschonken!"
.format(ctx.author.display_name, person.display_name,
rep, checks.pluralS(amount)))
@commands.group(name="Bank", aliases=["B"], case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Currency)
async def bank(self, ctx):
"""
Command that shows the user's Didier Bank.
:param ctx: Discord Context
"""
# 0 1 2 3 4 5 6 7 8 9 10
# ID dinks level investedamount investeddays profit defense offense bc nightly streak
response = currency.getOrAddUser(ctx.author.id)
# Calculate the cost to level your bank
interestLevelPrice = round(math.pow(1.28, int(response[2])) * 300)
ratio = round(float(1 * (1 + (int(response[2]) * 0.01))), 4)
# Calculate the amount of levels the user can purchase
counter = 0
sumPrice = float(math.pow(1.28, int(response[2])) * 300)
while float(response[1]) + float(response[3]) + float(response[5]) > sumPrice:
counter += 1
sumPrice += round(float(math.pow(1.28, int(response[2]) + counter) * 300), 4)
maxLevels = "" if counter == 0 else " (+{})".format(str(counter))
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Bank van {}".format(ctx.author.display_name))
embed.set_thumbnail(url=str(ctx.author.avatar.url))
embed.add_field(name="Level:", value=str(response[2]) + maxLevels, inline=True)
embed.add_field(name="Ratio:", value=str(ratio), inline=True)
embed.add_field(name="Prijs voor volgend level:", value="{:,}".format(interestLevelPrice), inline=False)
embed.add_field(name="Momenteel geïnvesteerd:", value="{:,}".format(math.floor(float(response[3]))), inline=False)
embed.add_field(name="Aantal dagen geïnvesteerd:", value=str(response[4]), inline=True)
embed.add_field(name="Huidige winst na claim:", value="{:,}".format(math.floor(response[5])), inline=False)
await ctx.send(embed=embed)
@bank.command(name="Stats")
async def stats(self, ctx):
"""
Command that shows the user's bank stats.
:param ctx: Discord Context
"""
response = currency.getOrAddUser(ctx.author.id)
# Calculate the prices to level stats up
defense = int(response[6])
defenseLevelPrice = math.floor(math.pow(1.4, defense) * 365) if defense < 38 else 5 * calcCapacity(defense - 6)
offense = int(response[7])
capacity = calcCapacity(offense)
offenseLevelPrice = math.floor(math.pow(1.5, offense) * 369) if offense < 32 else 5 * capacity
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Bank van {}".format(ctx.author.display_name))
embed.add_field(name="Offense:", value=str(offense), inline=True)
embed.add_field(name="Prijs voor volgend level:", value="{:,}".format(int(offenseLevelPrice)), inline=True)
embed.add_field(name="Capaciteit:", value="{:,}".format(int(capacity)), inline=True)
embed.add_field(name="Security:", value=str(defense), inline=True)
embed.add_field(name="Prijs voor volgend level:", value="{:,}".format(int(defenseLevelPrice)), inline=True)
await ctx.send(embed=embed)
@bank.group(name="Upgrade", aliases=["U"], case_insensitive=True, usage="[Categorie]", invoke_without_command=True)
async def upgrade(self, ctx):
"""
Command group to upgrade bank stats,
calling the group itself does nothing.
:param ctx: Discord Context
"""
pass
@upgrade.command(name="Level", aliases=["L"], hidden=True)
async def level(self, ctx):
"""
Command that upgrades the user's bank level,
increasing interest.
:param ctx: Discord Context
"""
response = currency.getOrAddUser(ctx.author.id)
interestLevelPrice = float(math.pow(1.28, int(response[2])) * 300)
# Check if user has enough Didier Dinks to do this
if float(response[1]) >= interestLevelPrice:
currency.update(ctx.author.id, "dinks", float(response[1]) - interestLevelPrice)
currency.update(ctx.author.id, "banklevel", int(response[2]) + 1)
await ctx.send("**{}** heeft zijn bank geüpgradet naar level **{}**!"
.format(ctx.author.display_name, str(int(response[2]) + 1)))
else:
await ctx.send("Je hebt niet genoeg Didier Dinks om dit te doen, **{}**."
.format(ctx.author.display_name))
@upgrade.command(aliases=["Cap", "Capacity", "O", "Offence"], hidden=True)
async def offense(self, ctx):
"""
Command that upgrades the user's bank offense,
increasing capacity & rob chances.
:param ctx: Discord Context
"""
response = currency.getOrAddUser(ctx.author.id)
offense = int(response[7])
capacity = calcCapacity(offense)
offenseLevelPrice = math.floor(math.pow(1.5, offense) * 369) if offense < 32 else 5 * capacity
# Check if user has enough Didier Dinks to do this
if float(response[1]) >= offenseLevelPrice:
currency.update(ctx.author.id, "dinks", float(response[1]) - offenseLevelPrice)
currency.update(ctx.author.id, "offense", int(response[7]) + 1)
await ctx.send("**{}** heeft de offense van zijn bank geüpgradet naar level **{}**!"
.format(ctx.author.display_name, int(response[7]) + 1))
else:
await ctx.send("Je hebt niet genoeg Didier Dinks om dit te doen, **{}**."
.format(ctx.author.display_name))
@upgrade.command(aliases=["D", "Defence", "Def", "Security"], hidden=True)
async def defense(self, ctx):
"""
Command that upgrades the user's bank defense,
increasing chance of failed robs by others.
:param ctx: Discord Context
"""
response = currency.getOrAddUser(ctx.author.id)
defense = int(response[6])
defenseLevelPrice = math.floor(math.pow(1.4, defense) * 365) if defense < 38 else 5 * calcCapacity(defense - 6)
# Check if user has enough Didier Dinks to do this
if float(response[1]) >= defenseLevelPrice:
currency.update(ctx.author.id, "dinks", float(response[1]) - defenseLevelPrice)
currency.update(ctx.author.id, "defense", int(response[6]) + 1)
await ctx.send("**{}** heeft de security van zijn bank geüpgradet naar level **{}**!"
.format(ctx.author.display_name, int(response[6]) + 1))
else:
await ctx.send("Je hebt niet genoeg Didier Dinks om dit te doen, **{}**."
.format(ctx.author.display_name))
@commands.command(name="Invest", aliases=["Deposit"], usage="[Aantal]")
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Currency)
async def invest(self, ctx, *amount: Abbreviated):
"""
Command that invests Didier Dinks into the user's bank.
:param ctx: Discord Context
:param amount: the amount of Didier Dinks to invest
"""
# Tuples don't support assignment
amount = list(amount)
if len(amount) != 1:
await ctx.send("Geef een geldig bedrag op.")
elif not checks.isValidAmount(ctx, amount[0])[0]:
await ctx.send(checks.isValidAmount(ctx, amount[0])[1])
else:
user = currency.getOrAddUser(ctx.author.id)
if str(amount[0]).lower() == "all":
amount[0] = user[1]
amount[0] = float(amount[0])
currency.update(ctx.author.id, "investedamount", float(user[3]) + amount[0])
currency.update(ctx.author.id, "dinks", float(user[1]) - amount[0])
await ctx.send("**{}** heeft **{:,}** Didier Dink{} geïnvesteerd!"
.format(ctx.author.display_name, math.floor(amount[0]), checks.pluralS(amount[0])))
@commands.command(name="Claim", usage="[Aantal]*")
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Currency)
async def claim(self, ctx, *args):
"""
Command that claims profit out of the user's Didier Bank.
:param ctx:
:param args:
:return:
"""
user = currency.getOrAddUser(ctx.author.id)
args = list(args)
claimAll = False
if len(args) == 0:
args.append("all")
if args[0] == "all":
args[0] = float(user[5])
claimAll = True
if not claimAll:
args[0] = abbreviated(str(args[0]))
if args[0] is None:
return await ctx.send("Dit is geen geldig bedrag.")
try:
# Checks if it can be parsed to int
_ = int(args[0])
args[0] = float(args[0])
# Can't claim more than you have (or negative amounts)
if args[0] < 0 or args[0] > float(user[5]):
raise ValueError
currency.update(ctx.author.id, "profit", float(user[5]) - args[0])
currency.update(ctx.author.id, "dinks", float(user[1]) + args[0])
s = stats.getOrAddUser(ctx.author.id)
stats.update(ctx.author.id, "profit", float(s[7]) + args[0])
# If you claim everything, you get your invest back as well & your days reset
if claimAll:
currency.update(ctx.author.id, "dinks", float(user[1]) + float(user[3]) + float(user[5]))
currency.update(ctx.author.id, "investedamount", 0.0)
currency.update(ctx.author.id, "investeddays", 0)
await ctx.send("**{}** heeft **{:,}** Didier Dink{} geclaimt!"
.format(ctx.author.display_name, math.floor(args[0] + float(user[3])),
checks.pluralS(math.floor(args[0] + float(user[3])))))
else:
await ctx.send("**{}** heeft **{:,}** Didier Dink{} geclaimt!".format(
ctx.author.display_name, math.floor(args[0]), checks.pluralS(math.floor(args[0]))))
except ValueError:
await ctx.send("Geef een geldig bedrag op.")
@commands.group(name="Rob", usage="[@Persoon]", case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Currency)
async def rob(self, ctx, target: discord.User):
"""
Command to rob another user.
:param ctx: Discord Context
:param target: the target victim to be robbed
:return:
"""
canRob, caller, target = await self.canRob(ctx, target)
if not canRob:
return
threshold = 50 + round(int(target[6]) * 0.7)
rg = random.randint(0 + int(caller[7]), 100)
stat = stats.getOrAddUser(ctx.author.id)
# Rob succeeded
if rg > threshold:
capacity = float(calcCapacity(caller[7]))
remaining = capacity
# Try robbing out of invest first, then Dinks pouch
amount = capacity if float(target[3]) >= capacity else float(target[3])
remaining -= amount
currency.update(target[0], "investedamount", float(target[3]) - amount)
# Rob out of Dinks pouch
if amount != capacity and not float(target[1]) < 1:
if float(target[1]) >= remaining:
amount += remaining
currency.update(target[0], "dinks", float(target[1]) - remaining)
else:
amount += float(target[1])
currency.update(target[0], "dinks", 0.0)
# Update db
currency.update(caller[0], "dinks", float(caller[1]) + amount)
await ctx.send("**{}** heeft **{:,}** Didier Dink{} gestolen van **{}**!".format(
ctx.author.display_name, math.floor(amount), checks.pluralS(math.floor(amount)),
self.utilsCog.getDisplayName(ctx, target[0])
))
stats.update(ctx.author.id, "robs_success", int(stat[2]) + 1)
stats.update(ctx.author.id, "robs_total", float(stat[4]) + amount)
else:
# Rob failed
# Calculate what happens
fate = random.randint(1, 10)
# Leave Dinks behind instead of robbing
if fate < 8:
punishment = float(calcCapacity(caller[7]))/2
prisoned = round(float(caller[1])) < round(punishment)
# Doesn't have enough Dinks -> prison
if prisoned:
diff = round(punishment - float(caller[1]))
punishment = round(float(caller[1]))
days = 1 + round(int(caller[7]) // 10)
prison.imprison(caller[0], diff, days,
round(round(diff)//days))
# Update db
currency.update(target[0], "dinks", float(target[1]) + punishment)
currency.update(caller[0], "dinks", float(caller[1]) - punishment)
await ctx.send("**{}** was zo vriendelijk om **{}** zowaar **{:,}** Didier Dink{} te geven!"
.format(ctx.author.display_name,
self.utilsCog.getDisplayName(ctx, target[0]),
math.floor(punishment), checks.pluralS(math.floor(punishment))))
# Can't put this in the previous if- because the value of Punishment changes
if prisoned:
await ctx.send("Je bent naar de gevangenis verplaatst omdat je niet genoeg Didier Dinks had.")
elif fate == 9:
# Prison
totalSum = round(calcCapacity(caller[7]))
days = 1 + (int(caller[7])//10)
prison.imprison(caller[0], totalSum, days, totalSum/days)
await ctx.send("**{}** niet stelen, **{}** niet stelen!\nJe bent naar de gevangenis verplaatst.".format(
ctx.author.display_name, ctx.author.display_name
))
else:
# Escape
await ctx.send("Je poging is mislukt, maar je kon nog net op tijd vluchten, **{}**."
"\nAllez, 't is goed voor ene keer e, deugeniet.".format(ctx.author.display_name))
stats.update(ctx.author.id, "robs_failed", int(stat[3]) + 1)
@rob.command(name="Leaderboard", aliases=["Lb", "Leaderboards"], hidden=True)
async def rob_leaderboard(self, ctx):
"""
Command that shows the Rob Leaderboard.
Alias for Lb Rob.
:param ctx: Discord Context
"""
await self.client.get_cog("Leaderboards").callLeaderboard("rob", ctx)
@rob.command(name="Stats", hidden=True)
async def rob_stats(self, ctx):
"""
Command that shows the user's rob stats.
Alias for Stats Rob.
:param ctx: Discord Context
"""
await self.client.get_cog("Stats").callStats("rob", ctx)
@commands.command(name="Prison", aliases=["Jail"])
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Currency)
async def prison(self, ctx):
"""
Command that shows how long you have to sit in prison for.
:param ctx: Discord Context
"""
user = prison.getUser(ctx.author.id)
if len(user) == 0:
await ctx.send("Je zit niet in de gevangenis, **{}**.".format(ctx.author.display_name))
return
user = user[0]
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="De Gevangenis")
embed.add_field(name="Borgsom:", value="{:,}".format(math.floor(user[1])), inline=False)
embed.add_field(name="Resterende dagen:", value="{}".format((user[2])), inline=False)
await ctx.send(embed=embed)
@commands.command(name="Bail")
@commands.check(checks.allowedChannels)
@help.Category(Category.Currency)
async def bail(self, ctx):
"""
Command to bail yourself out of prison.
:param ctx: Discord Context
"""
user = prison.getUser(ctx.author.id)
if len(user) == 0:
return await ctx.send("Je zit niet in de gevangenis, **{}**.".format(ctx.author.display_name))
user = user[0]
# Check if user can afford this
valid = checks.isValidAmount(ctx, math.floor(user[1]))
if not valid[0]:
return await ctx.send(valid[1])
dinks = currency.dinks(ctx.author.id)
prison.remove(ctx.author.id)
currency.update(ctx.author.id, "dinks", float(dinks) - float(user[1]))
await ctx.send("**{}** heeft zichzelf vrijgekocht!".format(ctx.author.display_name))
# Update the user's stats
s = stats.getOrAddUser(ctx.author.id)
stats.update(ctx.author.id, "bails", int(s[10]) + 1)
# Increase the bail in the stats file
with open("files/stats.json", "r") as fp:
s = json.load(fp)
s["rob"]["bail_paid"] += float(user[1])
with open("files/stats.json", "w") as fp:
json.dump(s, fp)
async def canRob(self, ctx, target):
"""
Function that performs checks to see if a user can rob another user.
In case the rob is not possible, it already sends an error message to show this.
Returns the database dictionaries corresponding to these two users as they are
needed in this function anyways, so it prevents an unnecessary database call
in the rob command.
:param ctx: Discord Context
:param target: the target victim to be robbed
:return: success: boolean, user1 ("Caller"): tuple, user2 ("Target"): tuple
"""
# Can't rob in DM's
if str(ctx.channel.type) == "private":
await ctx.send("Dat doe je niet, {}.".format(ctx.author.display_name))
return False, None, None
# Can't rob bots
if str(ctx.author.id) in constants.botIDs:
await ctx.send("Nee.")
# Can't rob in prison
if len(prison.getUser(ctx.author.id)) != 0:
await ctx.send("Je kan niemand bestelen als je in de gevangenis zit.")
return False, None, None
# Check the database for these users
user1 = currency.getOrAddUser(ctx.author.id)
user2 = currency.getOrAddUser(target.id)
# Can't rob without Didier Dinks
if float(user1[1]) < 1.0:
await ctx.send("Mensen zonder Didier Dinks kunnen niet stelen.")
return False, None, None
# Target has no Didier Dinks to rob
if float(user2[1]) < 1.0 and float(user2[3]) < 1.0:
await ctx.send("Deze persoon heeft geen Didier Dinks om te stelen.")
return False, None, None
# Passed all tests
return True, user1, user2
def setup(client):
client.add_cog(Dinks(client))

View File

@ -1,384 +0,0 @@
from discord import Interaction
from data import constants
from data.snipe import Snipe, Action, should_snipe
import datetime
import discord
from discord.ext import commands
from functions import checks, easterEggResponses, stringFormatters
from functions.database import stats, muttn, custom_commands, commands as command_stats
import pytz
from settings import READY_MESSAGE, SANDBOX
from startup.didier import Didier
import time
class Events(commands.Cog):
def __init__(self, client: Didier):
self.client: Didier = client
self.utilsCog = self.client.get_cog("Utils")
self.failedChecksCog = self.client.get_cog("FailedChecks")
self.lastFeatureRequest = 0
self.lastBugReport = 0
@commands.Cog.listener()
async def on_connect(self):
"""
Function called when the bot connects to Discord.
"""
print("Connected")
@commands.Cog.listener()
async def on_ready(self):
"""
Function called when the bot is ready & done leading.
"""
print(READY_MESSAGE)
# Add constants to the client as a botvar
self.client.constants = constants.Live if SANDBOX else constants.Zandbak
@commands.Cog.listener()
async def on_message(self, message):
"""
Function called when someone sends a message the bot can see.
:param message: the discord.Message instance of the message
"""
# Check if the server is locked, if so only allow me (to unlock) & Didier (to send the message) to talk
if self.client.locked \
and message.guild is not None \
and str(message.author.id) != constants.myId \
and str(message.author.id) != constants.didierId:
# Auto unlock when someone sends a message past the current time
if time.time() > self.client.lockedUntil:
return await self.unlock(message.channel)
return await self.utilsCog.removeMessage(message)
# If FreeGamesCheck failed, remove the message & send the user a DM
if not checks.freeGamesCheck(message):
await self.failedChecksCog.freeGames(message)
# Boos React to people that call him Dider
if "dider" in message.content.lower() and str(message.author.id) not in [constants.myId, constants.didierId, constants.coolerDidierId]:
await message.add_reaction("<:boos:629603785840263179>")
# Check for other easter eggs
eER = easterEggResponses.control(self.client, message)
if eER:
await message.channel.send(eER)
# Check for custom commands
custom = custom_commands.is_custom_command(message)
if custom.id is not None:
await message.channel.send(custom.response)
# Earn XP & Message count
stats.sentMessage(message)
@commands.Cog.listener()
async def on_thread_join(self, thread: discord.Thread):
# Join threads automatically
if thread.me is None:
await thread.join()
@commands.Cog.listener()
async def on_command(self, ctx):
"""
Function called whenever someone invokes a command.
Logs commands in your terminal.
:param ctx: Discord Context
"""
print(stringFormatters.format_command_usage(ctx))
command_stats.invoked(command_stats.InvocationType.TextCommand)
@commands.Cog.listener()
async def on_command_error(self, ctx, err):
"""
Function called when a command throws an error.
:param ctx: Discord Context
:param err: the error thrown
"""
# Debugging Didier shouldn't spam the error logs
if self.client.user.id != int(constants.didierId):
raise err
# Don't handle commands that have their own custom error handler
if hasattr(ctx.command, 'on_error'):
return
# Someone just mentioned Didier without calling a real command,
# don't care about this error
if isinstance(err, (commands.CommandNotFound, commands.CheckFailure, commands.TooManyArguments, commands.ExpectedClosingQuoteError), ):
pass
# Someone used a command that was on cooldown
elif isinstance(err, commands.CommandOnCooldown):
await ctx.send("Je kan dit commando niet (meer) spammen.", delete_after=10)
elif isinstance(err, commands.MessageNotFound):
await ctx.send("Geen message gevonden die overeenkomt met het opgegeven argument.")
elif isinstance(err, (commands.ChannelNotFound, commands.ChannelNotReadable)):
await ctx.send("Geen channel gevonden dat overeenkomt met het opgegeven argument.")
elif isinstance(err, commands.ThreadNotFound):
await ctx.reply("Thread niet gevonden.", mention_author=False)
# Someone forgot an argument or passed an invalid argument
elif isinstance(err, (commands.BadArgument, commands.MissingRequiredArgument, commands.UnexpectedQuoteError)):
await ctx.reply("Controleer je argumenten.", mention_author=False)
else:
usage = stringFormatters.format_command_usage(ctx)
await self.sendErrorEmbed(err, "Command", usage)
@commands.Cog.listener()
async def on_interaction(self, interaction: Interaction):
"""
Function called whenever someone uses a slash command
"""
if not interaction.is_command():
return
print(stringFormatters.format_slash_command_usage(interaction))
command_stats.invoked(command_stats.InvocationType.SlashCommand)
@commands.Cog.listener()
async def on_application_command_error(self, ctx: discord.ApplicationContext, err):
# Debugging Didier shouldn't spam the error logs
if self.client.user.id != int(constants.didierId):
raise err
if isinstance(err, commands.CheckFailure):
return await ctx.respond("Je hebt geen toegang tot dit commando.", ephemeral=True)
elif isinstance(err, discord.NotFound):
print("Don't care")
return
usage = stringFormatters.format_slash_command_usage(ctx.interaction)
await self.sendErrorEmbed(err, "Slash Command", usage)
@commands.Cog.listener()
async def on_raw_reaction_add(self, react):
"""
Function called when someone adds a reaction to a message.
:param react: the RawReactionEvent associated with the reaction
"""
# Ignore RPS adding reacts
if self.client.get_user(react.user_id).bot:
return
# Feature request
if str(react.emoji) == "":
await self.sendReactEmbed(react, "Feature Request")
# Bug report
elif str(react.emoji) == "🐛":
await self.sendReactEmbed(react, "Bug Report")
# Muttn react
elif str(react.emoji) == "<:Muttn:761551956346798111>":
await self.addMuttn(react)
@commands.Cog.listener()
async def on_raw_reaction_remove(self, react):
"""
Function called when someone removes a reaction from a message.
:param react: the RawReactionEvent associated with the reaction
"""
# Decrease Muttn counter
if str(react.emoji) == "<:Muttn:761551956346798111>":
await self.removeMuttn(react)
async def removeMuttn(self, react):
"""
Function that decreases the Muttn counter for someone.
:param react: the RawReactionEvent associated with the reaction
"""
# Get the Message instance of the message
channel = self.client.get_channel(react.channel_id)
message = await channel.fetch_message(react.message_id)
muttn.removeMuttn(message)
async def addMuttn(self, react):
"""
Function that checks the Muttn counter for a message.
:param react: the RawReactionEvent associated with the reaction
"""
count = -1
# Get the Message instance of the message
channel = self.client.get_channel(react.channel_id)
message = await channel.fetch_message(react.message_id)
# Get the amount of reacts on this message
for reaction in message.reactions:
if str(reaction.emoji) == "<:Muttn:761551956346798111>":
count = reaction.count
for user in await reaction.users().flatten():
# Remove bot reacts
if user.bot:
count -= 1
break
# React was removed in the milliseconds the fetch_message needs to get the info
if count <= 0:
return
# Update the db
muttn.muttn(message.author.id, count, message.id)
def reactCheck(self, react, msg):
"""
Function that checks if feature requests/bug reports have been sent already.
:param react: the RawReactionEvent associated with the reaction
:param msg: the message this react was placed on
"""
# # Blacklist NinjaJay after spamming
# if react.user_id in [153162010576551946]:
# return False
# Don't spam DM's when something has already been reported
# Check if the react's count is 1
for reaction in msg.reactions:
if reaction.emoji == react.emoji.name:
return reaction.count == 1
async def sendReactEmbed(self, react, messageType):
"""
Function that sends a message in Zandbak with what's going on.
:param react: the RawReactionEvent associated with the reaction
:param messageType: the type of message to send
"""
channel = self.client.get_channel(react.channel_id)
msg = await channel.fetch_message(react.message_id)
# Didn't pass the check, ignore it
if not self.reactCheck(react, msg):
return
typeChannels = {"Feature Request": int(constants.FeatureRequests), "Bug Report": int(constants.BugReports)}
# Add a 10 second cooldown to requests/reports to avoid spam
# even tho apparently the people don't care
if round(time.time()) - (
self.lastFeatureRequest if messageType == "Feature Request" else self.lastBugReport) < 10:
await channel.send("Je moet even wachten vooraleer je nog een {} maakt.".format(messageType.lower()))
await msg.add_reaction("🕐")
return
# Report on an empty message
elif msg.content == "":
await channel.send("Dit bericht bevat geen tekst.")
await msg.add_reaction("")
return
# Update the variables
if messageType == "Feature Request":
self.lastFeatureRequest = round(time.time())
else:
self.lastBugReport = round(time.time())
# Ignore people reacting on Didier's messages
if str(msg.author.id) != constants.didierId:
# Get the user's User instance & the channel to send the message to
COC = self.client.get_guild(int(constants.CallOfCode))
user = COC.get_member(react.user_id)
targetChannel = self.client.get_channel(typeChannels[messageType])
await targetChannel.send("{} door **{}** in **#{}** ({}):\n``{}``\n{}".format(
messageType,
user.display_name,
channel.name if str(channel.type) != "private" else "DM",
channel.guild.name if str(channel.type) != "private" else "DM",
msg.content, msg.jump_url
))
await msg.add_reaction("")
@commands.Cog.listener()
async def on_message_edit(self, before: discord.Message, after: discord.Message):
"""
Function called when a message is edited,
so people can't edit messages in FreeGames to cheat the system.
:param before: the message before it was edited
:param after: the message after it was edited
"""
# Run the message through the checks again
if not checks.freeGamesCheck(after):
return await self.failedChecksCog.freeGames(after)
if before.content and after.content and should_snipe(before):
self.client.snipe[before.channel.id] = Snipe(before.author.id, before.channel.id, before.guild.id, Action.Edit,
before.content, after.content)
@commands.Cog.listener()
async def on_message_delete(self, message: discord.Message):
if should_snipe(message):
self.client.snipe[message.channel.id] = Snipe(message.author.id, message.channel.id, message.guild.id,
Action.Remove, message.content)
async def sendErrorEmbed(self, error: Exception, error_type: str, usage: str):
"""
Function that sends an error embed in #ErrorLogs.
"""
trace = stringFormatters.format_error_tb(error)
embed = discord.Embed(colour=discord.Colour.red())
embed.set_author(name="Error")
embed.add_field(name=f"{error_type}:", value=usage, inline=False)
embed.add_field(name="Error:", value=str(error)[:1024], inline=False)
embed.add_field(name="Message:", value=str(trace)[:1024], inline=False)
# Add remaining parts in extra fields
# (embed field limits)
if len(str(trace)) < 5500:
trace_split = [str(trace)[i:i + 1024] for i in range(1024, len(str(trace)), 1024)]
for spl in trace_split:
embed.add_field(name="\u200b", value=spl, inline=False)
errorChannel = self.client.get_channel(762668505455132722)
await errorChannel.send(embed=embed)
@commands.command(hidden=True)
@commands.check(checks.isMe)
async def lock(self, ctx, until=None):
"""
Command that locks the server during online exams.
:param ctx: Discord Context
:param until: the timestamp until which to lock (HH:MM)
"""
# No timestamp passed
if until is None:
return
until = until.split(":")
# Create timestamps
now = datetime.datetime.now()
untilTimestamp = time.time()
# Gets the current amount of minutes into the day
nowMinuteCount = (now.hour * 60) + now.minute
# Gets the target amount of minutes into the day
untilMinuteCount = (int(until[0]) * 60) + int(until[1])
# Adds the remaining seconds onto the current time to calculate the end of the lock
untilTimestamp += (60 * (untilMinuteCount - nowMinuteCount)) - now.second
self.client.locked = True
self.client.lockedUntil = round(untilTimestamp)
await ctx.send("De server wordt gelocked tot **{}**.".format(
datetime.datetime.fromtimestamp(untilTimestamp,
pytz.timezone("Europe/Brussels")
).strftime('%H:%M:%S')))
@commands.command(hidden=True)
@commands.check(checks.isMe)
async def unlock(self, ctx):
"""
Command to unlock the server manually before the timer is over.
:param ctx: Discord Context
"""
self.client.locked = False
self.client.lockedUntil = -1
await ctx.send("De server is niet langer gelocked.")
def setup(client):
client.add_cog(Events(client))

View File

@ -1,28 +0,0 @@
from data import constants
from discord.ext import commands
# Cog that handles failure of checks
# Has to be a Cog to have access to the Client
class FailedChecks(commands.Cog):
def __init__(self, client):
self.client = client
self.utilsCog = self.client.get_cog("Utils")
# User posted in #FreeGames without being allowed to do so
async def freeGames(self, ctx):
content = ctx.content
errorChannel = self.client.get_channel(int(constants.ErrorLogs))
await self.utilsCog.removeMessage(ctx)
await self.utilsCog.sendDm(ctx.author.id,
"Je bericht \n`{}`\n werd verwijderd uit #FreeGames omdat het geen link "
"bevatte.\nPost AUB enkel links in dit kanaal.\n*Als je bericht onterecht "
"verwijderd werd, stuur dan een DM naar DJ STIJN.*".format(content))
await errorChannel.send("`{}`\nDoor **{}** werd verwijderd uit #FreeGames.".format(content,
ctx.author.display_name))
def setup(client):
client.add_cog(FailedChecks(client))

View File

@ -1,178 +0,0 @@
from data import constants
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import stringFormatters, checks
from functions.database import faq
class Faq(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.group(name="FAQ", usage="[Categorie]* [@Personen]*", case_insensitive=True, invoke_without_command=True)
@help.Category(category=Category.Other)
async def faq(self, ctx, *args):
"""
Command group that controls the FAQ commands.
When this command is invoked, it sends a list of valid categories.
After invoking in a subject's channel (without passing a category),
it sends the FAQ for that subject instead.
:param ctx: Discord Context
:param args: args passed
"""
# A category was requested
# This is not the cleanest but 80 subcommands is a bit much
if len(args) != 0 and any("@" not in arg for arg in args):
return await self.faqCategory(ctx, args)
# Check if the command was used in a subject's channel
if ctx.channel.id in constants.faq_channels:
return await self.faqCategory(ctx, (constants.faq_channels[ctx.channel.id],))
# List of all categories with the first letter capitalized
resp = [stringFormatters.title_case(cat[0]) for cat in faq.getCategories()]
# Sort alphabetically
resp.sort()
# Create an embed with all the categories
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="FAQ Categorieën")
embed.description = "\n".join(resp)
# Check if the embed has to be sent to the user
# or if the user tagged anyone
if len(ctx.message.mentions) == 0:
await ctx.author.send(embed=embed)
else:
embed.set_footer(text="Doorgestuurd door {}".format(ctx.author.display_name))
# Send it to everyone that was mentioned
for person in ctx.message.mentions:
if not person.bot:
await person.send(embed=embed)
@faq.command(hidden=True, name="Add", usage="[Category] [Question]* [Answer]*")
@commands.check(checks.isMe)
async def add(self, ctx, category, question=None, answer=None, answer_markdown=None):
"""
Command to add a FAQ to the database
:param ctx: Discord Context
:param category: the category to add the FAQ to
:param question: the question
:param answer: the answer
:param answer_markdown: a version of the answer with markdown applied
"""
# Add a new category
if question is None or answer is None:
faq.addCategory(category)
await ctx.send("**{}** is toegevoegd.".format(category))
else:
# Add a new question/answer couple to a category
faq.addQuestion(category, question, answer, answer_markdown)
await ctx.send("``{}\n{}`` is toegevoegd in {}.".format(question, answer, category))
# Quotes a specific line of the fac instead of DM'ing the entire thing
@faq.command(name="Quote", aliases=["Q"], usage="[Categorie] [Index]")
@help.Category(category=Category.Other)
async def quote(self, ctx, category, index):
"""
Command that quotes 1 line of the FAQ into the current channel.
:param ctx: Discord Context
:param category: the category of the FAQ
:param index: the index in the list to quote
:return:y
"""
# Check if a (valid) number was passed
try:
index = int(index)
if index < 1:
raise ValueError
except ValueError:
await ctx.send("Dit is geen geldig getal.")
# Create a list of categories
categories = [t[0] for t in faq.getCategories()]
# Check if a valid category was passed
if category.lower() not in categories:
return await ctx.send("Dit is geen geldige categorie.")
resp = faq.getCategory(category.lower())
# Check if this index exists in this category
if len(resp) < index:
return await ctx.send("Dit is geen geldig getal.")
# Sort by entry Id
resp.sort(key=lambda x: int(x[0]))
await ctx.send("**{}**\n{}".format(resp[index - 1][2], resp[index - 1][3]))
async def faqCategory(self, ctx, args):
"""
Function that sends everything from a category.
:param ctx: Discord Context
:param args: the args passed
"""
# Create a list of categories
categories = [t[0] for t in faq.getCategories()]
# Random word was passed as a category
if not any(arg.lower() in categories for arg in args):
return await self.sendErrorEmbed(ctx, "Dit is geen geldige categorie.")
elif len(args) - len(ctx.message.mentions) != 1:
# Multiple categories were requested, which is not allowed
return await self.sendErrorEmbed(ctx, "Controleer je argumenten.")
category = ""
# Find the category the user requested
for word in args:
if word.lower() in categories:
category = word
break
resp = faq.getCategory(category.lower())
# Sort by entry Id
resp.sort(key=lambda x: int(x[0]))
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="FAQ {}".format(stringFormatters.title_case(category)))
# Add everything into the embed
for i, pair in enumerate(resp):
# Add custom markdown if it exists
embed.add_field(name="#{}: {}".format(str(i + 1), pair[2]), value=pair[3] if pair[4] is None else pair[4], inline=False)
# Check if anyone was tagged to send the embed to
if len(ctx.message.mentions) == 0:
await ctx.author.send(embed=embed)
else:
embed.set_footer(text="Doorgestuurd door {}".format(ctx.author.display_name))
# Author tagged some people to send it to
for person in ctx.message.mentions:
await person.send(embed=embed)
async def sendErrorEmbed(self, ctx, message: str):
"""
Function that sends an error embed.
:param ctx: Discord Context
:param message: the message to put in the embed
"""
embed = discord.Embed(colour=discord.Colour.red())
embed.set_author(name="Error")
embed.description = message
await ctx.send(embed=embed)
def setup(client):
client.add_cog(Faq(client))

View File

@ -1,43 +0,0 @@
from decorators import help
from discord.ext import commands
from enums.help_categories import Category
from functions import checks, config
from functions.football import get_matches, get_table, get_jpl_code
class Football(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.group(name="Jpl", case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(Category.Sports)
async def jpl(self, ctx, *args):
pass
@jpl.command(name="Matches", aliases=["M"], usage="[Week]*")
async def matches(self, ctx, day: int = None):
# Default is current day
if day is None:
day = int(config.get("jpl_day"))
await ctx.send(get_matches(day))
@jpl.command(name="Table", aliases=["Ranking", "Rankings", "Ranks", "T"])
async def table(self, ctx):
await ctx.send(get_table())
@commands.check(checks.isMe)
@jpl.command(name="Update")
async def update(self, ctx):
code = get_jpl_code()
config.config("jpl", code)
await ctx.message.add_reaction("")
def setup(client):
client.add_cog(Football(client))

View File

@ -1,137 +0,0 @@
import json
import random
import requests
from discord.ext import commands
from data.embeds.xkcd import XKCDEmbed
from data.menus.memes import MemesList
from decorators import help
from enums.help_categories import Category
from functions import checks
from functions.database import memes, trump, dadjoke
from functions.memes import generate
class Fun(commands.Cog):
def __init__(self, client):
self.client = client
self.utilsCog = self.client.get_cog("Utils")
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.command(name="Dadjoke", aliases=["Dj", "Dad"])
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Fun)
async def dadjoke(self, ctx):
"""
Command that sends a random dad joke.
:param ctx: Discord Context
"""
await ctx.send(dadjoke.getRandomJoke())
@commands.command(name="Stalin", aliases=["Ss", "StalinMotivation", "Motivate"])
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Quotes)
async def stalin(self, ctx):
"""
Command that sends a random Stalin quote.
:param ctx: Discord Context
"""
with open("files/StalinMotivation.json", "r") as fp:
file = json.load(fp)
await ctx.send(file[random.randint(1, len(file))])
@commands.command(name="Satan", aliases=["S8n", "SatanQuote"])
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Quotes)
async def satan(self, ctx):
"""
Command that sends a random Satan quote.
:param ctx: Discord Context
"""
with open("files/SatanQuotes.json", "r") as fp:
file = json.load(fp)
await ctx.send(file[random.randint(1, len(file))])
@commands.command(name="Trump")
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Quotes)
async def trump(self, ctx):
"""
Command that sends a random Trump quote.
:param ctx: Discord Context
"""
quote = trump.getRandomQuote()
await ctx.send("**\"{}**\"\n{} - {}".format(quote[1], quote[2], quote[3]))
@commands.command(name="8-Ball", aliases=["8b", "8Ball"], ignore_extra=True)
@help.Category(category=Category.Quotes)
async def eightball(self, ctx):
"""
Command that sends a random 8-ball response.
:param ctx: Discord Context
"""
with open("files/eightball.json", "r") as fp:
file = json.load(fp)
await ctx.send(file[random.randint(0, len(file) - 1)])
@commands.command(name="Memegen", usage="[Meme] [Velden]")
@commands.cooldown(1, 5, commands.BucketType.guild)
@help.Category(category=Category.Fun)
async def memegen(self, ctx, name="", *fields):
"""
Command that generates memes.
:param ctx: Discord Context
:param name: the name of the meme
:param fields: the fields to add to the meme
"""
if len(fields) == 0:
return await ctx.send("Controleer je argumenten.")
# Get the meme info that corresponds to this name
result: memes.Meme = memes.getMeme(name)
# No meme found
if result is None:
return await ctx.send("Deze meme staat niet in de database.")
generated = generate(result, fields)
# Send the meme's url or the error message
await ctx.reply(generated["message"], mention_author=False)
@commands.command(name="Memes")
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Fun)
async def memes(self, ctx):
"""
Command that shows a list of memes in the database.
:param ctx: Discord Context
"""
return await MemesList(ctx=ctx).send()
@commands.command(name="Pjoke")
@help.Category(category=Category.Fun)
async def pjoke(self, ctx):
"""
Command that sends a random programming joke.
:param ctx: Discord Context
"""
r = requests.get("https://official-joke-api.appspot.com/jokes/programming/random").json()
await ctx.send(r[0]["setup"] + "\n" + r[0]["punchline"])
@commands.command(name="xkcd")
@help.Category(category=Category.Fun)
async def xkcd(self, ctx, n: int = None):
"""
Send an xkcd comic
"""
return await ctx.reply(embed=XKCDEmbed(n).create())
def setup(client):
client.add_cog(Fun(client))

View File

@ -1,257 +0,0 @@
from converters.numbers import Abbreviated, abbreviated
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import checks, dinks
from functions.database import currency, stats
import json
import math
import random
class Games(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.group(name="Coinflip", aliases=["Cf"], usage="[Inzet]* [Aantal]*", case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Gamble)
async def coinflip(self, ctx, *args):
"""
Command to flip a coin, optionally for Didier Dinks.
:param ctx: Discord Context
:param args: bet & wager
"""
args = list(args)
choices = ["Kop", "Munt"]
result = random.choice(choices)
# No choice made & no wager
if len(args) == 0:
await ctx.send("**{}**!".format(result))
self.updateStats("cf", "h" if result == "Kop" else "t")
return
# Check for invalid args
if len(args) == 1 or args[0][0].lower() not in "kmht":
return await ctx.send("Controleer je argumenten.")
args[1] = abbreviated(args[1])
valid = checks.isValidAmount(ctx, args[1])
# Invalid amount
if not valid[0]:
return await ctx.send(valid[1])
# Allow full words, abbreviations, and English alternatives
args[0] = "k" if args[0][0].lower() == "k" or args[0][0].lower() == "h" else "m"
won = await self.gamble(ctx, args[0], result, valid[1], 2)
if won:
s = stats.getOrAddUser(ctx.author.id)
stats.update(ctx.author.id, "cf_wins", int(s[8]) + 1)
stats.update(ctx.author.id, "cf_profit", float(s[9]) + float(valid[1]))
self.updateStats("cf", "h" if result == "Kop" else "t")
@coinflip.command(name="Stats", hidden=True)
async def cf_stats(self, ctx):
return await self.client.get_cog("Stats").callStats("cf", ctx)
@commands.group(name="Dice", aliases=["Roll"], usage="[Inzet]* [Aantal]*", case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Gamble)
async def dice(self, ctx, *args):
"""
Command to roll a dice, optionally for Didier Dinks.
:param ctx: Discord Context
:param args: bet & wager
"""
args = list(args)
result = random.randint(1, 6)
# No choice made & no wager
if len(args) == 0:
self.updateStats("dice", result)
return await ctx.send(":game_die: **{}**!".format(result))
# Check for invalid args
if len(args) == 1 or not args[0].isdigit() or not 0 < int(args[0]) < 7:
return await ctx.send("Controleer je argumenten.")
args[1] = abbreviated(args[1])
valid = checks.isValidAmount(ctx, args[1])
# Invalid amount
if not valid[0]:
return await ctx.send(valid[1])
await self.gamble(ctx, args[0], str(result), valid[1], 6, ":game_die: ")
self.updateStats("dice", result)
@dice.command(name="Stats", hidden=True)
async def dice_stats(self, ctx):
await self.client.get_cog("Stats").callStats("dice", ctx)
async def gamble(self, ctx, bet, result, wager, factor, pre="", post=""):
"""
Function for gambling because it's the same thing every time.
:param ctx: Discord Context
:param bet: the option the user bet on
:param result: randomly generated result
:param wager: size of the bet of the user
:param factor: the factor by which the person's wager is amplified
:param pre: any string that might have to be pre-pended to the output string
:param post: any string that might have to be appended to the output string
:return: a boolean indicating whether or not the user has won
"""
# Code no longer first removes your bet to then add profit,
# resulting in triple coinflip profit (@Clement).
# Subtract one off of the factor to compensate for the initial wager
factor -= 1
answer = "**{}**! ".format(result)
won = False
# Check if won
if result[0].lower() == bet[0].lower():
won = True
answer += "Je wint **{:,}** Didier Dink{}"
currency.update(ctx.author.id, "dinks", float(currency.dinks(ctx.author.id)) + (float(wager) * factor))
else:
answer += "Je hebt je inzet (**{:,}** Didier Dink{}) verloren"
currency.update(ctx.author.id, "dinks", float(currency.dinks(ctx.author.id)) - float(wager))
self.loseDinks(round(float(wager)))
# If won -> multiple dinkS, if lost, it's possible that the user only bet on 1 dinK
await ctx.send(pre + answer.format(round(float(wager) * factor if won else float(wager)),
checks.pluralS(float(wager) * factor if won else float(wager))) +
", **{}**!".format(ctx.author.display_name))
return won
@commands.group(name="Slots", usage="[Aantal]", case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Gamble)
async def slots(self, ctx, wager: Abbreviated = None):
"""
Command to play slot machines.
:param ctx: Discord Context
:param wager: the amount of Didier Dinks to bet with
"""
valid = checks.isValidAmount(ctx, wager)
# Invalid amount
if not valid[0]:
return await ctx.send(valid[1])
ratios = dinks.getRatios()
def randomKey():
return random.choice(list(ratios.keys()))
def generateResults():
return [randomKey(), randomKey(), randomKey()]
# Generate the result
result = generateResults()
textFormatted = "{}\n{}\n:yellow_square:{}:yellow_square:\n:arrow_right:{}:arrow_left::part_alternation_mark:\n" \
":yellow_square:{}:yellow_square: :red_circle:\n{}\n{}".format(
dinks.slotsHeader, dinks.slotsEmptyRow,
"".join(generateResults()), "".join(result), "".join(generateResults()),
dinks.slotsEmptyRow, dinks.slotsFooter)
await ctx.send(textFormatted)
# Everything different -> no profit
if len(set(result)) == 3:
await ctx.send("Je hebt je inzet (**{:,}** Didier Dinks) verloren, **{}**.".format(
math.floor(float(valid[1])), ctx.author.display_name
))
currency.update(ctx.author.id, "dinks", float(currency.dinks(ctx.author.id)) - math.floor(float(valid[1])))
return
# Calculate the profit multiplier
multiplier = 1.0
for symbol in set(result):
multiplier *= ratios[symbol][result.count(symbol) - 1]
await ctx.send(":moneybag: Je wint **{:,}** Didier Dinks, **{}**! :moneybag:".format(
round(float(valid[1]) * multiplier, 2), ctx.author.display_name
))
currency.update(ctx.author.id, "dinks",
float(currency.dinks(ctx.author.id)) + (float(valid[1]) * multiplier) - math.floor(
float(valid[1])))
# Current Dinks - wager + profit
# Returns list of profits
@slots.command(name="Chart", aliases=["Symbols", "Profit"])
async def chart(self, ctx):
"""
Command to show the profit distributions for the Didier Slotmachines.
:param ctx: Discord Context
"""
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Slots Profit Chart")
ratios = dinks.getRatios()
# Add every symbol into the embed
for ratio in ratios:
embed.add_field(name=ratio, value="1: x{}\n2: x{}\n3: x{}".format(
str(ratios[ratio][0]), str(ratios[ratio][1]), str(ratios[ratio][2])
))
await ctx.send(embed=embed)
@commands.group(name="Lost", case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Gamble)
async def lost(self, ctx):
"""
Command that shows the amount of Didier Dinks that have been lost due to gambling.
:param ctx: Discord Context
"""
await ctx.send("Er zijn al **{:,}** Didier Dinks verloren sinds 13/03/2020."
.format(dinks.lost()))
@lost.command(name="Today")
async def today(self, ctx):
"""
Command that shows the amount of Didier Dinks lost today.
:param ctx: Discord Context
"""
await ctx.send("Er zijn vandaag al **{:,}** Didier Dinks verloren."
.format(dinks.lostToday()))
def updateStats(self, game, key):
"""
Function to update the stats file for a game.
:param game: the game to change the stats for
:param key: the key in the game's dict to update
"""
with open("files/stats.json", "r") as fp:
s = json.load(fp)
s[game][str(key)] += 1
with open("files/stats.json", "w") as fp:
json.dump(s, fp)
def loseDinks(self, amount):
"""
Function that adds Didier Dinks to the lost file.
:param amount: the amount of Didier Dinks lost
"""
with open("files/lost.json", "r") as fp:
fc = json.load(fp)
fc["lost"] = fc["lost"] + round(amount)
fc["today"] = fc["today"] + round(amount)
with open("files/lost.json", "w") as fp:
json.dump(fc, fp)
def setup(client):
client.add_cog(Games(client))

View File

@ -1,31 +0,0 @@
from discord.ext import commands
from decorators import help
from enums.help_categories import Category
from functions.scrapers.google import google_search, create_google_embed
class Google(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.command(name="Google", aliases=["Gtfm", "Search"], usage="[Query]", case_insensitive=True)
@help.Category(Category.Other)
async def google(self, ctx, *query):
if not query:
return await ctx.reply("Je hebt geen query opgegeven.", mention_author=True)
result = google_search(" ".join(query))
if not result.results:
return await ctx.send("Er ging iets fout (Response {})".format(result.status_code))
embed = create_google_embed(result)
await ctx.reply(embed=embed, mention_author=False)
def setup(client):
client.add_cog(Google(client))

View File

@ -1,170 +0,0 @@
from decorators import help
from discord.ext import commands
from enums.help_categories import Category
from functions import checks
import json
import os
import random
def randomWord():
lineb = random.randrange(os.stat("files/words-dutch.txt").st_size)
with open("files/words-dutch.txt", encoding="latin-1") as file:
file.seek(lineb)
file.readline()
return file.readline().rstrip().upper()
class Hangman(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.group(name="Hangman", aliases=["Hm"], usage="[Letter]*", case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Games)
async def hangman(self, ctx, letter=None):
if letter and letter.strip():
greek = "αβΓγΔδεζηΘθικΛλμνΞξΠπρΣσςτυΦφχΨψΩω"
if len(letter) == 1 and (letter.isalpha() or letter.isdigit()) and letter not in greek:
await self.guessLetter(ctx, letter)
else:
await ctx.send("Dit is geen geldige letter.")
return
await self.gameStatus(ctx)
async def guessLetter(self, ctx, letter):
with open("files/hangman.json", "r") as fp:
file = json.load(fp)
if letter.upper() in file["guessed"]:
await ctx.send("Deze letter is al eens geprobeerd.")
return
file["guessed"] += letter.upper()
if letter.upper() not in file["word"]:
file["guesses"] += 1
if int(file["guesses"] >= 9):
await ctx.send(self.endGame(file["word"]))
return
with open("files/hangman.json", "w") as fp:
json.dump(file, fp)
await self.gameStatus(ctx)
@hangman.command(name="Start", usage="[Woord]*")
async def start(self, ctx, *, word=None):
with open("files/hangman.json", "r") as fp:
file = json.load(fp)
# Can't play two games at once
if file["word"]:
await ctx.send("Er is al een spel bezig.")
return
if word:
if not all(letter.isalpha() or letter.isdigit() or letter in [" "] for letter in word):
await ctx.send("Dit is geen geldig woord.")
return
# Can only supply your own words in DM
if str(ctx.channel.type) != "private":
await ctx.message.delete()
await ctx.author.send("Het is niet slim om hangman games aan te maken waar iedereen het kan zien."
"\nGebruik dit commando hier zodat niemand het woord op voorhand weet.")
return
# Choose a random word when none were passed
if not word:
word = randomWord()
with open("files/hangman.json", "w") as fp:
json.dump({"guessed": [], "guesses": 0, "word": word}, fp)
await self.gameStatus(ctx)
async def gameStatus(self, ctx):
with open("files/hangman.json", "r") as fp:
file = json.load(fp)
if not file["word"]:
await ctx.send("Er is geen spel bezig.")
return
guessed = " ".join(letter for letter in file["guessed"] if letter not in file["word"])
filled = self.fillWord(file)
if filled.replace(" ", "") == file["word"]:
self.clearGame()
await ctx.send("**Het woord is geraden.**")
await ctx.message.add_reaction("")
return
await ctx.send("{}\n{}\nFoute letters: {}".format(filled, self.hangManString(file["guesses"]), guessed))
@hangman.command(name="Guess", usage="[Woord]")
async def guess(self, ctx, *, word=None):
if not word:
await ctx.send("Geef een woord op.")
return
with open("files/hangman.json", "r") as fp:
file = json.load(fp)
if not file["word"]:
await ctx.send("Er is geen spel bezig.")
return
if word.upper() == file["word"].upper():
self.clearGame()
await ctx.send("**Het woord is geraden**")
await ctx.message.add_reaction("")
else:
file["guesses"] += 1
await ctx.send("**{}** is een foute gok.".format(word))
await ctx.message.add_reaction("")
if file["guesses"] >= 9:
await ctx.send(self.endGame(file["word"]))
else:
with open("files/hangman.json", "w") as fp:
json.dump(file, fp)
await self.gameStatus(ctx)
# Create a representation of the word by filling in letters that have been guessed, and dots otherwise
def fillWord(self, file):
return "**" + " ".join(
letter if letter in file["guessed"] else "." if letter.isalpha() or letter.isdigit() else letter for letter
in file["word"]) + "**"
def endGame(self, word):
self.clearGame()
return self.hangManString(9) + "\nHet woord was **{}**.".format(word)
def clearGame(self):
with open("files/hangman.json", "w") as fp:
json.dump({"guessed": [], "guesses": 0, "word": ""}, fp)
def hangManString(self, number):
dic = {
0: "\n \n \n \n",
1: "\n \n \n \n ===",
2: "\n |\n |\n |\n ===",
3: "--+---+\n |\n |\n |\n ===",
4: "--+---+\n | O\n |\n |\n ===",
5: "--+---+\n | O\n | |\n |\n ===",
6: "--+---+\n | O\n | /|\n |\n ===",
7: "--+---+\n | O\n | /|\\\n |\n ===",
8: "--+---+\n | O\n | /|\\\n | /\n ===",
9: "--+---+\n | O\n | /|\\\n | / \\\n ===",
}
return dic[number]
def setup(client):
client.add_cog(Hangman(client))

View File

@ -1,223 +0,0 @@
from data import constants
import discord
from discord.commands import SlashCommand
from discord.ext import commands
from enums.help_categories import categories, getCategory, Category
import json
class HelpCommand(commands.MinimalHelpCommand):
def __init__(self, **options):
super().__init__(**options)
self.ctx = None
self.target = None
self.is_mod = False
# Slightly modifying the original callback in order to
# allow sending help to DM's & checking for Enums
# while removing cog help & checking for mentions
async def command_callback(self, ctx, *, command=None):
self.ctx = ctx
self.is_mod = str(ctx.author.id) == constants.myId
bot = ctx.bot
if ctx.bot.locked:
return
# If mention prefix was used, don't count it as a target
if ctx.message.content.startswith("<@"):
ctx.message.mentions = ctx.message.mentions[1:]
if len(ctx.message.mentions) > 5:
return await ctx.send("Je kan Help maar naar maximaal 5 mensen doorsturen.")
# Send help categories
if command is None:
return await self.send_bot_help(self.get_bot_mapping())
# Check if command is a category
if command.lower() == "mod" and not self.is_mod:
return await self.send_error_message("Je hebt geen toegang tot deze commando's.")
category = getCategory(command, self.is_mod)
if category:
return await self.send_category_help(category)
# Cut the mentions out & split based on subcommand
spl = command.split(" ")
spl = spl[:len(spl) - len(self.ctx.message.mentions)]
# A person was mentioned without passing an argument
if not spl:
return await self.send_bot_help(self.get_bot_mapping())
# Turn dic to lowercase to allow proper name searching
all_commands = {}
for k, v in bot.all_commands.items():
if k is None or v is None:
continue
if isinstance(v, SlashCommand):
continue
all_commands[k.lower()] = v
if spl[0].lower() not in all_commands:
return await self.send_error_message(await self.command_not_found(spl[0]))
cmd = all_commands[spl[0].lower()]
# Check if the entered command path exists
for key in spl[1:]:
try:
all_commands = dict((k.lower(), v) for k, v in cmd.all_commands.items())
if key.lower() not in all_commands:
raise AttributeError
found = all_commands[key.lower()]
except AttributeError:
return await self.send_error_message(await self.subcommand_not_found(cmd, key))
cmd = found
# Subcommands should have the parent command's category
temp = cmd
while temp.parent is not None:
temp = temp.parent
# Don't allow non-mods to see mod commands
try:
if temp.callback.category == Category.Mod and not self.is_mod:
return await self.send_error_message("Je hebt geen toegang tot dit commando.")
except AttributeError:
return await self.send_error_message("Dit is geen (openbaar) commando.")
if isinstance(cmd, commands.Group):
return await self.send_group_help(cmd)
else:
return await self.send_command_help(cmd)
def get_bot_mapping(self):
return categories(self.is_mod)
# Sends list of commands in a category
async def send_category_help(self, category):
# Get a list of all commands in this category
category_commands = [command.name if not command.callback.unpack else command
for command in self.ctx.bot.commands
if hasattr(command.callback, "category") and command.callback.category == category]
# Unpack any groups that have to be unpacked
for command in list(category_commands):
if not isinstance(command, str):
category_commands.remove(command)
category_commands.extend([self.get_name(c) for c in self.unpack_group(command)])
embed = self.create_help_embed(category.value)
embed.add_field(name="Commands", value="\n".join(sorted(category_commands)))
for person in await self.get_destination():
await person.send(embed=embed)
async def send_bot_help(self, mapping):
embed = self.create_help_embed("Help")
embed.add_field(name="Categorieën", value="\n".join(sorted(mapping)))
await self.ctx.send(embed=embed)
async def send_command_help(self, command):
with open("files/help.json", "r") as fp:
helpFile = json.load(fp)
try:
helpDescription = helpFile[self.get_name(command).lower()]
except KeyError:
helpDescription = "Indien je dit leest is DJ STIJN vergeten om dit commando in de help page te zetten. Stuur hem een DM om hem eraan te herinneren."
embed = self.create_help_embed("Help")
embed.add_field(name=await self.get_command_signature(command),
value=await self.add_aliases_formatting(sorted(command.aliases)) + helpDescription)
for person in await self.get_destination():
# Can't send to bots
if person.bot:
continue
await person.send(embed=embed)
async def send_group_help(self, group):
with open("files/help.json", "r") as fp:
helpFile = json.load(fp)
embed = self.create_help_embed(group.name + " Commando's")
try:
helpDescription = helpFile[self.get_name(group).lower()]
except KeyError:
helpDescription = "Indien je dit leest is DJ STIJN vergeten om dit commando in de help page te zetten. Stuur hem een DM om hem eraan te herinneren."
embed.add_field(name=await self.get_command_signature(group),
value=await self.add_aliases_formatting(sorted(group.aliases)) + helpDescription,
inline=False)
# Signature: Aliases - Usage
for subcommand in self.unpack_group(group):
embed.add_field(name=await self.get_command_signature(subcommand),
value=await self.add_aliases_formatting(sorted(subcommand.aliases)) +
helpFile[self.get_name(subcommand).lower()], inline=False)
for person in await self.get_destination():
# Can't send to bots
if person.bot:
continue
await person.send(embed=embed)
# Allow mentioning people to send it to them instead
async def get_destination(self):
if self.ctx.message.mentions:
return set(mention for mention in self.ctx.message.mentions if not mention.bot)
return [self.ctx.author]
async def command_not_found(self, string):
return "Er bestaat geen commando met de naam **{}**".format(string)
async def subcommand_not_found(self, command, string):
return "**{}** heeft geen subcommando met de naam **{}**.".format(command.name, string)
async def get_command_signature(self, command):
return "{} {}".format(self.get_name(command), command.usage if command.usage is not None else "")
async def add_aliases_formatting(self, aliases):
return "*Alias: {}*\n".format(", ".join(aliases)) if aliases else ""
async def send_error_message(self, error):
embed = discord.Embed(colour=discord.Colour.red())
embed.set_author(name="Help")
embed.add_field(name="Error", value=error)
await self.ctx.author.send(embed=embed)
def unpack_group(self, group):
# Create a list of all command objects in this group, in case they aren't hidden, sorted by name
subcommands = [group.all_commands.get(command) for command in group.all_commands]
subcommands.sort(key=lambda x: x.name)
subcommands = filter(lambda x: not x.hidden, subcommands)
return list(set(subcommands))
def get_name(self, command):
return command.qualified_name if command.parents else command.name
def create_help_embed(self, title):
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name=title)
embed.set_footer(text="Syntax: Didier Help [Categorie] of Didier Help [Commando]")
return embed
class Help(commands.Cog):
def __init__(self, client):
self.client = client
self._original_help_command = client.help_command
client.help_command = HelpCommand(command_attrs={"aliases": ["rtfm"]})
client.help_command.cog = self
def cog_unload(self):
self.client.help_command = self._original_help_command
def setup(client):
client.add_cog(Help(client))

View File

@ -1,56 +0,0 @@
from datetime import datetime
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import checks
import pytz
import requests
# Temporarily disabled because of API (setup @ bottom)
class Launch(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.command(name="Launch", aliases=["SpaceX"])
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Other)
async def launch(self, ctx, *args):
resp = self.getNextLaunch()
resp: dict = resp[list(resp.keys())[0]]
embed = discord.Embed(
colour=discord.Colour.blue()
)
embed.set_author(name="🚀 Volgende SpaceX lancering 🚀")
embed.add_field(name="Naam:", value=resp["name"], inline=False)
embed.add_field(name="Tijdstip:", value=resp["time"])
await ctx.send(embed=embed)
def getNextLaunch(self):
resp = requests.get("https://launchlibrary.net/1.3/launch?next=1&name=falcon").json()
if "status" in resp and (resp["status"] == "fail" or resp["status"] == "error"):
return {"error": "fail" if resp["status"].lower() == "fail" else "none found"}
resp = resp["launches"]
ret = {}
for launch in resp:
ret[launch["id"]] = {
"name": launch["name"],
"time": self.parseDate(launch["net"][:-4]) if launch["tbdtime"] == 0 else "TBA",
"TBA": launch["tbdtime"] == "0"
}
return ret
def parseDate(self, timestr):
d = datetime.strptime(timestr, "%B %d, %Y %H:%M:%S").timestamp()
return str(
datetime.fromtimestamp(int(d) + 7200, pytz.timezone("Europe/Brussels")).strftime('%B %d %Y om %H:%M:%S'))
def setup(client):
pass
# client.add_cog(Launch(client))

View File

@ -1,77 +0,0 @@
import discord
from discord.ext import commands
from data.menus import leaderboards
from decorators import help
from enums.help_categories import Category
from functions import checks
class Leaderboards(commands.Cog):
def __init__(self, client):
self.client = client
self.utilsCog = self.client.get_cog("Utils")
# Don't allow any commands to work when locked
def cog_check(self, _):
return not self.client.locked
@commands.group(name="Leaderboard", aliases=["Lb", "Leaderboards"], case_insensitive=True, usage="[Categorie]*",
invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Other)
async def leaderboard(self, ctx):
categories = ["Bitcoin", "Corona", "Dinks", "Messages", "Poke", "Rob", "XP"]
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Leaderboard Categorieën")
embed.description = "\n".join(categories)
await ctx.channel.send(embed=embed)
@leaderboard.command(name="Dinks", aliases=["Cash"], hidden=True)
async def dinks(self, ctx):
lb = leaderboards.DinksLeaderboard(ctx=ctx)
await lb.send()
@leaderboard.command(name="Corona", hidden=True)
async def corona(self, ctx):
lb = leaderboards.CoronaLeaderboard(ctx=ctx)
await lb.send()
@leaderboard.command(name="Bitcoin", aliases=["Bc"], hidden=True)
async def bitcoin(self, ctx):
lb = leaderboards.BitcoinLeaderboard(ctx=ctx)
await lb.send()
@leaderboard.command(name="Rob", hidden=True)
async def rob(self, ctx):
lb = leaderboards.RobLeaderboard(ctx=ctx)
await lb.send()
@leaderboard.command(name="Poke", hidden=True)
async def poke(self, ctx):
lb = leaderboards.PokeLeaderboard(ctx=ctx)
await lb.send()
@leaderboard.command(name="Xp", aliases=["Level"], hidden=True)
async def xp(self, ctx):
lb = leaderboards.XPLeaderboard(ctx=ctx)
await lb.send()
@leaderboard.command(name="Messages", aliases=["Mc", "Mess"], hidden=True)
async def messages(self, ctx):
lb = leaderboards.MessageLeaderboard(ctx=ctx)
await lb.send()
@leaderboard.command(name="Muttn", aliases=["M", "Mutn", "Mutten"], hidden=True)
async def muttn(self, ctx):
lb = leaderboards.MuttnLeaderboard(ctx=ctx)
await lb.send()
async def callLeaderboard(self, name, ctx):
command = [command for command in self.leaderboard.commands if command.name.lower() == name.lower()][0]
await command(ctx)
def setup(client):
client.add_cog(Leaderboards(client))

View File

@ -1,156 +0,0 @@
from decorators import help
from discord.ext import commands
from enums.help_categories import Category
from functions import checks
import itertools
import random
class Minesweeper(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.command(name="Minesweeper", aliases=["Ms"], usage="[Niveau]*")
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Games)
async def minesweeper(self, ctx, difficulty="m"):
if difficulty[0].lower() not in "emh":
await ctx.send("Geef een geldige moeilijkheidsgraad op.")
return
await ctx.send(self.createGame(difficulty[0].lower()))
def createGame(self, difficutly):
# [X, Y, BombCount]
dimensions = {
"e": [5, 5, 4],
"m": [10, 10, 20],
"h": [13, 8, 35]
}
numbers = {
0: "||:zero:||",
1: "||:one:||",
2: "||:two:||",
3: "||:three:||",
4: "||:four:||",
5: "||:five:||",
6: "||:six:||",
7: "||:seven:||",
8: "||:eight:||",
}
# Initialize an empty grid
grid = [[" " for _ in range(dimensions[difficutly][0])] for _ in range(dimensions[difficutly][1])]
# Generate every possible position on the grid
positions = set(itertools.product([x for x in range(len(grid))], repeat=2))
# Place the bombs in the grid randomly
for i in range(dimensions[difficutly][2]):
bombPosition = random.choice(list(positions))
positions.remove(bombPosition)
grid[bombPosition[0]][bombPosition[1]] = "||:boom:||"
# Add in the numbers representing the amount of bombs nearby
for i, row in enumerate(grid):
for j, cell in enumerate(row):
if cell == " ":
grid[i][j] = numbers[self.countBombs(grid, [i, j])]
# Reveal the biggest empty space to the player
self.revealSpaces(grid, self.findBiggestEmptySpace(grid))
# Join the grid into a string
li = [" ".join(row) for row in grid]
return "\n".join(li)
# Counts the amount of bombs near a given cell
def countBombs(self, grid, cell):
positions = [
[1, -1], [1, 0], [1, 1],
[0, -1], [0, 1],
[-1, -1], [-1, 0], [-1, 1]
]
count = 0
for position in positions:
if 0 <= cell[0] + position[0] < len(grid) and 0 <= cell[1] + position[1] < len(grid[0]):
if "boom" in grid[cell[0] + position[0]][cell[1] + position[1]]:
count += 1
return count
# Finds the biggest spot of 0's on the grid to reveal at the start
def findBiggestEmptySpace(self, grid):
spaces = []
biggest = []
# Floodfill
for i, row in enumerate(grid):
for j, cell in enumerate(row):
# Only check cells that aren't part of a space yet
if not any(cell in space for space in spaces) and "zero" in cell:
li = [[i, j]]
changed = True
while changed:
changed = False
for added in li:
neighb = self.neighbours(grid, added)
# Add all neighbours that have not yet been added to this space
for neighbour in neighb:
if neighbour not in li:
li.append(neighbour)
changed = True
spaces.append(li)
# If it's bigger than the current biggest, make it the new biggest
if len(li) > len(biggest):
biggest = li
return biggest
# Returns all neighbouring cells containing a 0
def neighbours(self, grid, cell):
positions = [
[1, 0],
[0, -1], [0, 1],
[-1, 0]
]
neighb = []
for position in positions:
if 0 <= cell[0] + position[0] < len(grid) and 0 <= cell[1] + position[1] < len(grid[0]):
if "zero" in grid[cell[0] + position[0]][cell[1] + position[1]]:
neighb.append([cell[0] + position[0], cell[1] + position[1]])
return neighb
# Take away the spoiler marks from the biggest empty space to help the player start
def revealSpaces(self, grid, emptySpaces):
positions = [
[1, -1], [1, 0], [1, 1],
[0, -1], [0, 1],
[-1, -1], [-1, 0], [-1, 1]
]
for space in emptySpaces:
grid[space[0]][space[1]] = ":zero:"
# Reveal all spaces around this one
for position in positions:
# Check if the space is not zero & is contained inside the grid & the space hasn't been cut before
if 0 <= space[0] + position[0] < len(grid) and \
0 <= space[1] + position[1] < len(grid[0]) and \
"zero" not in grid[space[0] + position[0]][space[1] + position[1]] and \
"||" in grid[space[0] + position[0]][space[1] + position[1]]:
# Cut the spoiler from this cell
grid[space[0] + position[0]][space[1] + position[1]] = grid[space[0] + position[0]][
space[1] + position[1]][2:-2]
def setup(client):
client.add_cog(Minesweeper(client))

View File

@ -1,214 +0,0 @@
from data import constants
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import checks, config, timeFormatters
from functions.database import memes, githubs, twitch, dadjoke
from functions.database.custom_commands import add_command, add_alias
import json
import os
class ModCommands(commands.Cog):
def __init__(self, client):
self.client = client
self.utilsCog = self.client.get_cog('Utils')
@commands.command(name="Remove", aliases=["Rm"], hidden=True)
@commands.check(checks.isMe)
@help.Category(category=Category.Mod)
async def remove(self, ctx, message: str):
spl = message.split("/")
channel = self.client.get_channel(int(spl[-2]))
message = await channel.fetch_message(int(spl[-1]))
await message.delete()
# Load a cog
@commands.group(name="Load", usage="[Cog]", case_insensitive=True, invoke_without_command=True)
@commands.check(checks.isMe)
@help.Category(category=Category.Mod)
async def load(self, ctx, extension: str):
try:
self.client.load_extension("cogs.{}".format(extension))
await self.sendDm(constants.myId, "Loaded **{}**".format(extension))
except discord.ExtensionAlreadyLoaded:
await self.sendDm(constants.myId, "**{}** has already been loaded".format(extension))
@commands.command(name="Config", aliases=["Setup", "Set"], case_insensitive=True, usage="[Categorie] [Value]",
invoke_without_commands=True)
@commands.check(checks.isMe)
@help.Category(Category.Mod)
async def set(self, ctx, category, value):
if config.config(category, value):
await ctx.message.add_reaction("")
# Load all cogs except for modCommands
@load.command(name="All")
async def loadAll(self, ctx):
for file in os.listdir("./cogs"):
if file.endswith(".py") and not file == "modCommands.py":
await self.load(ctx, file[:-3])
# Unload a cog
@commands.group(name="Unload", usage="[Cog]", case_insensitive=True, invoke_without_command=True)
@commands.check(checks.isMe)
@help.Category(category=Category.Mod)
async def unload(self, ctx, extension: str):
try:
self.client.unload_extension("cogs.{}".format(extension))
await self.sendDm(constants.myId, "Unloaded **{}**".format(extension))
except discord.ExtensionNotLoaded:
await self.sendDm(constants.myId, "**{}** has already been unloaded".format(extension))
# Unload all cogs except for modCommands
@unload.command(name="All")
async def unloadAll(self, ctx):
for file in os.listdir("./cogs"):
if file.endswith(".py") and not file == "modCommands.py":
await self.unload(ctx, file[:-3])
# Reloads a cog
@commands.command(name="Reload", aliases=["Update"], usage="[Cog]")
@commands.check(checks.isMe)
@help.Category(category=Category.Mod)
async def reload(self, ctx, cog):
await self.unload(ctx, cog)
await self.load(ctx, cog)
await ctx.message.add_reaction("")
# Repeat what was said
@commands.command(name="Repeat", usage="[Text]")
@commands.check(checks.isMe)
@help.Category(category=Category.Mod)
async def repeat(self, ctx, *text):
await self.utilsCog.removeMessage(ctx.message)
await ctx.send(" ".join(text))
# Add a reaction to a message
@commands.command(name="Reac", aliases=["Reacc"], usage="[Emoji] [Id]")
@commands.check(checks.isMe)
@help.Category(category=Category.Mod)
async def reac(self, ctx, emoji, messageId):
channel = ctx.channel
# Check if the URL or the Id was passed
if messageId.count("/") > 3:
spl = messageId.split("/")
channel = self.client.get_channel(int(spl[-2]))
if channel is None:
return await ctx.send("Ik kan geen kanaal zien met dit Id.")
messageId = int(spl[-1])
await self.utilsCog.removeMessage(ctx.message)
message = await channel.fetch_message(messageId)
if message is None:
return await ctx.send("Ik kan geen bericht zien met dit Id.")
await message.add_reaction(emoji)
@commands.group(name="Add", usage="[Category] [Args]", case_insensitive=True, invoke_without_command=False)
@commands.check(checks.isMe)
@help.Category(category=Category.Mod)
async def add(self, ctx):
"""
Commands group that adds database entries
"""
pass
@add.command(name="Custom", usage="[Name] [Response]")
async def custom(self, ctx, name, *, resp):
err_msg = add_command(name, resp)
# Something went wrong
if err_msg:
return await ctx.send(err_msg)
else:
await ctx.message.add_reaction("")
@add.command(name="Alias", usage="[Name] [Alias]")
async def add_alias(self, ctx, command, alias):
err_msg = add_alias(command, alias)
# Something went wrong
if err_msg:
return await ctx.send(err_msg)
else:
await ctx.message.add_reaction("")
@add.command(name="Dadjoke", aliases=["Dj", "Dad"], usage="[Joke]")
async def dadjoke(self, ctx, *, joke):
dadjoke.addJoke(joke)
await ctx.send("Added ``{}``.".format(joke))
await ctx.message.add_reaction("")
@add.command(name="8-Ball", aliases=["8b", "Eightball", "8Ball"], usage="[Response]")
async def eightball(self, ctx, message):
with open("files/eightball.json", "r") as fp:
file = json.load(fp)
file.append(message)
with open("files/eightball.json", "w") as fp:
json.dump(file, fp)
# Adds a meme into the database
@add.command(name="Meme", aliases=["Mem"], usage="[Id] [Name] [Aantal Velden]")
async def meme(self, ctx, memeid, meme, fields):
await ctx.send(memes.insert(memeid, meme, fields))
# Adds a person's GitHub into the database
@add.command(name="GitHub", aliases=["Gh", "Git"], usage="[Id] [Link]")
async def github(self, ctx, userid, link):
# Allow tagging to work as well
if len(ctx.message.mentions) == 1:
userid = ctx.message.mentions[0].id
githubs.add(userid, link)
await ctx.send("{}'s GitHub is toegevoegd aan de database.".format(self.utilsCog.getDisplayName(ctx, userid)))
# Adds a person's Twitch into the database
@add.command(name="Twitch", aliases=["Stream", "Streamer", "Tw"])
async def twitch(self, ctx, userid, link):
# Allow tagging to work as well
if len(ctx.message.mentions) == 1:
userid = ctx.message.mentions[0].id
twitch.add(userid, link)
await ctx.send("{}'s Twitch is toegevoegd aan de database.".format(self.utilsCog.getDisplayName(ctx, userid)))
@commands.command(name="WhoIs", aliases=["Info", "Whodis"], usage="[@User]")
@help.Category(Category.Mod)
async def whois(self, ctx, user: discord.User = None):
if user is None:
return
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name=user.display_name, icon_url=user.avatar.url)
embed.add_field(name="Discriminator", value=f"#{user.discriminator}")
embed.add_field(name="Discord id", value=user.id)
embed.add_field(name="Bot", value="Nee" if not user.bot else "Ja")
created_local = timeFormatters.epochToDate(user.created_at.timestamp())
embed.add_field(name="Account aangemaakt", value=f"<t:{round(created_local['dateDT'].timestamp())}:R>", inline=False)
# Check if the user is in the current guild
if ctx.guild is not None:
member_instance = ctx.guild.get_member(user.id)
if member_instance is not None:
joined_local = timeFormatters.epochToDate(member_instance.joined_at.timestamp())
embed.add_field(name=f"Lid geworden van {ctx.guild.name}",
value=f"<t:{round(joined_local['dateDT'].timestamp())}:R>")
embed.add_field(name="Mention String", value=member_instance.mention, inline=False)
await ctx.send(embed=embed)
# Send a DM to a user -- Can't re-use Utils cog in (un)load because the cog might not be loaded
async def sendDm(self, userid, message: str):
user = self.client.get_user(int(userid))
await user.send(message)
def setup(client):
client.add_cog(ModCommands(client))

View File

@ -1,37 +0,0 @@
import discord
from discord.ext import commands
from decorators import help
from enums.help_categories import Category
from functions.database import muttn
class Muttn(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.group(name="Muttn", aliases=["HowMuttn", "M", "Mutn", "Mutten"], usage="[@Persoon]", case_insensitive=True, invoke_without_command=True)
@help.Category(Category.Fun)
async def muttn(self, ctx, member: discord.Member = None):
if member is None:
member = ctx.author
user = muttn.getOrAddUser(member.id)
embed = discord.Embed(colour=discord.Colour.blue(), title=member.display_name)
embed.set_author(name="Muttn-O'-Meter")
embed.add_field(name="Percentage", value="{}%".format(round(float(user[1]), 2)))
embed.add_field(name="Aantal {}'s".format("<:Muttn:761551956346798111>"), value=str(user[2]))
await ctx.send(embed=embed)
@muttn.command(name="Leaderboard", aliases=["Lb"], hidden=True)
async def lb(self, ctx):
await self.client.get_cog("Leaderboards").callLeaderboard("muttn", ctx)
def setup(client):
client.add_cog(Muttn(client))

View File

@ -1,163 +0,0 @@
from data import constants
import datetime
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import checks, clap, mock, sunrise, timeFormatters
import pytz
from requests import get
import time
import urllib.parse
# Random things that are usually oneliners & don't belong in any other categories
class Oneliners(commands.Cog):
def __init__(self, client):
self.client = client
self.utilsCog = self.client.get_cog('Utils')
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.command(name="Age", usage="[Formaat]*")
@help.Category(category=Category.Didier)
async def age(self, ctx, specification=None):
allowedSpecifications = ["d", "days", "m", "months", "w", "weeks", "y", "years"]
if specification is not None and specification.lower() not in allowedSpecifications:
await ctx.send("**{}** is geen geldig formaat.".format(specification))
return
if specification is None:
timeString = timeFormatters.diffYearBasisString(constants.creationDate)
else:
ageSeconds = round(time.time()) - constants.creationDate
timeFormat = timeFormatters.getFormat(specification)
timeString = str(timeFormatters.timeIn(ageSeconds, timeFormat)[0])
timeString += " " + timeFormatters.getPlural(int(timeString), timeFormat)
await ctx.send("Didier is **{}** oud.".format(timeString))
@commands.command(name="Clap", usage="[Tekst]")
@help.Category(category=Category.Other)
async def clap(self, ctx, *args):
await ctx.send(clap.clap("".join(args)))
await self.utilsCog.removeMessage(ctx.message)
@commands.command(name="Reverse", aliases=["Rev"], usage="[Tekst]")
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Other)
async def reverse(self, ctx, *, args):
await ctx.send(args[::-1])
@commands.command(name="Government", aliases=["Gov", "Regering"])
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Other)
async def government(self, ctx):
now = timeFormatters.dateTimeNow()
newGov = datetime.datetime.fromtimestamp(1601539200, tz=pytz.timezone("Europe/Brussels"))
delta = now - newGov
zin = "Na **494** dagen is er weer een regering, **47** dagen te vroeg om het record te breken. Very sad times.\nMAAR hoelang denk je dat de nieuwe regering het gaat volhouden? Place your bets! Momenteel zitten we aan **{}** dag{}.".format(
delta.days, "en" if delta.days != 1 else ""
)
# now = int(time.time())
# valVorige = 1545350400
# verkiezingen = 1558828800
# valDiff = now - valVorige
# verkiezingenDiff = now - verkiezingen
# zin = (
# "We zitten al **%d** dagen zonder regering, en proberen al **%d** dagen een nieuwe te vormen.\nHet "
# "huidige wereldrecord is "
# "**541** dagen, dus nog **%d** dagen tot we het gebroken hebben." %
# (valDiff // 86400, verkiezingenDiff // 86400, 541 - int(verkiezingenDiff // 86400)))
await ctx.send(zin)
@commands.command()
async def inject(self, ctx):
await ctx.send("**{}** heeft wat code geïnjecteerd.".format(ctx.author.display_name))
@commands.command(name="Mock", usage="[Tekst]")
@help.Category(category=Category.Other)
async def mock(self, ctx, *text):
await ctx.channel.send("{} - **{}**".format(mock.mock(" ".join(text)), ctx.author.display_name))
await self.utilsCog.removeMessage(ctx.message)
@commands.command(name="Molest", usage="[@Persoon]")
async def molest(self, ctx):
if constants.didierId in ctx.message.content:
await ctx.send("Nee.")
elif str(ctx.author.id) in ctx.message.content or ctx.message.content == "molest me":
await ctx.send("I didn't know you swing that way, " + ctx.author.display_name)
elif "171671190631481345" in ctx.message.content:
await ctx.send("Nee")
else:
await ctx.send("https://imgur.com/a/bwA6Exn")
@commands.command(name="Changelog", aliases=["Cl", "Change", "Changes"])
@help.Category(category=Category.Didier)
async def changelog(self, ctx):
await ctx.send("v2.3.0: https://github.com/stijndcl/didier/releases/tag/2.3.0")
@commands.command(name="Todo", aliases=["List", "Td"])
@help.Category(category=Category.Didier)
async def todo(self, ctx):
await ctx.send("https://trello.com/b/PdtsAJea/didier-to-do-list")
@commands.command(name="LMGTFY", aliases=["Dsfr"], usage="[Query]")
@help.Category(category=Category.Other)
async def lmgtfy(self, ctx, *, query=None):
if query:
await ctx.send("https://lmgtfy.com/?q={}&iie=1".format(urllib.parse.quote(query)))
@commands.command(name="Neck", aliases=["Necc"], usage="[Lengte]*")
@help.Category(category=Category.Fun)
async def neck(self, ctx, size=None):
if not size:
size = 1
try:
size = int(size)
if not 0 < size < 16:
raise ValueError
except ValueError:
return await ctx.send("Geef een geldig getal op.")
await ctx.send("<:WhatDidYou:744476950654877756>" + ("<:DoTo:744476965951504414>" * size) + "<:MyDrink:744476979939508275>")
@commands.command()
async def open(self, ctx):
# await ctx.send(file=discord.File("files/images/open_source_bad.jpg"))
await ctx.send("Shut, it already is.")
@commands.command()
async def sc(self, ctx):
await ctx.send("http://take-a-screenshot.org/")
@commands.command(aliases=["os", "sauce", "src"])
async def source(self, ctx):
# await ctx.send("<https://bit.ly/31z3BuH>")
await ctx.send("https://github.com/stijndcl/didier")
@commands.command(aliases=["sunrise", "sunshine"])
async def sun(self, ctx):
s = sunrise.Sun()
await ctx.send(":sunny:: **{}**\n:crescent_moon:: **{}**".format(s.sunrise(), s.sunset()))
@commands.command(name="Tias", aliases=["TryIt"])
async def tias(self, ctx):
await ctx.send("***Try it and see***")
@commands.command(name="Inspire")
@help.Category(Category.Other)
async def inspire(self, ctx):
image = get("https://inspirobot.me/api?generate=true")
if image.status_code == 200:
await ctx.send(image.text)
else:
await ctx.send("Uh oh API down.")
def setup(client):
client.add_cog(Oneliners(client))

View File

@ -1,74 +0,0 @@
import discord
from discord.ext import commands
from data.embeds.snipe import EditSnipe, DeleteSnipe
from data.links import get_link_for
from data.menus import custom_commands
from data.snipe import Action, Snipe
from decorators import help
from enums.help_categories import Category
from functions.utils import reply_to_reference
from startup.didier import Didier
class Other(commands.Cog):
def __init__(self, client: Didier):
self.client: Didier = client
# Don't allow any commands to work when locked
def cog_check(self, _):
return not self.client.locked
@commands.command(name="Link", usage="[Naam]")
@help.Category(category=Category.Other)
async def link(self, ctx: commands.Context, name: str):
"""
Send commonly used links
"""
match = get_link_for(name)
if match is None:
return await ctx.reply(f"Geen match gevonden voor \"{name}\".", mention_author=False, delete_after=15)
await reply_to_reference(ctx, content=match)
@commands.command(name="Custom")
@help.Category(category=Category.Didier)
async def list_custom(self, ctx):
"""
Get a list of all custom commands
"""
await custom_commands.CommandsList(ctx).send()
@commands.command(name="Join", usage="[Thread]")
@help.Category(category=Category.Didier)
async def join_thread(self, ctx, thread: discord.Thread):
"""
Join threads
"""
if thread.me is None:
await thread.join()
await ctx.message.add_reaction("")
@commands.command(name="Snipe")
@help.Category(category=Category.Other)
async def snipe(self, ctx):
"""
Shame people for editing & removing messages.
The dict is stored in memory so it will be cleared whenever the bot restarts.
"""
if ctx.guild is None:
return
if ctx.channel.id not in self.client.snipe:
return await ctx.send("Er is hier niemand om uit te lachen.")
s: Snipe = self.client.snipe[ctx.channel.id]
embed_class = (EditSnipe(s) if s.action == Action.Edit else DeleteSnipe(s))
return await ctx.send(embed=embed_class.to_embed(self.client))
def setup(client):
client.add_cog(Other(client))

View File

@ -1,113 +0,0 @@
from data import constants
import datetime
from decorators import help
from discord.ext import commands
from enums.help_categories import Category
from functions import checks, timeFormatters
from functions.database import poke, stats
class Poke(commands.Cog):
def __init__(self, client):
self.client = client
self.utilsCog = self.client.get_cog("Utils")
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.group(name="Poke", usage="[@Persoon]", case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Games)
async def poke(self, ctx, member=None):
if not await self.pokeChecks(ctx):
return
member = ctx.message.mentions[0]
await ctx.send("**{}** heeft **{}** getikt. **{}** is hem!".format(
ctx.author.display_name, member.display_name, member.display_name))
# Add into the database
poke.update(ctx.author.id, member.id)
stats.update(member.id, "poked", int(stats.getOrAddUser(member.id)[1]) + 1)
@poke.command(name="Blacklist", aliases=["Bl"])
async def blacklist(self, ctx):
if poke.blacklisted(ctx.author.id):
await ctx.send("Je hebt jezelf al geblacklisted, {}.".format(ctx.author.display_name))
return
if str(poke.get()[0]) == str(ctx.author.id):
await ctx.send("Je kan jezelf niet blacklisten als je aan de beurt bent, {}.".format(
ctx.author.display_name))
return
poke.blacklist(ctx.author.id)
await ctx.send("**{}** heeft zichzelf geblacklisted en kan niet meer getikt worden.".format(
ctx.author.display_name))
@poke.command(aliases=["wl"], hidden=True)
async def whitelist(self, ctx, *user):
user = ctx.message.mentions[0].id
if not poke.blacklisted(user):
await ctx.send("Deze persoon is niet geblacklisted.")
return
poke.blacklist(user, False)
await ctx.send("**{}** heeft {} gewhitelisted.".format(
ctx.author.display_name, self.utilsCog.getDisplayName(ctx, user)))
@poke.command(name="Current")
async def current(self, ctx):
p = poke.get()
pokedTimeStamp = timeFormatters.epochToDate(int(p[1]))["dateDT"]
timeString = timeFormatters.diffDayBasisString(pokedTimeStamp)
await ctx.send("Het is al **{}** aan **{}**.".format(timeString, self.utilsCog.getDisplayName(ctx, p[0])))
@poke.command(hidden=True)
async def me(self, ctx):
await ctx.send("Liever niet.")
@poke.command(hidden=True)
@commands.check(checks.isMe)
async def reset(self, ctx):
new = poke.reset()
await ctx.send("Poke is gereset. <@!{}> is hem!".format(str(new)))
@poke.command(aliases=["Lb", "Leaderboards"], hidden=True)
async def leaderboard(self, ctx):
await self.client.get_cog("Leaderboards").callLeaderboard("poke", ctx)
async def pokeChecks(self, ctx):
if len(ctx.message.mentions) == 0:
await ctx.send("Dit is geen geldige persoon.")
return False
if len(ctx.message.mentions) > 1:
await ctx.send("Je kan maar 1 persoon tegelijk tikken.")
return False
if ctx.message.mentions[0].id == ctx.author.id:
await ctx.send("Je kan jezelf niet tikken, {}.".format(ctx.author.display_name))
return False
if ctx.message.mentions[0].id == self.client.user.id:
await ctx.send("Je kan me niet tikken, {}.".format(ctx.author.display_name))
return False
if str(ctx.message.mentions[0].id) in constants.botIDs:
await ctx.send("Je kan geen bots tikken, {}.".format(ctx.author.display_name))
return False
# Check database things
p = poke.get()
if str(p[0]) != str(ctx.author.id):
await ctx.send("Het is niet jouw beurt, {}.".format(ctx.author.display_name))
return False
if str(ctx.message.mentions[0].id) == str(p[2]):
await ctx.send("Je mag niet terugtikken, {}.".format(ctx.author.display_name))
return False
if poke.blacklisted(ctx.message.mentions[0].id):
await ctx.send("Deze persoon heeft zichzelf geblacklisted en kan niet meer getikt worden.")
return False
return True
def setup(client):
client.add_cog(Poke(client))

View File

@ -1,44 +0,0 @@
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import checks
import requests
import urllib.parse
class QR(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.command(name="QR", usage="[Tekst]")
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Other)
async def QR(self, ctx, *link):
if len(link) != 1:
await ctx.send(file=discord.File("files/images/ngguuqr.png"))
await self.client.get_cog("Utils").removeMessage(ctx.message)
else:
self.generate("".join(link))
await ctx.send(file=discord.File("files/images/qrcode.png"))
self.remove()
await self.client.get_cog("Utils").removeMessage(ctx.message)
def generate(self, link):
fileContent = requests.get(
"https://image-charts.com/chart?chs=999x999&cht=qr&chl={}&choe=UTF-8&chof=.png".format(
urllib.parse.quote(link))).content
with open("files/images/qrcode.png", "wb+") as fp:
fp.write(fileContent)
def remove(self):
import os
os.remove("files/images/qrcode.png")
def setup(client):
client.add_cog(QR(client))

View File

@ -1,128 +0,0 @@
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import colours
import random
import requests
import json
class Random(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
# Creates an alias
@commands.command(name="Choice", aliases=["Choose"], usage="[Argumenten]")
async def choose(self, ctx, *options):
await self.choice(ctx, *options)
@commands.command(name="Shuffle", usage="[Argumenten]")
async def _shuffle(self, ctx, *options):
await self.shuffle(ctx, *options)
@commands.group(name="Random", aliases=["R", "Rand", "RNG"], case_insensitive=True, invoke_without_command=True)
@help.Category(category=Category.Random, unpack=True)
async def random(self, ctx):
pass
@random.command(name="Choice", usage="[Argumenten]")
async def choice(self, ctx, *options):
if not options or not options[0]:
return await ctx.send("Geef een geldige reeks op.")
await ctx.send(random.choice(options))
@random.command(name="Number", aliases=["Int"], usage="[Van]* [Tot]*")
async def number(self, ctx, to=100, start=1):
# This allows number(to) to work, as well as number(start, to)
if start > to:
start, to = to, start
await ctx.send(random.randint(start, to))
@number.error
async def on_number_error(self, ctx, error):
if isinstance(error, discord.ext.commands.BadArgument):
await ctx.send("Dit is geen geldig getal.")
else:
raise error
@random.command(name="Name")
async def name(self, ctx):
try:
name = requests.get("https://randomuser.me/api/").json()
except json.decoder.JSONDecodeError:
await ctx.send("Er ging iets mis. Probeer het opnieuw.")
return
name = name["results"][0]["name"]
await ctx.send("{} {} {}".format(name["title"], name["first"], name["last"]))
@random.command(name="Identity", aliases=["Id"])
async def identity(self, ctx):
try:
identity = requests.get("https://randomuser.me/api/").json()
except json.decoder.JSONDecodeError:
return await ctx.send("Er ging iets mis. Probeer het opnieuw.")
identity = identity["results"][0]
name = identity["name"]
name = "{} {} {}".format(name["title"], name["first"], name["last"])
gender = identity["gender"]
street = "{} {}".format(identity["location"]["street"]["number"], identity["location"]["street"]["name"])
location = "{}, {}, {}, {}".format(street, identity["location"]["city"],
identity["location"]["state"], identity["location"]["country"])
age = identity["dob"]["age"]
await ctx.send("{}\n{}, {}\n{}".format(name, age, gender, location))
@random.command(name="Shuffle", aliases=["Order"], usage="[Argumenten]")
async def shuffle(self, ctx, *args):
if not args:
return await ctx.send("Geef een geldige reeks op.")
args = list(args)
random.shuffle(args)
await ctx.send(" - ".join(args))
@random.command(name="Colour", aliases=["Color"])
async def colour(self, ctx):
r, g, b = colours.randomRGB()
embed = discord.Embed(colour=discord.Colour.from_rgb(r, g, b))
embed.set_author(name="Random Colour")
embed.add_field(name="RGB", value="{}, {}, {}".format(r, g, b), inline=False)
embed.add_field(name="HEX", value=colours.RGBToHEX(r, g, b), inline=False)
embed.add_field(name="HSL", value="{}°, {}%, {}%".format(*colours.RGBToHSL(r, g, b)), inline=False)
embed.add_field(name="HSV", value="{}°, {}%, {}%".format(*colours.RGBToHSV(r, g, b)), inline=False)
await ctx.send(embed=embed)
@random.command(name="Timestamp", aliases=["Time", "Ts"])
async def timestamp(self, ctx):
hour = str(random.randint(0, 23))
hour = ("0" if len(hour) == 1 else "") + hour
minutes = str(random.randint(0, 23))
minutes = ("0" if len(minutes) == 1 else "") + minutes
await ctx.send("{}:{}".format(hour, minutes))
@random.command(name="Fact", aliases=["Knowledge"])
async def fact(self, ctx):
randomFact = requests.get("https://uselessfacts.jsph.pl/random.json?language=en").json()
await ctx.send(randomFact["text"])
@commands.command(name="Yes/No", aliases=["Yn"])
@help.Category(Category.Random)
async def yesno(self, ctx):
await ctx.send(random.choice(["Ja.", "Nee."]))
def setup(client):
client.add_cog(Random(client))

View File

@ -1,66 +0,0 @@
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import reactWord
class ReactWord(commands.Cog):
def __init__(self, client):
self.client = client
self.utilsCog = self.client.get_cog("Utils")
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.command(name="React", usage="[Tekst] [Message id/url]*")
@help.Category(category=Category.Other)
async def react(self, ctx, *words):
words = list(words)
message = ctx.message
target = False
# Message id or URL passed as final argument
if (len(words[-1]) == 18 and all(i.isdigit() for i in words[-1])) or "discord.com/channels/" in words[-1]:
target = True
message = await commands.MessageConverter().convert(ctx, words[-1])
# Cut id or URL
words = words[:-1]
# Reactions that were added before this command was executed
previousReactions = ([x.emoji for x in message.reactions]) if len(message.reactions) != 0 else []
eligible, arr = reactWord.check(list(words), previousReactions)
if not eligible:
await ctx.send(arr[0])
else:
if target:
await self.utilsCog.removeMessage(ctx.message)
for reac in arr:
await message.add_reaction(reac)
@commands.command(name="Character", aliases=["Char"], usage="[Karakter]")
@help.Category(category=Category.Other)
async def char(self, ctx, char: str = None):
# Nothing passed
if char is None:
return await ctx.send("Controleer je argumenten")
char = char.lower()
# Not 1 char passed
if len(char) != 1 or char not in reactWord.allowedCharacters():
return await ctx.send("Dit is geen geldig karakter.")
var = reactWord.getAllVariants(char)
return await ctx.send("**Karakter**: {}\nOpties (**{}**): {}".format(
char, len(var), " ".join(var)
))
def setup(client):
client.add_cog(ReactWord(client))

View File

@ -1,138 +0,0 @@
import datetime
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import checks
import requests
import time
class Release(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
# Gets upcoming game releases
@commands.group(name="Releases", usage="[Pagina]*", case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Games)
async def releases(self, ctx, page="1"):
for char in page:
if not char.isdigit():
await ctx.send("Geef een geldige pagina op.")
return
dates = self.getDates()
resp = requests.get("https://api.rawg.io/api/games?dates={},{}&page_size=25&page={}&ordering=released".format(
dates[0], dates[1], page
)).json()
try:
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Volgende Game Releases | Pagina {}".format(page))
embed.description = "\n".join(
["{} (#{}): {}".format(result["name"], result["id"], self.rewriteDate(result["released"]))
for result in resp["results"]])
embed.set_footer(text="Voor gedetailleerde info: Didier Game Info [id]")
except KeyError:
embed = discord.Embed(colour=discord.Colour.red())
embed.set_author(name="Game Releases")
embed.add_field(name="Error", value="Er ging iets fout.")
await ctx.send(embed=embed)
def getDates(self):
today = datetime.datetime.fromtimestamp(time.time())
nextMonth = datetime.datetime.fromtimestamp(time.time() + 2629743)
return ["-".join([str(today.year), self.leadingZero(str(today.month)), self.leadingZero(str(today.day))]),
"-".join([str(nextMonth.year),
self.leadingZero(str(nextMonth.month)), self.leadingZero(str(nextMonth.day))])]
def leadingZero(self, num):
return num if len(num) == 2 else "0" + num
# Shows more detailed information for a game
@releases.command(name="Info", aliases=["Details"], usage="[Game Id]")
async def info(self, ctx, *, game_id):
game_id = self.create_slug(game_id)
resp = requests.get("https://api.rawg.io/api/games/{}".format(str(game_id))).json()
if "redirect" in resp:
resp = requests.get("https://api.rawg.io/api/games/{}".format(resp["slug"])).json()
if "Not found." in resp.values():
embed = discord.Embed(colour=discord.Colour.red())
embed.set_author(name="Game Info")
embed.description = "Er is geen game gevonden met deze id of naam."
else:
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Game Info")
embed.add_field(name="Naam", value=resp["name"])
embed.add_field(name="Id", value=resp["id"])
embed.add_field(name="Datum", value="TBA" if resp["tba"] else self.rewriteDate(resp["released"]))
embed.add_field(name="Platforms", value=", ".join(self.getPlatforms(resp)))
embed.add_field(name="Stores", value=", ".join(self.getStores(resp)))
embed.add_field(name="Genres", value=", ".join(self.getGenres(resp)))
embed.add_field(name="Tags", value=self.getTags(resp), inline=False)
embed.add_field(name="Description", value=self.writeDescription(resp["description_raw"]), inline=False)
await ctx.send(embed=embed)
# Turns name into a slug
def create_slug(self, game_id):
try:
# Check if it's a number
game_id = int(game_id)
return str(game_id)
except ValueError:
game_id = game_id.lower().replace(" ", "-").replace(":", "").replace("'", "")
return game_id
def rewriteDate(self, date):
date = date.split("-")
return "-".join(reversed(date))
def getGenres(self, release):
return sorted([genre["name"] for genre in release["genres"]])
# Returns a list of all platforms this game is available on
def getPlatforms(self, release):
return sorted([platform["platform"]["name"] for platform in release["platforms"]])
# Returns a list of all stores this game is available on
def getStores(self, release):
return sorted(store["store"]["name"] for store in release["stores"])
# Returns a list of all tags associated with this game
def getTags(self, release):
if len(release["tags"]) == 0:
return "N/A"
li = sorted([tag["name"] for tag in release["tags"]])
st = li[0]
for tag in li[1:]:
if len(st) + 2 + len(tag) > 1024:
break
st += ", " + tag
return st if st else "N/A"
# Truncates the description if necessary
def writeDescription(self, description):
if len(description) > 700:
return description[:697] + "..."
return description if description else "N/A"
@info.error
async def info_on_error(self, ctx, error):
if isinstance(error, commands.BadArgument):
await ctx.send("Geef een geldig getal op.")
elif isinstance(error, commands.MissingRequiredArgument):
await ctx.send("Controleer je argumenten.")
else:
raise error
def setup(client):
client.add_cog(Release(client))

View File

@ -1,56 +0,0 @@
import discord
from discord.ext import commands
from decorators import help
from enums.help_categories import Category
from functions.database import remind
class Remind(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.group(name="Remind", aliases=["Remindme"], usage="[Categorie]", case_insensitive=True, invoke_without_command=True)
@help.Category(Category.Other)
async def remind(self, ctx):
"""
Command group to remind the user of a certain thing every day.
:param ctx: Discord Context
"""
rows = remind.getOrAddUser(ctx.author.id)
# TODO use a loop for this when not lazy
categories = [remind.getIcon(rows[1]) + " Nightly", remind.getIcon(rows[2]) + " Les"]
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Remind Categorieën")
embed.description = "\n".join(sorted(categories))
await ctx.send(embed=embed)
@remind.command(name="Nightly")
async def nightly(self, ctx):
"""
Command to get a daily Nightly reminder
"""
if remind.switchReminder(ctx.author.id, "nightly"):
await ctx.send("Vanaf nu word je er dagelijks aan herinnerd om Didier Nightly te doen.")
else:
await ctx.send("Je zal er niet langer aan herinnerd worden om Didier Nightly te doen.")
@remind.command(name="Les", aliases=["Class", "Classes", "Sched", "Schedule"])
async def les(self, ctx):
"""
Command to get a daily reminder with an embed of your schedule
"""
if remind.switchReminder(ctx.author.id, "les"):
await ctx.send("Vanaf nu krijg je dagelijks je lessenrooster toegestuurd.")
else:
await ctx.send("Je zal je lessenrooster niet langer toegestuurd krijgen.")
def setup(client):
client.add_cog(Remind(client))

View File

@ -1,116 +0,0 @@
from data import schedule
from data.courses import find_course_from_name
from data.embeds.deadlines import Deadlines
from data.embeds.food import Menu
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import config, les
from functions.stringFormatters import capitalize
from functions.timeFormatters import skip_weekends
from functions.utils import reply_to_reference
class School(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.command(name="Eten", aliases=["Food", "Menu"], usage="[Dag]*")
# @commands.check(checks.allowedChannels)
@help.Category(category=Category.School)
async def eten(self, ctx, day: str = None):
if day is not None:
day = day.lower()
embed = Menu(day).to_embed()
await ctx.reply(embed=embed, mention_author=False)
@commands.command(name="Les", aliases=["Class", "Classes", "Sched", "Schedule"], usage="[Dag]*")
# @commands.check(checks.allowedChannels)
@help.Category(category=Category.School)
async def les(self, ctx, day=None):
if day is not None:
day = day.lower()
date = les.find_target_date(day)
# Person explicitly requested a weekend-day
if day is not None and day.lower() in ("morgen", "overmorgen") and date.weekday() > 4:
return await ctx.send(f"{capitalize(day)} is het weekend.")
date = skip_weekends(date)
s = schedule.Schedule(date, int(config.get("year")), int(config.get("semester")), day is not None)
if s.semester_over:
return await ctx.send("Het semester is afgelopen.")
# DM only shows user's own minor
if ctx.guild is None:
minor_roles = [*schedule.find_minor(self.client, ctx.author.id)]
return await ctx.send(embed=s.create_schedule(minor_roles=minor_roles).to_embed())
return await ctx.send(embed=s.create_schedule().to_embed())
@commands.command(name="Pin", usage="[Message]*")
@help.Category(category=Category.Other)
async def pin(self, ctx, message: discord.Message = None):
# In case people abuse, check if they're blacklisted
blacklist = []
if ctx.author.id in blacklist:
return
# Support replying to the message that should be pinned
if message is None:
reference = ctx.message.reference
if reference is None:
return await ctx.reply("Controleer je argumenten.")
# If the message is cached, avoid sending an API call
if not reference.cached_message:
# Message is always in the current channel because we came from a reply
message = await ctx.channel.fetch_message(reference.message_id)
else:
message = reference.cached_message
if message.is_system():
return await ctx.send("Dus jij wil system messages pinnen?\nMag niet.")
await message.pin(reason=f"Didier Pin door {ctx.author.display_name}")
await ctx.message.add_reaction("")
@commands.command(name="Fiche", usage="[Vak]", aliases=["guide", "studiefiche"])
@help.Category(category=Category.School)
async def study_guide(self, ctx, name: str):
"""
Send links to study guides
"""
# Find code corresponding to the search query
course = find_course_from_name(name)
# Code not found
if course is None:
return await ctx.reply(f"Onbekend vak: \"{name}\".", mention_author=False, delete_after=15)
# Get the guide for the current year
year = 2018 + int(config.get("year"))
link = f"https://studiekiezer.ugent.be/studiefiche/nl/{course.code}/{year}"
return await reply_to_reference(ctx, content=link)
@commands.command(name="Deadlines", aliases=["dl"])
@help.Category(category=Category.School)
async def deadlines(self, ctx):
await ctx.send(embed=Deadlines().to_embed())
def setup(client):
client.add_cog(School(client))

View File

@ -1,102 +0,0 @@
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import checks
from functions.database import githubs, twitch
class SelfPromo(commands.Cog):
def __init__(self, client):
self.client = client
self.utilsCog = self.client.get_cog("Utils")
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.group(name="GitHub", aliases=["Git", "GitHubs", "Gh"], case_insensitive=True, usage="[@Persoon]*", invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Other)
async def github(self, ctx, member: discord.Member = None):
# Get a specific member's GitHub
if member:
user_git = githubs.get_user(member.id)
if not user_git:
return await ctx.send("**{}** heeft zijn GitHub link nog niet doorgegeven.".format(member.display_name))
return await self.createPersonalPromo(ctx, member, user_git[0][0], discord.Colour.from_rgb(250, 250, 250), "GitHub")
l = githubs.getAll()
await self.createPromoEmbed(ctx, l, discord.Colour.from_rgb(250, 250, 250), "GitHub", "files/images/github.png")
@github.command(name="Add", aliases=["Insert", "Register", "Set"], usage="[Link]")
async def githubadd(self, ctx, link):
if "github.com" not in link.lower() and "github.ugent.be" not in link.lower() and "gitlab.com" not in link.lower():
link = "https://github.com/{}".format(link)
githubs.add(ctx.author.id, link)
await ctx.message.add_reaction("")
@commands.group(name="Twitch", aliases=["Streams"], case_insensitive=True, usage="[@Persoon]", invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Other)
async def twitch(self, ctx, member: discord.Member = None):
# Get a specific member's GitHub
if member:
user_twitch = twitch.get_user(member.id)
if not user_twitch:
return await ctx.send("**{}** heeft zijn Twitch link nog niet doorgegeven.".format(member.display_name))
return await self.createPersonalPromo(ctx, member, user_twitch[0][0], discord.Colour.from_rgb(100, 65, 165), "Twitch")
l = twitch.getAll()
await self.createPromoEmbed(ctx, l, discord.Colour.from_rgb(100, 65, 165), "Twitch", "files/images/twitch.png")
@twitch.command(name="Add", aliases=["Insert", "Register", "Set"], usage="[Link]")
async def twitchadd(self, ctx, link):
if "twitch.tv" not in link.lower():
link = "https://www.twitch.tv/{}".format(link)
twitch.add(ctx.author.id, link)
await ctx.message.add_reaction("")
# Creates embed with everyone's links & a fancy image
async def createPromoEmbed(self, ctx, users, colour, type, imageUrl=None):
# Image file
file = None
# Sort users by Discord name
users = [[self.utilsCog.getMember(ctx, user[0]), user[1]] for user in users if self.utilsCog.getMember(ctx, user[0]) is not None]
users.sort(key=lambda x: x[0].name)
embed = discord.Embed(colour=colour)
if imageUrl is not None:
# Link
if "https" in imageUrl:
embed.set_thumbnail(url=imageUrl)
else:
# Local file
file = discord.File(imageUrl, filename="icon.png")
embed.set_thumbnail(url="attachment://icon.png")
embed.set_author(name="{} Links".format(type))
for user in users:
embed.add_field(name="{} ({})".format(
user[0].display_name, user[0].name
), value=user[1], inline=False)
embed.set_footer(text="Wil je je eigen {0} hierin? Gebruik {0} Add [Link] of stuur een DM naar DJ STIJN.".format(type))
if file is not None:
await ctx.send(embed=embed, file=file)
else:
await ctx.send(embed=embed)
async def createPersonalPromo(self, ctx, user, link, colour, type):
embed = discord.Embed(colour=colour)
embed.set_author(name="{} Links".format(type), icon_url=user.avatar.url)
embed.add_field(name="{} link van {}".format(type, user.display_name), value=link)
await ctx.send(embed=embed)
def setup(client):
client.add_cog(SelfPromo(client))

View File

@ -1,19 +0,0 @@
from discord.ext import commands
from discord.commands import slash_command, ApplicationContext, Option
from data.embeds.urban_dictionary import Definition
from startup.didier import Didier
class DefineSlash(commands.Cog):
def __init__(self, client: Didier):
self.client: Didier = client
@slash_command(name="define", description="Urban Dictionary")
async def _define_slash(self, ctx: ApplicationContext, query: Option(str, "Search query", required=True)):
embed = Definition(query).to_embed()
await ctx.respond(embed=embed)
def setup(client: Didier):
client.add_cog(DefineSlash(client))

View File

@ -1,38 +0,0 @@
from discord.ext import commands
from discord.commands import Option, SlashCommandGroup, ApplicationContext, permissions
from functions import config
from functions.football import get_matches, get_table, get_jpl_code
from startup.didier import Didier
class FootballSlash(commands.Cog):
def __init__(self, client: Didier):
self.client: Didier = client
_jpl_group = SlashCommandGroup("jpl", "Jupiler Pro League commands")
@_jpl_group.command(name="matches", description="Schema voor een bepaalde speeldag")
async def _jpl_matches_slash(self, ctx: ApplicationContext,
day: Option(int, name="day", description="Speeldag (default huidige)", required=False, default=None)
):
# Default is current day
if day is None:
day = int(config.get("jpl_day"))
await ctx.respond(get_matches(day))
@_jpl_group.command(name="table", description="Huidige rangschikking")
async def _jpl_table_slash(self, ctx: ApplicationContext):
await ctx.response.defer()
await ctx.send_followup(get_table())
@_jpl_group.command(name="update", description="Update de code voor deze competitie (owner-only)", default_permission=False)
@permissions.is_owner()
async def _jpl_update_slash(self, ctx: ApplicationContext):
code = get_jpl_code()
config.config("jpl", code)
await ctx.respond(f"Done (code: {code})")
def setup(client: Didier):
client.add_cog(FootballSlash(client))

View File

@ -1,70 +0,0 @@
from discord.ext import commands
from discord.commands import slash_command, ApplicationContext, Option, AutocompleteContext
from functions.database import memes
from functions.database.memes import getAllMemes
from data.embeds.xkcd import XKCDEmbed
from data.menus.memes import MemesList
from functions.memes import generate
from functions.stringFormatters import title_case
from startup.didier import Didier
all_memes = getAllMemes()
def autocomplete_memes(ctx: AutocompleteContext) -> list[str]:
starting = []
containing = []
val = ctx.value.lower()
# First show matches that start with this word, then matches that contain it
for meme in all_memes:
if meme[1].startswith(val):
starting.append(title_case(meme[1]))
elif val in meme[1]:
containing.append(title_case(meme[1]))
return [*starting, *containing]
class FunSlash(commands.Cog):
def __init__(self, client: Didier):
self.client: Didier = client
@slash_command(name="xkcd", description="Zoek xkcd comics")
async def _xkcd_slash(self, ctx: ApplicationContext,
num: Option(int, description="Nummer van de comic (default de comic van vandaag).", required=False, default=None)
):
return await ctx.respond(embed=XKCDEmbed(num).create())
@slash_command(name="memes", description="Lijst van memegen-memes")
async def _memes_slash(self, ctx: ApplicationContext):
return await MemesList(ctx=ctx).respond()
@slash_command(name="memegen", description="Genereer memes")
async def _memegen_slash(self, ctx: ApplicationContext,
meme: Option(str, description="Naam van de template", required=True, autocomplete=autocomplete_memes),
field1: Option(str, required=True),
field2: Option(str, required=False, default=""),
field3: Option(str, required=False, default=""),
field4: Option(str, required=False, default="")):
# Get the meme info that corresponds to this name
result: memes.Meme = memes.getMeme(meme)
# No meme found
if result is None:
return await ctx.respond("Deze meme staat niet in de database.", ephemeral=True)
await ctx.response.defer()
fields = (field1, field2, field3, field4)
generated = generate(result, fields)
# Send generated meme or error message
await ctx.send_followup(generated["message"])
def setup(client: Didier):
client.add_cog(FunSlash(client))

View File

@ -1,23 +0,0 @@
from discord.ext import commands
from discord.commands import slash_command, ApplicationContext, Option
from functions.scrapers.google import google_search, create_google_embed
from startup.didier import Didier
class GoogleSlash(commands.Cog):
def __init__(self, client: Didier):
self.client: Didier = client
@slash_command(name="google", description="Google search")
async def _google_slash(self, ctx: ApplicationContext, query: Option(str, "Search query")):
result = google_search(query)
if not result.results:
return await ctx.respond("Er ging iets fout (Response {})".format(result.status_code))
embed = create_google_embed(result)
await ctx.respond(embed=embed)
def setup(client: Didier):
client.add_cog(GoogleSlash(client))

View File

@ -1,41 +0,0 @@
from discord.ext import commands
from discord.commands import slash_command, ApplicationContext, AutocompleteContext, Option
from requests import get
from data.links import load_all_links, get_link_for
from startup.didier import Didier
links = load_all_links()
def link_autocomplete(ctx: AutocompleteContext) -> list[str]:
return [link for link in links if link.lower().startswith(ctx.value.lower())]
class OtherSlash(commands.Cog):
def __init__(self, client: Didier):
self.client: Didier = client
@slash_command(name="inspire", description="Genereer quotes via Inspirobot.")
async def _inspire_slash(self, ctx: ApplicationContext):
image = get("https://inspirobot.me/api?generate=true")
if image.status_code == 200:
await ctx.respond(image.text)
else:
await ctx.respond("Uh oh API down.")
@slash_command(name="link", description="Shortcut voor nuttige links.")
async def _link_slash(self, ctx: ApplicationContext,
name: Option(str, description="Naam van de link.", required=True,
autocomplete=link_autocomplete)):
match = get_link_for(name)
if match is None:
return await ctx.respond(f"Geen match gevonden voor \"{name}\".")
return await ctx.respond(match)
def setup(client: Didier):
client.add_cog(OtherSlash(client))

View File

@ -1,121 +0,0 @@
import json
from discord import SlashCommandGroup
from discord.ext import commands
from discord.commands import slash_command, ApplicationContext, Option, AutocompleteContext
from data import schedule
from data.courses import load_courses, find_course_from_name
from data.embeds.food import Menu
from data.embeds.deadlines import Deadlines
from data.menus import leaderboards
from functions import les, config
from functions.stringFormatters import capitalize
from functions.timeFormatters import skip_weekends
from startup.didier import Didier
# Preload autocomplete constants to allow for smoother results
courses = load_courses()
days = ["Morgen", "Overmorgen", "Maandag", "Dinsdag", "Woensdag", "Donderdag", "Vrijdag"]
def day_autocomplete(ctx: AutocompleteContext) -> list[str]:
return [day for day in days if day.lower().startswith(ctx.value.lower())]
def course_autocomplete(ctx: AutocompleteContext) -> list[str]:
return [course for course in courses if course.lower().startswith(ctx.value.lower())]
class SchoolSlash(commands.Cog):
def __init__(self, client: Didier):
self.client: Didier = client
@slash_command(name="eten", description="Menu in de UGent resto's op een bepaalde dag")
async def _food_slash(self, ctx: ApplicationContext,
dag: Option(str, description="Dag", required=False, default=None, autocomplete=day_autocomplete)
):
embed = Menu(dag).to_embed()
await ctx.respond(embed=embed)
@slash_command(name="deadlines", description="Aanstaande deadlines")
async def _deadlines_slash(self, ctx: ApplicationContext):
embed = Deadlines().to_embed()
await ctx.respond(embed=embed)
@slash_command(name="les", description="Lessenrooster voor [Dag] (default vandaag)",)
async def _schedule_slash(self, ctx: ApplicationContext,
dag: Option(str, description="Dag", required=False, default=None, autocomplete=day_autocomplete)
):
"""It's late and I really don't want to refactor the original right now"""
if dag is not None:
dag = dag.lower()
date = les.find_target_date(dag)
# Person explicitly requested a weekend-day
if dag is not None and dag.lower() in ("morgen", "overmorgen") and date.weekday() > 4:
return await ctx.respond(f"{capitalize(dag)} is het weekend.", ephemeral=True)
date = skip_weekends(date)
s = schedule.Schedule(date, int(config.get("year")), int(config.get("semester")), dag is not None)
if s.semester_over:
return await ctx.respond("Het semester is afgelopen.", ephemeral=True)
# DM only shows user's own minor
if ctx.guild is None:
minor_roles = [*schedule.find_minor(self.client, ctx.interaction.user.id)]
return await ctx.respond(embed=s.create_schedule(minor_roles=minor_roles).to_embed())
return await ctx.respond(embed=s.create_schedule().to_embed())
@slash_command(name="fiche", description="Zoek de studiefiche voor een vak.")
async def _study_guide_slash(self, ctx: ApplicationContext,
vak: Option(str, description="Naam van het vak. Afkortingen werken ook, maar worden niet geautocompletet.",
required=True, autocomplete=course_autocomplete)):
# Find code corresponding to the search query
course = find_course_from_name(vak, courses)
# Code not found
if course is None:
return await ctx.respond(f"Onbekend vak: \"{vak}\".", ephemeral=True)
# Get the guide for the current year
year = 2018 + int(config.get("year"))
return await ctx.respond(f"https://studiekiezer.ugent.be/studiefiche/nl/{course.code}/{year}")
_compbio_group = SlashCommandGroup("compbio", "Commands voor compbio opdrachten")
@_compbio_group.command(name="leaderboard", description="Gesorteerd en ingevuld leaderboard")
async def _compbio_lb_slash(self, ctx: ApplicationContext,
benchmark: Option(str, "De specifieke benchmark om op te halen (default 10000-10)", choices=["100-10", "100-100", "1000-100", "10000-10"], default="10000-10")):
await ctx.response.defer()
size, amount = benchmark.split("-")
lb = leaderboards.CompbioLeaderboard(ctx, size=size, amount=amount)
await lb.respond()
@_compbio_group.command(name="submit", description="Link een Dodona-submission aan jouw username")
async def _compbio_submit_slash(self, ctx: ApplicationContext,
submission: Option(int, description="Id van je Dodona indiening.", required=True)):
await ctx.response.defer(ephemeral=True)
with open("files/compbio_benchmarks_4.json", "r") as fp:
file = json.load(fp)
submission = str(submission)
if submission in file:
return await ctx.send_followup("❌ Deze submission is al aan iemand gelinkt.")
with open("files/compbio_benchmarks_4.json", "w") as fp:
file[submission] = ctx.user.id
json.dump(file, fp)
return await ctx.send_followup(f"✅ Submission **{submission}** is aan jouw naam gelinkt.")
def setup(client: Didier):
client.add_cog(SchoolSlash(client))

View File

@ -1,23 +0,0 @@
from discord.ext import commands
from discord.commands import slash_command, ApplicationContext, Option
from data.embeds.translate import Translation
from startup.didier import Didier
class TranslateSlash(commands.Cog):
def __init__(self, client: Didier):
self.client: Didier = client
@slash_command(name="translate", description="Google Translate")
async def _translate_slash(self, ctx: ApplicationContext,
text: Option(str, description="Tekst om te vertalen"),
from_lang: Option(str, description="Taal om van te vertalen (default auto-detect)", default="auto"),
to_lang: Option(str, description="Taal om naar te vertalen (default NL)", default="nl")
):
translation = Translation(text=text, fr=from_lang.lower(), to=to_lang.lower())
await ctx.respond(embed=translation.to_embed())
def setup(client: Didier):
client.add_cog(TranslateSlash(client))

View File

@ -1,131 +0,0 @@
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import checks
from functions.database import stats
import json
class Stats(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.group(name="Stats", usage="[Categorie]*", case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(category=Category.Other)
async def stats(self, ctx):
s = stats.getOrAddUser(ctx.author.id)
# Calculate the percentages
robAttempts = int(s[2]) + int(s[3]) if int(s[2]) + int(s[3]) != 0 else 1
robSuccessPercent = round(100 * int(s[2]) / robAttempts, 2)
robFailedPercent = round(100 * int(s[3]) / robAttempts, 2)
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="{}'s Stats".format(ctx.author.display_name))
embed.add_field(name="Geslaagde Rob Pogingen", value="{} ({})%".format(s[2], robSuccessPercent))
embed.add_field(name="Gefaalde Rob Pogingen", value="{} ({})%".format(s[3], robFailedPercent))
embed.add_field(name="Aantal Dinks Gestolen", value="{:,}".format(round(s[4])))
embed.add_field(name="Aantal Nightlies", value=str(s[6]))
embed.add_field(name="Langste Nightly Streak", value=str(s[5]))
embed.add_field(name="Totale Profit", value="{:,}".format(round(s[7])))
embed.add_field(name="Aantal keer gepoked", value=str(s[1]))
embed.add_field(name="Aantal Gewonnen Coinflips", value=str(s[8]))
embed.add_field(name="Totale winst uit Coinflips", value="{:,}".format(round(s[9])))
embed.add_field(name="Aantal Bails", value="{:,}".format(int(s[10])))
await ctx.send(embed=embed)
@stats.command(aliases=["Coinflip"], hidden=True)
async def cf(self, ctx):
with open("files/stats.json", "r") as fp:
s = json.load(fp)
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Coinflip Stats")
embed.description = "**Kop**: {:,} ({}%)\n**Munt**: {:,} ({}%)".format(
s["cf"]["h"], self.percent(s["cf"], "h"), s["cf"]["t"], self.percent(s["cf"], "t"))
await ctx.send(embed=embed)
@stats.command(aliases=["Roll"], hidden=True)
async def dice(self, ctx):
with open("files/stats.json", "r") as fp:
s = json.load(fp)
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Dice Stats")
embed.description = "\n".join(["**{}**: {:,} ({}%)".format(
i, s["dice"][i], self.percent(s["dice"], i)) for i in sorted(s["dice"].keys())])
await ctx.send(embed=embed)
@stats.command(hidden=True)
async def rob(self, ctx):
with open("files/stats.json", "r") as fp:
s = json.load(fp)["rob"]
totalAttempts = s["robs_success"] + s["robs_failed"]
successPercent = round(100 * s["robs_success"] / totalAttempts, 2)
failedPercent = round(100 * s["robs_failed"] / totalAttempts, 2)
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Rob Stats")
embed.description = "**Geslaagd**: {:,} ({}%)\n**Gefaald**: {:,} ({}%)\n**Borg betaald**: {:,}".format(
s["robs_success"], successPercent, s["robs_failed"], failedPercent, round(s["bail_paid"])
)
await ctx.send(embed=embed)
@stats.command(name="Channels", aliases=["C", "CA"], usage="[#Channel]*", hidden=True)
@commands.check(checks.isMod)
async def channels(self, ctx, channel: discord.TextChannel = None):
res = stats.channel_activity(channel)
embed = discord.Embed(colour=discord.Colour.blue())
if channel:
embed.set_author(name="Channel Activity - {}".format(channel.name))
channel_instance = self.client.get_channel(int(res[0][0]))
embed.add_field(name="Aantal berichten", value="{:,}".format(round(float(res[0][1]), 2)), inline=False)
try:
last_message = await channel_instance.fetch_message(channel_instance.last_message_id)
except discord.NotFound:
last_message = None
if last_message is None:
embed.add_field(name="Laatste bericht", value="[Verwijderd]", inline=False)
else:
embed.add_field(name="Laatste bericht", value="[Jump URL]({})".format(last_message.jump_url), inline=False)
elif ctx.guild:
embed.set_author(name="Channel Activity - {}".format(ctx.guild))
description = ""
for c in sorted(res, key=lambda x: float(x[1]), reverse=True):
if not any(tc.id == int(c[0]) for tc in ctx.guild.text_channels):
continue
channel_instance = self.client.get_channel(int(c[0]))
description += "{}: {:,}\n".format(channel_instance.mention, round(float(c[1]), 2))
embed.description = description
else:
return await ctx.send("Dit commando werkt niet in DM's.")
return await ctx.send(embed=embed)
async def callStats(self, name, ctx):
await [command for command in self.stats.commands if command.name == name][0](ctx)
def percent(self, dic, stat):
total = sum([int(dic[s]) for s in dic])
if total == 0:
total = 1
return round(100 * int(dic[stat]) / total, 2)
def setup(client):
client.add_cog(Stats(client))

View File

@ -1,123 +0,0 @@
from converters.numbers import Abbreviated
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from enums.numbers import Numbers
from functions import checks
from functions.database import store, currency
from functions.numbers import getRep
class Store(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.group(name="Store", aliases=["Shop"], case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(Category.Currency)
async def store(self, ctx):
pass
# entries = store.getAllItems()
# await storePages.Pages(source=storePages.Source(entries), clear_reactions_after=True).start(ctx)
@store.command(name="Buy", aliases=["Get"], hidden=True)
async def store_buy(self, ctx, item, amount: Abbreviated = 1):
if amount is None:
return
await self.buy(ctx, item, amount)
@commands.command(name="Buy", aliases=["Get"], usage="[Item id] [Aantal]*")
@commands.check(checks.allowedChannels)
@help.Category(Category.Currency)
async def buy(self, ctx, item, amount: Abbreviated = 1):
if amount is None:
return
try:
item = int(item)
except ValueError:
return await ctx.send("Dit is geen geldig id.")
success, message = store.buy(ctx, ctx.author.id, item, amount)
if not success:
return await ctx.send(message)
rep = getRep(message["price"], Numbers.t.value)
return await ctx.send("**{}** heeft **{} {}{}** gekocht voor **{}** Didier Dink{}.".format(
ctx.author.display_name, amount, message["name"], checks.pluralS(amount),
rep, checks.pluralS(message["price"])
))
@store.command(name="Sell", hidden=True)
async def store_sell(self, ctx, itemid, amount: Abbreviated = 1):
if amount is None:
return
await self.sell(ctx, itemid, amount)
@commands.command(name="Sell", usage="[Item id] [Aantal]")
@commands.check(checks.allowedChannels)
@help.Category(Category.Currency)
async def sell(self, ctx, itemid, amount: Abbreviated = 1):
if amount is None:
return
try:
itemid = int(itemid)
except ValueError:
return await ctx.send("Dit is geen geldig id.")
inv = store.inventory(ctx.author.id)
if not inv or not any(int(item[0]) == itemid for item in inv):
return await ctx.send("Je hebt geen item met dit id.")
item_tuple = None
for item in inv:
if item[0] == itemid:
item_tuple = item
break
if str(amount).lower() == "all":
amount = int(item_tuple[2])
if int(item_tuple[2]) < amount:
return await ctx.send("Je hebt niet zoveel {}s.".format(item_tuple[1]))
store.sell(int(ctx.author.id), itemid, int(amount), int(item_tuple[2]))
price = int(store.getItemPrice(itemid)[0])
returnValue = round(0.8 * (price * amount))
currency.update(ctx.author.id, "dinks", currency.dinks(ctx.author.id) + returnValue)
await ctx.send("**{}** heeft **{} {}{}** verkocht voor **{}** Didier Dinks!".format(
ctx.author.display_name, amount, item_tuple[1], "s" if amount != 1 else "",
getRep(returnValue, Numbers.t.value)
))
@commands.command(name="Inventory", aliases=["Inv", "Items"])
@commands.check(checks.allowedChannels)
@help.Category(Category.Currency)
async def inventory(self, ctx, *args):
inv = store.inventory(ctx.author.id)
inv = sorted(inv, key=lambda x: x[1])
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Inventory van {}".format(ctx.author.display_name))
embed.set_thumbnail(url=str(ctx.author.avatar.url))
if len(inv) == 0:
embed.description = "Je hebt nog niets gekocht!\n" \
"Koop iets in de Store wanneer DJ STIJN niet langer te lui is om er iets in te steken."
else:
embed.description = "\n".join("#{} {}: {}".format(item[0], item[1], item[2]) for item in inv)
await ctx.send(embed=embed)
def setup(client):
client.add_cog(Store(client))

View File

@ -1,295 +0,0 @@
from data import constants
from data.remind import Reminders
from discord.ext import commands, tasks
from enums.numbers import Numbers
from functions import timeFormatters
from functions.config import config
from functions.database import currency, poke, prison, birthdays, stats
from functions.scrapers.sporza import getMatchweek
from functions import ufora_notifications
import json
import random
import requests
import time
class Tasks(commands.Cog):
def __init__(self, client):
self.client = client
self.bankInterest.start()
self.resetPrison.start()
self.resetLost.start()
# self.resetPoke.start()
self.checkBirthdays.start()
self.updateMessageCounts.start()
self.sendReminders.start()
self.updateMatchweek.start()
self.uforaAnnouncements.start()
@tasks.loop(hours=1.0)
async def bankInterest(self):
"""
Task that gives daily interest
"""
# Don't do it multiple times a day if bot dc's, ...
with open("files/lastTasks.json", "r") as fp:
lastTasks = json.load(fp)
if int(self.getCurrentHour()) == 4 and int(time.time()) - int(lastTasks["interest"]) > 10000:
users = currency.getAllRows()
bitcoinPrice = self.getCurrentBitcoinPrice()
for user in users:
# People in prison don't get interest
if len(prison.getUser(int(user[0]))) != 0:
continue
if float(user[3]) != 0.0:
currency.update(user[0], "investeddays", int(user[4]) + 1)
profit = ((float(user[3]) + float(user[5])) * (1 + (float(user[2]) * 0.01))) - float(user[3])
# Can't exceed 1 quadrillion
# Check BC as well so they can't put everything into BC to cheat the system
if float(user[1]) + float(user[3]) + float(user[5]) + profit + (float(user[8]) * bitcoinPrice) > Numbers.q.value:
# In case adding profit would exceed 1q, only add the difference
profit = Numbers.q.value - float(user[1]) - float(user[3]) - float(user[5]) - (float(user[8]) * bitcoinPrice)
# Don't reduce the current profit if Dinks were gained some other way (rob, bc, ...)
if profit > 0:
currency.update(user[0], "profit", float(user[5]) + profit)
await self.client.get_user(int(user[0])).send("Je hebt de invest-limiet van 1Q Didier Dinks bereikt.\nIndien je nog meer Didier Dinks wil sparen, kan je 1q Didier Dinks omruilen voor een Platinum Dink in de shop.")
else:
currency.update(user[0], "profit", float(user[5]) + profit)
lastTasks["interest"] = int(round(time.time()))
with open("files/lastTasks.json", "w") as fp:
json.dump(lastTasks, fp)
@bankInterest.before_loop
async def beforeBankInterest(self):
await self.client.wait_until_ready()
@tasks.loop(hours=1.0)
async def resetLost(self):
"""
Task that resets Lost Today
"""
# Don't do it multiple times a day if bot dc's, ...
with open("files/lastTasks.json", "r") as fp:
lastTasks = json.load(fp)
if int(self.getCurrentHour()) == 0 and int(time.time()) - int(lastTasks["lost"]) > 10000:
with open("files/lost.json", "r") as fp:
fc = json.load(fp)
fc["today"] = 0
with open("files/lost.json", "w") as fp:
json.dump(fc, fp)
lastTasks["lost"] = round(time.time())
with open("files/lastTasks.json", "w") as fp:
json.dump(lastTasks, fp)
@resetLost.before_loop
async def beforeResetLost(self):
await self.client.wait_until_ready()
@tasks.loop(hours=6.0)
async def resetPoke(self):
"""
Task that resets Poke
"""
if int(time.time()) - int(poke.get()[1]) > 259200:
await self.client.get_guild(int(self.client.constants.CallOfCode))\
.get_channel(int(self.client.constants.DidierPosting))\
.send("Poke is gereset door inactiviteit. <@!{}> is hem!".format(int(poke.reset())))
@resetPoke.before_loop
async def beforeResetPoke(self):
await self.client.wait_until_ready()
@tasks.loop(hours=1.0)
async def resetPrison(self):
"""
Task that lowers prison time daily
"""
# Don't do it multiple times a day if bot dc's, ...
with open("files/lastTasks.json", "r") as fp:
lastTasks = json.load(fp)
if int(self.getCurrentHour()) == 0 and int(time.time()) - int(lastTasks["prison"]) > 10000:
prison.dailyLowers()
with open("files/lastTasks.json", "w") as fp:
lastTasks["prison"] = round(time.time())
json.dump(lastTasks, fp)
@resetPrison.before_loop
async def beforeResetPrison(self):
await self.client.wait_until_ready()
@tasks.loop(hours=1.0)
async def checkBirthdays(self):
"""
Task that wishes people a happy birthday
"""
# Don't do it multiple times a day if bot dc's, ...
with open("files/lastTasks.json", "r") as fp:
lastTasks = json.load(fp)
if int(self.getCurrentHour()) == 6 and int(time.time()) - int(lastTasks["birthdays"]) > 10000:
dt = timeFormatters.dateTimeNow()
res = birthdays.get_users_on_date(dt.day, dt.month)
COC = self.client.get_guild(int(constants.CallOfCode))
people = [COC.get_member(int(user[0])) for user in res]
general = COC.get_channel(int(constants.CoCGeneral))
lastTasks["birthdays"] = round(time.time())
with open("files/lastTasks.json", "w") as fp:
json.dump(lastTasks, fp)
if not people:
return
if len(people) == 1:
return await general.send("Gelukkige verjaardag {}!".format(people[0].mention))
return await general.send("Gelukkige verjaardag {} en {}!".format(
", ".join(user.mention for user in people[:-1]),
people[-1].mention
))
@checkBirthdays.before_loop
async def beforecheckBirthdays(self):
await self.client.wait_until_ready()
@tasks.loop(hours=1.0)
async def updateMessageCounts(self):
"""
Task that updates the activity counter for channels
"""
# Don't do it multiple times a day if bot dc's, ...
with open("files/lastTasks.json", "r") as fp:
lastTasks = json.load(fp)
if int(self.getCurrentHour()) == 0 and int(time.time()) - int(lastTasks["channels"]) > 10000:
channels = stats.channel_activity()
for channel in channels:
stats.lower_channel(int(channel[0]), 0.95 * float(channel[1]))
with open("files/lastTasks.json", "w") as fp:
lastTasks["channels"] = round(time.time())
json.dump(lastTasks, fp)
@updateMessageCounts.before_loop
async def beforeupdateMessageCounts(self):
await self.client.wait_until_ready()
@tasks.loop(hours=1.0)
async def sendReminders(self):
"""
Task that sends people daily reminders
"""
# Don't do it multiple times a day if bot dc's, ...
with open("files/lastTasks.json", "r") as fp:
lastTasks = json.load(fp)
if int(self.getCurrentHour()) == 4 and int(time.time()) - int(lastTasks["remind"]) > 10000:
reminders = Reminders()
weekday = self.getCurrentWeekday()
for category in reminders.categories:
# Check if this reminder is temporarily disabled
if category["disabled"]:
continue
# Checks if this reminder can be sent on weekdays
if (not category["weekends"]) and weekday > 4:
continue
# Create embed once because this can be heavy
if "embed" in category and category["embed_once"]:
embed = category["embed"]()
else:
embed = None
for user in category["users"]:
userInstance = self.client.get_user(user)
# User can't be fetched for whatever reason, ignore instead of crashing
if userInstance is None:
continue
# Check if a special embed has to be attached for this reminder
if "embed" not in category:
await userInstance.send(random.choice(category["messages"]))
else:
# Embed has to be customized per user
if embed is None and "embed_once" in category:
# TODO clean this up, there's a better way :)
# remind category with a before- method & setup per user
args = category["argsf"](self.client, user)
embed = category["embed"](*args)
await userInstance.send(random.choice(category["messages"]), embed=embed)
with open("files/lastTasks.json", "w") as fp:
lastTasks["remind"] = round(time.time())
json.dump(lastTasks, fp)
@sendReminders.before_loop
async def beforeSendReminders(self):
await self.client.wait_until_ready()
@tasks.loop(hours=2.0)
async def updateMatchweek(self):
"""
Task that checks the current JPL matchweek & changes the dict value
"""
# Don't run this when testing
if self.client.user.id != int(constants.didierId):
return
matchweek = getMatchweek()
if matchweek is None:
return
# Change the setting in the config
config("jpl_day", int(matchweek))
@updateMatchweek.before_loop
async def beforeUpdateMatchweek(self):
await self.client.wait_until_ready()
@tasks.loop(minutes=10.0)
async def uforaAnnouncements(self):
"""
Task that checks for new Ufora announcements every few minutes
"""
# Don't run this when testing
if self.client.user.id != int(constants.didierId):
return
# Get new notifications
announcements = ufora_notifications.run()
if announcements:
announcements_channel = self.client.get_channel(816724500136591380)
for an in announcements:
await announcements_channel.send(embed=an.to_embed())
@uforaAnnouncements.before_loop
async def beforeUforaAnnouncements(self):
await self.client.wait_until_ready()
def getCurrentHour(self):
return timeFormatters.dateTimeNow().hour
def getCurrentWeekday(self):
return timeFormatters.dateTimeNow().weekday()
def getCurrentBitcoinPrice(self):
result = requests.get("https://api.coindesk.com/v1/bpi/currentprice.json").json()
currentPrice = result["bpi"]["EUR"]["rate_float"]
return float(currentPrice)
def setup(client):
client.add_cog(Tasks(client))

View File

@ -1,31 +0,0 @@
from discord.ext import commands
from functions import checks
class TestCog(commands.Cog):
def __init__(self, client):
self.client = client
def cog_check(self, ctx):
"""
Check executed for every command in this cog.
If necessary, create your own check here. A check is just a function
that returns True or False, and takes ctx as an argument. A command will
only be executed when this check returns True, which is why that is the default
implementation for this function.
"""
return True
@commands.command()
async def test(self, ctx):
pass
@test.error
async def test_handler(self, ctx, error):
raise error
def setup(client):
client.add_cog(TestCog(client))

View File

@ -1,116 +0,0 @@
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions.stringFormatters import title_case as tc
from googletrans import Translator, LANGUAGES
import re
class Translate(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.command(name="Translate", aliases=["Tl", "Trans"], usage="[Tekst] [Van]* [Naar]*")
@help.Category(Category.Words)
async def translate(self, ctx, query=None, to="nl", fr="auto"):
if query is None:
return await ctx.send("Controleer je argumenten.")
success, query = await self.getQuery(ctx, query)
if not success:
return await ctx.send(query)
translator = Translator()
# From & To were provided, swap them
if fr != "auto":
temp = fr
fr = to
to = temp
try:
translation = translator.translate(query, to, fr)
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Didier Translate")
if fr == "auto":
language = translation.src
embed.add_field(name="Gedetecteerde taal", value=tc(LANGUAGES[language]))
if translation.extra_data["confidence"] is not None:
embed.add_field(name="Zekerheid", value="{}%".format(translation.extra_data["confidence"] * 100))
embed.add_field(name="Origineel ({})".format(translation.src.upper()), value=query, inline=False)
embed.add_field(name="Vertaling ({})".format(to.upper()), value=translation.text)
await ctx.send(embed=embed)
except ValueError as e:
message = str(e)
if "destination" in message:
return await ctx.send("{} is geen geldige taal.".format(tc(to)))
if "source" in message:
return await ctx.send("{} is geen geldige taal.".format(tc(fr)))
raise e
# @commands.command(name="Detect", aliases=["Ld"], usage="[Tekst]")
# @help.Category(Category.Words)
async def detect(self, ctx, query=None):
if query is None:
return await ctx.send("Controleer je argumenten.")
success, query = await self.getQuery(ctx, query)
if not success:
return await ctx.send(query)
translator = Translator()
language = translator.detect(query)
confidence = language.confidence * 100
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Language Detection")
embed.add_field(name="Zin", value=query, inline=False)
embed.add_field(name="Gedetecteerde taal", value=tc(LANGUAGES[language.lang]))
embed.add_field(name="Zekerheid", value="{}%".format(confidence))
await ctx.send(embed=embed)
async def getQuery(self, ctx, query):
# Check if it's a link to a message
if re.match(r"^https://discord.com/channels/[0-9A-Za-z@]+/[0-9]+/[0-9]+$", query):
spl = query.split("/")
channel = self.client.get_channel(int(spl[-2]))
if channel is None:
return False, "Ik kan geen kanaal zien met dit id."
message = await channel.fetch_message(spl[-1])
if message is None:
return False, "Ik kan geen bericht zien met dit id."
query = message.content
else:
try:
# An id was passed instead
query = int(query)
message = await ctx.channel.fetch_message(query)
if message is None:
return False, "Ik kan geen bericht zien met dit id."
query = message.content
except ValueError:
pass
if not query:
return False, "Dit is geen geldig bericht."
return True, query
def setup(client):
client.add_cog(Translate(client))

View File

@ -1,67 +0,0 @@
from data import constants
import discord
from discord.ext import commands
from decorators import help
from enums.help_categories import Category
class Utils(commands.Cog):
def __init__(self, client):
self.client = client
self.client.locked = False
self.client.lockedUntil = -1
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
# Marco Polo to check if bot is running & delay
@commands.command(name="Marco")
@help.Category(category=Category.Didier)
async def marco(self, ctx):
await ctx.send("Polo! {}ms".format(round(self.client.latency * 1000)))
async def removeMessage(self, message):
try:
await message.delete()
except discord.Forbidden:
pass
# Send a DM to a user
async def sendDm(self, userid, message: str):
user = self.client.get_user(int(userid))
await user.send(message)
# Send an Embed to a user
async def sendEmbed(self, userid, embed):
await discord.utils.get(self.client.get_all_members(), id=int(userid)).send(embed=embed)
# Returns a member object of a user
def getMember(self, ctx, memberid):
if str(ctx.channel.type) == "private" or ctx.guild.get_member(int(memberid)) is None:
if str(memberid) == str(ctx.author.id):
return ctx.author
COC = self.client.get_guild(int(constants.CallOfCode))
return COC.get_member(int(memberid))
return ctx.guild.get_member(int(memberid))
# Returns a user's display name if he's in this server, else COC
def getDisplayName(self, ctx, memberid):
# Checks if this is a DM, or the user is not in the guild
if str(ctx.channel.type) == "private" or ctx.guild.get_member(int(memberid)) is None:
if str(memberid) == str(ctx.author.id):
return ctx.author.display_name
COC = self.client.get_guild(int(constants.CallOfCode))
member = COC.get_member(int(memberid))
if member is not None:
return member.display_name
return "[Persoon die de server misschien geleaved is | {}]".format(memberid)
mem = ctx.guild.get_member(int(memberid))
return mem.display_name
def setup(client):
client.add_cog(Utils(client))

View File

@ -1,52 +0,0 @@
from decorators import help
from discord.ext import commands
from enums.help_categories import Category
import requests
class Words(commands.Cog):
def __init__(self, client):
self.client = client
# Don't allow any commands to work when locked
def cog_check(self, ctx):
return not self.client.locked
@commands.command(name="Adjective", aliases=["Adj", "Adjectives"], usage="[Woord]")
@help.Category(category=Category.Words)
async def adjective(self, ctx, word=None):
await self.getData(ctx, word, "rel_jjb")
@commands.command(name="Synonym", aliases=["Syn", "Synonyms"], usage="[Woord]")
@help.Category(category=Category.Words)
async def synonym(self, ctx, word=None):
await self.getData(ctx, word, "rel_syn")
@commands.command(name="Antonym", aliases=["Ant", "Antonyms", "Opp", "Opposite"], usage="[Woord]")
@help.Category(category=Category.Words)
async def antonym(self, ctx, word=None):
await self.getData(ctx, word, "rel_ant")
@commands.command(name="Rhyme", aliases=["Rhymes"], usage="[Woord]")
@help.Category(category=Category.Words)
async def rhyme(self, ctx, word=None):
await self.getData(ctx, word, "rel_rhy")
# Contacts the API & returns the response, as these commands all do the same anyways
async def getData(self, ctx, word, relation):
if not word:
await ctx.send("Geef een woord op.")
return
res = requests.get("https://api.datamuse.com/words?{}={}".format(relation, word)).json()
# Only show top 20 results
res = res if len(res) <= 15 else res[:15]
# Pull the words out of the dicts
res = [word["word"] for word in res]
await ctx.send(", ".join(res) if len(res) > 0 else "Geen resultaten gevonden.")
def setup(client):
client.add_cog(Words(client))

View File

@ -1,45 +0,0 @@
from data import constants
from decorators import help
import discord
from discord.ext import commands
from enums.help_categories import Category
from functions import checks, xp
from functions.database import stats
class Xp(commands.Cog):
def __init__(self, client):
self.client = client
@commands.group(name="Xp", aliases=["Level", "Mc", "Mess", "Messages"], case_insensitive=True, invoke_without_command=True)
@commands.check(checks.allowedChannels)
@help.Category(Category.Other)
async def xp(self, ctx, user: discord.Member = None):
# if user is not None and str(ctx.author.id) != constants.myId:
# return await ctx.send("Je hebt geen toegang tot dit commando.")
target = user if user is not None else ctx.author
target_stats = stats.getOrAddUser(target.id)
message_count = stats.getTotalMessageCount()
perc = round(int(target_stats[11]) * 100/message_count, 2)
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name=target.display_name, icon_url=target.avatar.url)
embed.add_field(name="Aantal Berichten", value="{:,} ({}%)".format(int(target_stats[11]), perc))
embed.add_field(name="Level", value=str(xp.calculate_level(target_stats[12])))
embed.add_field(name="XP", value="{:,}".format(int(target_stats[12])))
embed.set_footer(text="*Sinds Didier 2.0 Launch")
await ctx.send(embed=embed)
@xp.command(name="Leaderboard", aliases=["Lb"], hidden=True)
async def xpleaderboard(self, ctx, *args):
if any(alias in ctx.message.content for alias in ["mc", "mess", "messages"]):
return await self.client.get_cog("Leaderboards").callLeaderboard("Messages", ctx)
await self.client.get_cog("Leaderboards").callLeaderboard("Xp", ctx)
def setup(client):
client.add_cog(Xp(client))

View File

@ -1,67 +0,0 @@
import re
from discord.ext import commands
# Gets the numerical value of a string representation like 1.6k
def abbreviated(rep):
if rep.lower() == "all":
return rep
validMatch = r"^-?[0-9]+\.?[0-9]*[A-Za-z]*$"
numericalValue = r"^-?[0-9]+\.?[0-9]*"
indicator = r"[A-Za-z]*$"
valid = re.match(validMatch, rep)
# Invalid representation
if not valid:
return None
numerical = re.search(numericalValue, rep)
# Invalid number
if float(numerical[0]) == 0.0:
return None
indic = re.search(indicator, rep)
if not indic[0]:
try:
return int(rep)
except ValueError:
# If no indicator was passed, it has to be a whole number
return None
# Invalid indicator
if indic[0] not in exponents() and not any(exp.lower() == indic[0].lower() for exp in exponents()):
return None
if indic[0] in exponents():
try:
return int(float(numerical[0]) * int("1" + ("0" * (exponents().index(indic[0]) + 1) * 3)))
except ValueError:
# Can't be cast to int
return None
for i, exponent in enumerate(exponents()):
if exponent.lower() == indic[0].lower():
try:
return int(float(numerical[0]) * int("1" + ("0" * (i + 1) * 3)))
except ValueError:
# Can't be cast to int
return None
class Abbreviated(commands.Converter):
async def convert(self, ctx, argument):
if argument is None:
return None
converted = abbreviated(argument)
if converted is None:
await ctx.send("Dit is geen geldig getal.")
return None
return converted
def exponents():
return ["K", "M", "B", "t", "q", "Q", "s", "S", "o", "n", "d", "U", "D", "T", "Qt", "Qd", "Sd", "St", "O", "N", "v"]

View File

@ -1,121 +0,0 @@
# Creating Commands
The following document shows you how to create new commands, which kwargs to add, and what they do.
Make sure to properly register the appropriate help decorator to each command. More info on help decorators in `readme.md`.
Do _NOT_ create commands outside of `Cogs`!
### Commands
Commands require a set list of kwargs in order to work correctly, and show up in the help page.
- Name: The name of the command, with only the first letter capitalised.
- Aliases: Optional, a list of all aliases the command can have. Make sure to check if the alias already exists (if not, Discord will throw an error when starting the bot.)
Aliases should only have the first letter capitalised, and be placed in alphabetical order.
- Usage: Optional, only required when the command takes arguments. Show how the command has to be used. Arguments should be placed between \[square brackets\], and optional arguments need an asterisk* after the brackets.
- Hidden: Optional, should only be added when you _don't_ want the command to show up in the help page.
- Ignore_Extra: Optional, should only be used when you don't want the command to be called when too many arguments are passed.
This defaults to `True`, so if you don't care about extra arguments then adding this is obsolete. Setting this to `False` makes sentences like _Didier Bank is a cool command_ not trigger the `Didier Bank` command.
##### Checks
Further, checks can be added to commands to make sure the function only executes in certain situations. There are already a few [built-in checks](https://discordpy.readthedocs.io/en/latest/ext/commands/api.html?highlight=checks#checks) to do commonly-used things, but you can easily create your own functions to do this.
New checks should be added to `/functions/checks.py`. Checks are just a basic function that returns `True` or `False`, and takes `ctx` as a parameter.
The example below shows how to create a command that only takes 1 required argument, 1 optional argument, and does not show up in the help page. The `check` makes sure the command does not work in DM's.
```python
def check(ctx):
return ctx.guild is not None
@commands.command(name="Name", aliases=["N"], usage=["[Arg], [Arg2]*"], hidden=True, ignore_extra=True)
@commands.check(check=check)
async def name(ctx, arg, arg2=None):
pass
```
### Groups
It's possible that you might want to create subcommands for a certain command. At first glance, you might want to do something like this:
```python
@commands.command()
async def parent_command(ctx, arg):
if arg == "sub1":
await sub1()
elif arg == "sub2":
await sub2()
...
async def sub1():
pass
async def sub2():
pass
```
Looking at this you might think that there must be a better way to do this, and there is. The only situation where this type of code will be accepted, is when there are too many possible subcommands to the point where it's not really doable to create all of them (think help & faq categories).
Discord.py has implemented [Command Groups](https://discordpy.readthedocs.io/en/latest/ext/commands/api.html?highlight=group#discord.ext.commands.Group) for this purpose. Groups allow you to register commands to other commands in order to create subcommands (or even subgroups!).
Groups _do_ have a few extra kwargs on top of the usual `Command Kwargs` mentioned above:
- case_insensitive: Indicates whether or not subcommands should be case-insensitive (think `lb dinks` and `lb DINKS`). You are *required* to set this parameter to `True` so Didier will always work case-insensitively.
- invoke_without_command: Indicates whether or not the group command should only execute if no subcommand was found. In most cases, this will be `True`.
The example below shows how to create a group so that `Didier A B D` will execute `b("D")`, but `Didier A C` will execute `a("C")`.
```python
@commands.group(name="A", usage="[Args]", case_insensitive=True, invoke_without_command=True)
async def a(ctx, args):
print(args)
# Prints "C"
@a.command(name="B")
async def b(ctx, args):
print(args)
# Prints "D"
```
### Unpacking Groups
It's possible that you want to create a group to organise subcommands, but want the subcommands to be listed in the category instead of under the group (or when the Cog and Group have the same name). An example is Didier's `Random` module.
When creating a group without doing anything, the help page will look like this:
Categories:
Currency
...
Random (category)
...
Help Random (category):
Random (group)
Help Random (group):
Random Choice
...
This requires an unnecessary step to get to the commands (as there's only 1 result), and it would be nicer to have all subcommands list in the category instead. Seeing as the Cog & Group both have the same name, it also feels weird to do this (and would be near impossible to code in a user-friendly way). When the Cog has the same name, you expect all the commands to be right there immediately.
The `Help Decorator` has an optional argument that can do this for you. When registering a group to a category, you can add `unpack=True` in order to accomplish this.
```python
@commands.group(name="Random")
@help.Category(Category.Random, unpack=True)
async def random(self, ctx):
pass
```
This way, the help page will look like this:
Random (category):
Random Choice
...
### Testing & Messing Around
In case you want to quickly test 1 line of code, or test a quick idea, you'd have to create an entire Cog just to use once. With the Cog Template this is not _that_ bad, but it's still a bit cumbersome.
For this purpose, there is a Cog called `testCog.py`, with 1 command (`Test()`). Slap your code in there, and use `Didier Test` in Discord. Just remember to never commit these changes (right click in the file manager -> `Git` -> `Rollback`).

View File

@ -1,77 +0,0 @@
from enum import Enum
myId = "171671190631481345"
didierId = "680510935164911730"
coolerDidierId = "728361496874057812"
botIDs = [
"155149108183695360",
"234395307759108106",
"239631525350604801",
"408785106942164992",
"679679176466235399",
"680510935164911730",
"706148003949314069",
"728361496874057812"
]
BugReports = "762668401960812554"
BotTesting = "679701786189103106"
CallOfCode = "626699611192688641"
CoCGeneral = "626699611813314561"
DeZandbak = "728361030404538488"
ErrorLogs = "762668505455132722"
FeatureRequests = "762668473313787964"
ZandbakSpeeltuin = "769248992957038612"
mods = {
626699611192688641: [384457911377854467, 171671190631481345],
728361030404538488: [171671190631481345],
689200072268841106: [332948151226990614, 171671190631481345, 280025474485321729, 237239312385703951],
710515840566427721: [171671190631481345, 140186024075722752, 296197359539585024]
}
allowedChannels = {
"bot-games": 634327239361691658,
"bot-commands": 629034637955694602,
"bot-testing": 679701786189103106,
"shitposting": 676713433567199232,
"didier-testings": 689202224437395573,
"freegames": 705745297908826113,
"bot-commandsCOCGI": 714170653124722738,
"generalZandbak": 728361031008780422,
"freegamesZandbak": 728545397127118898,
"spelenZandbak": 769248992957038612,
"warzone": 691933718880714843,
"dangerzone": 918519109203931136
}
creationDate = 1582243200
holidayAPIKey = "af4e1ebe-465d-4b93-a828-b95df18e6424"
prefixes = ["big d", "didier"]
faq_channels = {
727876753523081216: "ad2",
727876779481497600: "comnet",
727876797458284584: "funcprog",
727876819264733244: "statprob",
727876836587208714: "sysprog",
676713433567199232: "didier",
807566495550013460: "comparch",
807567007355895838: "multimedia",
807567261216538644: "sel1",
807567345484169237: "webdev",
807567387775336499: "wetrek"
}
class Live(Enum):
CallOfCode = "626699611192688641"
DidierPosting = "728361031008780422"
General = "626699611813314561"
class Zandbak(Enum):
CallOfCode = "728361030404538488"
DidierPosting = "728361031008780422"
General = "728361031008780422"

View File

@ -1,67 +0,0 @@
from dataclasses import dataclass
from typing import Optional
import dacite
import json
from os import path
@dataclass
class Course:
abbreviations: list[str]
code: str
name: str
year: int
alt: Optional[str] = None
def load_courses() -> dict[str, Course]:
"""Create a list of all courses"""
# Allows testing
filepath = path.join(path.dirname(__file__), "..", "files", "courses.json")
with open(filepath, "r") as file:
data = json.load(file)
courses = {}
for course_name in data:
# Add name into the dict to allow flexibility
course_data = data[course_name]
course_data["name"] = course_name
courses[course_name] = dacite.from_dict(data_class=Course, data=course_data)
return courses
def find_course_from_name(name: str, courses: Optional[dict[str, Course]] = None, case_insensitive: bool = True) -> Optional[Course]:
# Allow passing a course dict in to avoid having to create it all the time
if courses is None:
courses = load_courses()
if case_insensitive:
name = name.lower()
def _perhaps_lower(inp: str) -> str:
"""Cast a string to lowercase if necessary"""
if case_insensitive:
return inp.lower()
return inp
# Iterate over all courses to look for a match
for course_name, course in courses.items():
# Check name first
if _perhaps_lower(course_name) == name:
return course
# Then abbreviations
for abbreviation in course.abbreviations:
if _perhaps_lower(abbreviation) == name:
return course
# Finally alternative names
if course.alt is not None and _perhaps_lower(course.alt) == name:
return course
return None

View File

@ -1,12 +0,0 @@
from attr import dataclass
@dataclass
class CustomCommand:
"""
Class to store custom commands being triggered
"""
id: int = None
name: str = None
response: str = None
alias_used: str = None

View File

@ -1 +0,0 @@
from .ufora import UforaNotification

View File

@ -1,70 +0,0 @@
import json
import time
from dataclasses import dataclass
from discord import Embed, Colour
from functions.stringFormatters import get_edu_year
from typing import Dict
"""
Sample json structure:
{
"1": {
"ad1": {
"proj1": 123456789
}
}
}
"""
@dataclass
class Deadline:
course: str
name: str
t: int
passed: bool
def __str__(self) -> str:
v = f"{self.course} - {self.name}: <t:{self.t}:R>"
if self.passed:
v = f"~~{v}~~"
return v
class Deadlines:
data: Dict
def __init__(self):
with open("files/deadlines.json") as f:
self.data = json.load(f)
def to_embed(self) -> Embed:
embed = Embed(colour=Colour.dark_gold())
embed.set_author(name="Aanstaande Deadlines")
now = time.time()
courses: Dict
for year, courses in sorted(self.data.items(), key=lambda x: x[0]):
content = []
deadlines: Dict[str, int]
for course, deadlines in courses.items():
for deadline, t in deadlines.items():
content.append(Deadline(course, deadline, t, t < now))
content.sort(key=lambda x: x.t)
content = list(map(lambda x: str(x), content))
if content:
embed.add_field(name=get_edu_year(int(year)), value="\n".join(content), inline=False)
# No deadlines planned
if not embed.fields:
embed.description = "Er staan geen deadlines gepland."
embed.set_image(url="https://c.tenor.com/RUzJ3lDGQUsAAAAC/iron-man-you-can-rest-now.gif")
return embed

View File

@ -1,55 +0,0 @@
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional, Tuple, Dict
import discord
from functions import les, eten
from functions.timeFormatters import skip_weekends, intToWeekday
from functions.stringFormatters import leading_zero as lz
restos: Dict[str, str] = {
"ardoyen": "Ardoyen",
"coupure": "Coupure",
"debrug": "De Brug",
"dunant": "Dunant",
"heymans": "Heymans",
"kantienberg": "Kantienberg",
"merelbeke": "Merelbeke",
"sterre": "Sterre"
}
@dataclass
class Menu:
day: Optional[str] = None
resto: str = "sterre"
_day: datetime = field(init=False)
_menu: Tuple[str, str, str, str] = field(init=False)
def __post_init__(self):
self._day = les.find_target_date(self.day if self.day else None)
self._day = skip_weekends(self._day)
self.day = intToWeekday(self._day.weekday())
self._menu = eten.etenScript(self._day)
def to_embed(self) -> discord.Embed:
embed = discord.Embed(colour=discord.Colour.blue())
date_formatted = f"{lz(self._day.day)}/{lz(self._day.month)}/{self._day.year}"
embed.set_author(name=f"Menu voor {self.day.lower()} {date_formatted}")
# embed.title = f"Resto {restos[self.resto]}"
if "gesloten" in self._menu[0].lower():
embed.description = "Restaurant gesloten"
else:
embed.add_field(name="🥣 Soep:", value=self._menu[0], inline=False)
embed.add_field(name="🍴 Hoofdgerechten:", value=self._menu[1], inline=False)
if self._menu[2]:
embed.add_field(name="❄️ Koud:", value=self._menu[2], inline=False)
if self._menu[3]:
embed.add_field(name="🥦 Groenten:", value=self._menu[3], inline=False)
return embed

View File

@ -1,40 +0,0 @@
from data.snipe import Snipe
import discord
from startup.didier import Didier
class EditSnipe:
"""
Creates an Embed to snipe people that edited a message
"""
def __init__(self, snipe: Snipe):
self.snipe = snipe
def to_embed(self, client: Didier) -> discord.Embed:
guild: discord.Guild = client.get_guild(self.snipe.guild)
member: discord.Member = guild.get_member(self.snipe.user)
embed = discord.Embed(title="Edit Snipe", colour=discord.Colour.blue())
embed.set_author(name=member.display_name, icon_url=member.avatar.url)
embed.add_field(name="Voor", value=self.snipe.old, inline=False)
embed.add_field(name="Na", value=self.snipe.new, inline=False)
return embed
class DeleteSnipe:
"""
Creates an Embed to snipe people that removed a message
"""
def __init__(self, snipe: Snipe):
self.snipe = snipe
def to_embed(self, client: Didier) -> discord.Embed:
guild: discord.Guild = client.get_guild(self.snipe.guild)
member: discord.Member = guild.get_member(self.snipe.user)
embed = discord.Embed(title="Delete Snipe", colour=discord.Colour.blue())
embed.set_author(name=member.display_name, icon_url=member.avatar.url)
embed.add_field(name="Message", value=self.snipe.old)
return embed

View File

@ -1,61 +0,0 @@
import discord
from googletrans import Translator, LANGUAGES
from functions.stringFormatters import title_case
from typing import Optional
class Translation:
def __init__(self, text: str, fr: str, to: str):
self.text = text
self.fr = fr
self.to = to
self.embed: Optional[discord.Embed] = None
self.translation = None
self.translate(text, fr, to)
def translate(self, query: str, fr: str, to: str):
"""
Translate [query] into [to]
"""
try:
translator = Translator()
self.translation = translator.translate(query, to, fr)
except ValueError as e:
message = str(e)
if "destination" in message:
self._create_error_embed(f"{title_case(to)} is geen geldige taal.")
return
if "source" in message:
self._create_error_embed(f"{title_case(fr)} is geen geldige taal.")
return
raise e
def _create_error_embed(self, message):
embed = discord.Embed(colour=discord.Colour.red())
embed.set_author(name="Didier Translate")
embed.description = message
self.embed = embed
def to_embed(self) -> discord.Embed:
# There's an error embed to show
if self.embed is not None:
return self.embed
embed = discord.Embed(colour=discord.Colour.blue())
embed.set_author(name="Didier Translate")
if self.fr == "auto":
language = self.translation.src
embed.add_field(name="Gedetecteerde taal", value=title_case(LANGUAGES[language]))
if self.translation.extra_data["confidence"] is not None:
embed.add_field(name="Zekerheid", value="{}%".format(self.translation.extra_data["confidence"] * 100))
embed.add_field(name="Origineel ({})".format(self.translation.src.upper()), value=self.text, inline=False)
embed.add_field(name="Vertaling ({})".format(self.to.upper()), value=self.translation.text)
return embed

View File

@ -1,68 +0,0 @@
from datetime import datetime
from discord import Embed, Colour
from functions.stringFormatters import leading_zero as lz
from functions.timeFormatters import intToWeekday
from markdownify import markdownify as md
import pytz
class UforaNotification:
def __init__(self, content: dict, course, notif_id, course_id):
self._content: dict = content
self._course = course
self._notif_id = notif_id
self._course_id = course_id
self._view_url = self._create_url()
self._title = self._clean_content(self._content["title"])
self._description = self._get_description()
self._published = self._get_published()
def to_embed(self):
embed = Embed(colour=Colour.from_rgb(30, 100, 200))
embed.set_author(name=self._course)
embed.title = self._title
embed.url = self._view_url
embed.description = self._description
embed.set_footer(text=self._published)
return embed
def get_id(self):
return int(self._notif_id) if self._notif_id is not None else self._content["id"]
def _create_url(self):
if self._notif_id is None or self._course_id is None:
return self._content["link"]
return "https://ufora.ugent.be/d2l/le/news/{0}/{1}/view?ou={0}".format(self._course_id, self._notif_id)
def _get_description(self):
desc = self._clean_content(self._content["summary"])
if len(desc) > 4096:
return desc[:4093] + "..."
return desc
def _clean_content(self, text: str):
# Escape *-characters because they mess up the layout
text = text.replace("*", "\\*")
return md(text)
def _get_published(self):
# Datetime is unable to parse the timezone because it's useless
# We will hereby cut it out and pray the timezone will always be UTC+0
published = self._content["published"].rsplit(" ", 1)[0]
time_string = "%a, %d %b %Y %H:%M:%S"
dt = datetime.strptime(published, time_string)\
.astimezone(pytz.timezone("Europe/Brussels"))
# Apply timezone offset in a hacky way
dt = dt + dt.utcoffset()
return "{} {}/{}/{} om {}:{}:{}".format(
intToWeekday(dt.weekday()),
lz(dt.day), lz(dt.month), lz(dt.year),
lz(dt.hour), lz(dt.minute), lz(dt.second)
)

View File

@ -1,112 +0,0 @@
import discord
import os
import requests
from typing import Dict
class Definition:
def __init__(self, query: str):
self.query = query
self.definition = Definition.lookup(query)
@staticmethod
def lookup(word) -> Dict:
"""
Function that sends the API request to get the definition.
:param word: the woord to look up
:return: a dictionary representing the info of this word
"""
url = "https://mashape-community-urban-dictionary.p.rapidapi.com/define"
querystring = {"term": word}
headers = {
'x-rapidapi-host': "mashape-community-urban-dictionary.p.rapidapi.com",
'x-rapidapi-key': os.getenv("URBANDICTIONARY")
}
try:
if word.lower() == "didier":
return Definition.define_didier()
response = requests.get(url, headers=headers, params=querystring).json()["list"]
if len(response) > 0:
return {"word": response[0]["word"], "definition": response[0]["definition"],
"example": response[0]["example"], "thumbs_up": response[0]["thumbs_up"],
"thumbs_down": response[0]["thumbs_down"], "link": response[0]["permalink"],
"author": response[0]["author"]}
# No valid response
return {}
except Exception:
return Definition.define_didier()
@staticmethod
def clean_string(text: str):
"""
Function that cuts off definitions that are too long & strips out UD markdown
from an input string.
:param text: the input string to clean up
:return: the edited version of the string
"""
text = text.replace("[", "")
text = text.replace("]", "")
if not text:
return "N/A"
return text if len(text) < 1024 else text[:1021] + "..."
@staticmethod
def ratio(dic) -> float:
"""
Function that calculates the upvote/downvote ratio of the definition.
:param dic: the dictionary representing the definition
:return: the upvote/downvote ratio (float)
"""
return (100 * int(dic["thumbs_up"])) / (int(dic["thumbs_up"]) + int(dic["thumbs_down"])) \
if int(dic["thumbs_down"]) != 0 else 100.0
@staticmethod
def define_didier() -> Dict:
"""
Function that returns a stock dictionary to define Didier
in case people call it, or no definition was found.
:return: a dictionary that defines Didier
"""
return {"word": "Didier", "definition": "Didier", "example": "1: Didier\n2: Hmm?", "thumbs_up": 69420,
"thumbs_down": 0, "author": "Didier",
"link": "https://upload.wikimedia.org/wikipedia/commons/a/a5"
"/Didier_Reynders_in_Iranian_Parliament_02.jpg"}
def to_embed(self) -> discord.Embed:
"""
Create an embed for this definition
"""
# No results found
if not self.definition:
return self._nothing_found_embed()
embed = discord.Embed(colour=discord.Colour.from_rgb(220, 255, 0))
embed.set_author(name="Urban Dictionary")
embed.add_field(name="Woord", value=self.definition["word"], inline=True)
embed.add_field(name="Auteur", value=self.definition["author"], inline=True)
embed.add_field(name="Definitie", value=Definition.clean_string(self.definition["definition"]), inline=False)
embed.add_field(name="Voorbeeld", value=Definition.clean_string(self.definition["example"]), inline=False)
embed.add_field(name="Rating", value=str(round(Definition.ratio(self.definition), 2)) + "%")
embed.add_field(name="Link naar de volledige definitie",
value="[Urban Dictionary]({})".format(str(self.definition["link"])))
return embed
def _nothing_found_embed(self) -> discord.Embed:
"""
Special embed when no results could be found
"""
embed = discord.Embed(colour=discord.Colour.red(), title=self.query[:256])
embed.set_author(name="Urban Dictionary")
embed.description = "Geen resultaten gevonden"
return embed

View File

@ -1,28 +0,0 @@
import discord
from requests import get
from functions.stringFormatters import leading_zero
class XKCDEmbed:
n: int
def __init__(self, n: int = None):
self.n = n
def create(self) -> discord.Embed:
endpoint = "https://xkcd.com/info.0.json" if self.n is None else f"https://xkcd.com/{self.n}/info.0.json"
response = get(endpoint)
if response.status_code != 200:
embed = discord.Embed(colour=discord.Colour.red())
embed.set_author(name="xkcd")
embed.description = f"Er ging iets mis (status {response.status_code})."
return embed
data = response.json()
embed = discord.Embed(colour=discord.Colour.from_rgb(150, 168, 200), title=data["safe_title"])
embed.set_author(name=f"xkcd #{data['num']}")
embed.set_image(url=data["img"])
embed.set_footer(text=f"{leading_zero(data['day'])}/{leading_zero(data['month'])}/{data['year']}")
return embed

View File

@ -1,16 +0,0 @@
import json
from typing import Optional
def load_all_links() -> dict[str, str]:
with open("files/links.json", "r") as file:
return json.load(file)
def get_link_for(name: str) -> Optional[str]:
links = load_all_links()
for link in links:
if link.lower() == name.lower():
return links[link]
return None

View File

@ -1,18 +0,0 @@
from typing import Union
from discord import ApplicationContext
from discord.ext.commands import Context
from data.menus.paginated import Paginated
from functions.database.custom_commands import get_all
from functions.stringFormatters import capitalize
class CommandsList(Paginated):
def __init__(self, ctx: Union[ApplicationContext, Context]):
all_commands = get_all()
commands_sorted = list(sorted(map(lambda x: (capitalize(x["name"]),), all_commands)))
super().__init__(ctx=ctx, title="Custom Commands", data=commands_sorted, per_page=15)
def format_entry(self, index: int, value: tuple) -> str:
return value[0]

View File

@ -1,307 +0,0 @@
import json
import math
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Union, Optional
import discord
import requests
from discord import ApplicationContext
from discord.ext.commands import Context
import settings
from data.menus.paginated import Paginated
from enums.numbers import Numbers
from functions import xp
from functions.database import currency, stats, poke, muttn
from functions.utils import get_display_name, get_mention
@dataclass
class Leaderboard(Paginated, ABC):
highlight: str = None
colour: discord.Colour = discord.Colour.blue()
fetch_names: bool = True
ignore_non_pos: bool = True
reverse: bool = True
def __post_init__(self):
self.data = self.process_data(self.get_data())
@abstractmethod
def get_data(self) -> list[tuple]:
pass
def process_data(self, entries: list[tuple]) -> Optional[list[tuple]]:
data = []
for i, v in enumerate(sorted(entries, key=self.get_value, reverse=self.reverse)):
entry_data = self.get_value(v)
# Leaderboard is empty
if i == 0 and entry_data == 0 and self.ignore_non_pos:
return None
# Ignore entries with no data
if self.ignore_non_pos and entry_data <= 0:
continue
data.append((self.get_key(v), f"{entry_data:,}", entry_data,))
return data
def get_key(self, data: tuple):
return data[0]
def get_value(self, data: tuple):
return data[1]
def _should_highlight(self, data) -> bool:
"""Check if an entry should be highlighted"""
if self.fetch_names:
return data == self.ctx.author.id
return data == self.highlight
def format_entry_data(self, data: tuple) -> str:
return str(data[1])
def format_entry(self, index: int, data: tuple) -> str:
name = data[0]
if self.fetch_names:
name = get_display_name(self.ctx, int(data[0]))
s = f"{index + 1}: {name} ({self.format_entry_data(data)})"
if self._should_highlight(data[0]):
return f"**{s}**"
return s
@property
def empty_description(self) -> str:
return ""
async def empty_leaderboard(self, ctx: Union[ApplicationContext, Context], **kwargs):
embed = discord.Embed(colour=self.colour)
embed.set_author(name=self.title)
embed.description = self.empty_description
if isinstance(ctx, ApplicationContext):
return await ctx.respond(embed=embed)
return await ctx.reply(embed=embed, **kwargs)
async def respond(self, **kwargs) -> discord.Message:
if self.data is None or not self.data:
return await self.empty_leaderboard(self.ctx, **kwargs)
return await super().respond(**kwargs)
async def send(self, **kwargs) -> discord.Message:
if self.data is None or not self.data:
return await self.empty_leaderboard(self.ctx, **kwargs)
return await super().send(**kwargs)
@dataclass
class BitcoinLeaderboard(Leaderboard):
title: str = field(default="Bitcoin Leaderboard")
def get_data(self) -> list[tuple]:
return currency.getAllRows()
def get_value(self, data: tuple):
return round(float(data[8]), 8)
@property
def empty_description(self) -> str:
return "Er zijn nog geen personen met Bitcoins."
@dataclass
class CompbioLeaderboard(Leaderboard):
colour: discord.Colour = field(default=discord.Colour.green())
title: str = field(default="Leaderboard Computationele Biologie #4")
reverse: bool = False
size: int = 10000
amount: int = 10
def __post_init__(self):
self.title += f" ({self.size}-{self.amount})"
super().__post_init__()
def get_submission_user(self, submission_id: str) -> str:
with open("files/compbio_benchmarks_4.json", "r") as fp:
file = json.load(fp)
if submission_id in file:
user_id = file[submission_id]
return get_mention(self.ctx, user_id)
return f"[# {submission_id}]"
def get_data(self) -> list[tuple]:
url = f"https://github.ugent.be/raw/computationele-biologie/benchmarks-2022/main/profile_hmm/size{self.size}-amount{self.amount}.md"
headers = {"Authorization": f"token {settings.UGENT_GH_TOKEN}"}
result = requests.get(url, headers=headers).text
# Remove table headers
result = result.split("\n")[2:]
data = []
for line in result:
try:
cells = line.split("|")
submission_id = cells[1].strip()
mean = float(cells[2].strip().split(" ")[0])
except IndexError:
# Other lines because of markdown formatting
continue
data.append((submission_id, mean, ))
return data
def _should_highlight(self, data) -> bool:
# TODO maybe find a fix for this?
return False
def format_entry(self, index: int, data: tuple) -> str:
return f"{index + 1}: {self.get_submission_user(data[0])} ({self.format_entry_data(data)})"
def format_entry_data(self, data: tuple) -> str:
return f"{str(data[1])} ms"
def get_value(self, data: tuple):
return data[1]
@dataclass
class CoronaLeaderboard(Leaderboard):
colour: discord.Colour = field(default=discord.Colour.red())
fetch_names: bool = field(default=False)
highlight: str = field(default="Belgium")
title: str = field(default="Corona Leaderboard")
def get_data(self) -> list[tuple]:
result = requests.get("https://disease.sh/v3/covid-19/countries").json()
result.sort(key=lambda x: int(x["cases"]), reverse=True)
data = []
for country in result:
data.append((country["country"], f"{country['cases']:,}", country["cases"]))
return data
def get_value(self, data: tuple):
return data[2]
@dataclass
class DinksLeaderboard(Leaderboard):
title: str = field(default="Dinks Leaderboard")
def get_data(self) -> list[tuple]:
entries = currency.getAllRows()
platDinks = currency.getAllPlatDinks()
# Take platinum dinks into account
for i, user in enumerate(entries):
if str(user[0]) in platDinks:
# Tuples don't support assignment, cast to list
user = list(user)
user[1] += platDinks[str(user[0])] * Numbers.q.value
entries[i] = user
return entries
def get_value(self, data: tuple):
return float(data[1]) + float(data[3])
@property
def empty_description(self) -> str:
return "Er zijn nog geen personen met Didier Dinks."
@dataclass
class MessageLeaderboard(Leaderboard):
title: str = field(default="Message Leaderboard")
message_count: int = field(init=False)
def get_data(self) -> list[tuple]:
entries = stats.getAllRows()
self.message_count = stats.getTotalMessageCount()
return entries
def get_value(self, data: tuple):
return round(int(data[11]))
def format_entry_data(self, data: tuple) -> str:
perc = round(data[2] * 100 / self.message_count, 2)
return f"{data[2]:,} | {perc}%"
@dataclass
class MuttnLeaderboard(Leaderboard):
title: str = field(default="Muttn Leaderboard")
def get_data(self) -> list[tuple]:
return muttn.getAllRows()
def get_value(self, data: tuple):
return round(float(data[1]), 2)
def format_entry_data(self, data: tuple) -> str:
return f"{data[2]}%"
def empty_description(self) -> str:
return "Der zittn nog geen muttns in de server."
@dataclass
class PokeLeaderboard(Leaderboard):
title: str = field(default="Poke Leaderboard")
def get_data(self) -> list[tuple]:
data = stats.getAllRows()
blacklist = poke.getAllBlacklistedUsers()
return list(filter(lambda x: x[0] not in blacklist, data))
def get_value(self, data: tuple):
return round(int(data[1]))
@property
def empty_description(self) -> str:
return "Er is nog niemand getikt."
@dataclass
class RobLeaderboard(Leaderboard):
title: str = field(default="Rob Leaderboard")
def get_data(self) -> list[tuple]:
return list(stats.getAllRows())
def get_value(self, data: tuple):
return math.floor(float(data[4]))
@property
def empty_description(self) -> str:
return "Er heeft nog niemand Didier Dinks gestolen."
@dataclass
class XPLeaderboard(Leaderboard):
title: str = field(default="XP Leaderboard")
def get_data(self) -> list[tuple]:
return stats.getAllRows()
def get_value(self, data: tuple):
return round(int(data[12]))
def format_entry_data(self, data: tuple) -> str:
entry = data[2]
return f"Level {xp.calculate_level(entry):,} | {entry:,} XP"

View File

@ -1,26 +0,0 @@
from dataclasses import dataclass, field
from data.menus.paginated import Paginated
from functions import stringFormatters
from functions.database import memes
@dataclass
class MemesList(Paginated):
title: str = field(default="Memes")
def __post_init__(self):
self.data = self.get_data()
def get_data(self) -> list[tuple]:
data = []
meme_list = memes.getAllMemes()
for meme in sorted(meme_list, key=lambda x: x[1]):
name = stringFormatters.title_case(meme[1])
fields = meme[2]
data.append((name, fields,))
return data
def format_entry(self, index: int, value: tuple) -> str:
return f"{value[0]} ({value[1]})"

View File

@ -1,67 +0,0 @@
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Union
import discord
from discord import ApplicationContext
from discord.ext import pages
from discord.ext.commands import Context
@dataclass
class Paginated(ABC):
"""Abstract class to support paginated menus easily"""
ctx: Union[ApplicationContext, Context]
title: str
data: list[tuple] = None
per_page: int = 10
colour: discord.Colour = discord.Colour.blue()
def create_embed(self, description: str) -> discord.Embed:
embed = discord.Embed(colour=self.colour)
embed.set_author(name=self.title)
embed.description = description
return embed
@abstractmethod
def format_entry(self, index: int, value: tuple) -> str:
pass
def create_pages(self, data: list[tuple]) -> list[discord.Embed]:
# Amount of entries added to this page
added = 0
page_list = []
description = ""
for i, v in enumerate(data):
s = self.format_entry(i, v)
description += s + "\n"
added += 1
# Page full, create an embed & change counters
if added == self.per_page:
embed = self.create_embed(description)
description = ""
added = 0
page_list.append(embed)
# Add final embed if necessary
if added != 0:
embed = self.create_embed(description)
page_list.append(embed)
return page_list
def create_paginator(self) -> pages.Paginator:
return pages.Paginator(pages=self.create_pages(self.data), show_disabled=False, disable_on_timeout=True, timeout=30)
async def respond(self, **kwargs) -> discord.Message:
paginator = self.create_paginator()
return await paginator.respond(self.ctx.interaction, **kwargs)
async def send(self, **kwargs) -> discord.Message:
paginator = self.create_paginator()
return await paginator.send(self.ctx, **kwargs)

View File

@ -1,10 +0,0 @@
import re
STEAM_CODE = {"pattern": "[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}", "flags": re.IGNORECASE}
def contains(text: str, pattern: dict) -> bool:
if "flags" in pattern:
return re.search(pattern["pattern"], text, pattern["flags"]) is not None
else:
return re.search(pattern["pattern"], text) is not None

View File

@ -1,29 +0,0 @@
from typing import Optional
from data import schedule
from functions import les, config
from functions.database import remind
class Reminders:
def __init__(self):
rows = remind.getAllRows()
self._nightlyUsers = [int(user[0]) for user in rows if user[1]]
self._nightlyMessages = ["Dagelijkse herinnering om Didier Nightly te doen.", "Vrees niet, Nightly-streak-liefhebber! 't Zenne kik, Didier, me ne reminder!"]
self.nightly = {"users": self._nightlyUsers, "messages": self._nightlyMessages, "weekends": True, "disabled": False}
self._les = [int(user[0]) for user in rows if user[2]]
self._lesMessages = ["Lessenrooster voor vandaag:"]
self.les = {"users": self._les, "messages": self._lesMessages, "embed": self.les_embed, "argsf": schedule.find_minor, "embed_once": False, "weekends": False, "disabled": True}
self.categories = [self.nightly, self.les]
def les_embed(self, minor: Optional[int] = None):
dt = les.find_target_date()
s = schedule.Schedule(dt, int(config.get("year")), int(config.get("semester")))
if minor is not None:
return s.create_schedule(minor_roles=[minor]).to_embed()
return s.create_schedule().to_embed()

View File

@ -1,442 +0,0 @@
from abc import ABC, abstractmethod
from dacite import from_dict
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from discord import Colour, Embed
from discord.ext import commands
from enums.platform import Platform, get_platform
from functions.timeFormatters import fromArray, intToWeekday, timeFromInt
from settings import COC_ID
import json
from typing import Dict, Optional, List, Tuple
@dataclass
class Holiday:
start_date: List[int]
end_date: List[int]
start_date_parsed: datetime = field(init=False)
end_date_parsed: datetime = field(init=False)
duration: timedelta = field(init=False)
def __post_init__(self):
self.start_date_parsed = fromArray(self.start_date)
self.end_date_parsed = fromArray(self.end_date)
self.duration = self.end_date_parsed - self.start_date_parsed
def has_passed(self, current_day: datetime) -> bool:
"""
Check if a holiday has passed already
"""
return current_day > self.end_date_parsed
@dataclass
class Course:
name: str
def __str__(self):
return self.name
@dataclass
class Location:
campus: str
building: str
room: str
def __str__(self):
return f"{self.campus} {self.building} {self.room}"
@dataclass
class Timeslot:
course: Course
start_time: int
end_time: int
canceled: bool = False
is_special: bool = False
location: Optional[Location] = None
online_link: Optional[str] = None
online_platform: Optional[Platform] = None
def __str__(self):
time_str = f"{timeFromInt(self.start_time)} - {timeFromInt(self.end_time)}"
return f"{time_str}: {self.course} {self._get_location_str()}"
def get_link_str(self) -> Optional[str]:
if self.online_link is None or self.online_platform is None:
return None
return f"[{self.online_platform.value.get('name')}]({self.online_link})"
def _get_location_str(self, offline_prefix="in", online_prefix="**online** @") -> str:
return f"{offline_prefix} **{self.location}**" if self.location is not None \
else f"{online_prefix} **{self.get_link_str()}**"
def get_special_fmt_str(self) -> Optional[str]:
if not self.canceled and not self.is_special:
return None
# This class was canceled
if self.canceled:
return f"⚠️ {self.course} van {timeFromInt(self.start_time)} gaat vandaag **niet** door."
# Something else is wrong
return f"⚠️ {self.course} gaat vandaag door van **{timeFromInt(self.start_time)}** tot " \
f"**{timeFromInt(self.end_time)}** {self._get_location_str(online_prefix='op')}"
@staticmethod
def from_slot_dict(slot_dict: Dict, course_dict: Dict, current_week: int):
"""
Construct a Timeslot from a dict of data
"""
special = False
week = find_week(str(current_week), slot_dict)
if week is not None:
# If at least one thing was changed, this slot requires extra attention
special = True
# Overwrite the normal data with the customized entries
slot_dict.update(week)
# Only happens online, not on-campus
online_only = week.get("online_only", False)
if online_only:
slot_dict.pop("location")
course = Course(course_dict["course"])
start_time = slot_dict["time"]["start"]
end_time = slot_dict["time"]["end"]
# Location can be none if a class is online-only
location = from_dict(Location, slot_dict["location"]) if "location" in slot_dict else None
# Find platform & link if this class is online
online_platform: Platform = get_platform(slot_dict.get("online", None))
# Custom online link for this day if it exists, else the general link for this platform
online_link = \
slot_dict["online_link"] if "online_link" in slot_dict else \
course_dict["online_links"][online_platform.value["rep"]] \
if online_platform is not None \
else None
return Timeslot(course=course, start_time=start_time, end_time=end_time, canceled="canceled" in slot_dict,
is_special=special, location=location, online_platform=online_platform, online_link=online_link)
@dataclass
class Schedule:
day: datetime
year: int
semester: int
targeted_weekday: bool = False
week: int = field(init=False)
schedule_dict: Dict = field(init=False)
start_date: datetime = field(init=False)
end_date: datetime = field(init=False)
semester_over: bool = False
holiday_offset: int = 0
current_holiday: Optional[Holiday] = None
weekday_str: str = field(init=False)
def __post_init__(self):
self.day = self.day.replace(hour=2, minute=0, second=0, microsecond=0)
self.schedule_dict: Dict = self.load_schedule_file()
self.start_date = fromArray(self.schedule_dict["semester_start"])
self.end_date = fromArray(self.schedule_dict["semester_end"])
self._forward_to_semester()
# Semester is over
if self.end_date < self.day:
self.semester_over = True
return
self.check_holidays()
self.week = self.get_week()
# # Store the target weekday (in case it exists) so we can ask for the next
# # friday after the holiday, for example
# target_weekday = -1 if not self.targeted_weekday else self.day.weekday()
#
# # Show schedule for after holidays
# if self.current_holiday is not None:
# # Set day to day after holiday
# self.day = self.current_holiday.end_date_parsed + timedelta(days=1)
#
# # Find the next [DAY] after the holidays
# if target_weekday != -1:
# self.day = forward_to_weekday(self.day, target_weekday)
self.weekday_str = intToWeekday(self.day.weekday())
def _forward_to_semester(self):
"""
In case the semester hasn't started yet, fast forward the current date
by a week until it's no longer necessary
"""
while self.day < self.start_date:
self.day += timedelta(weeks=1)
def check_holidays(self):
"""
Do all holiday-related stuff here to avoid multiple loops
"""
for hol_entry in self.schedule_dict.get("holidays", []):
holiday: Holiday = from_dict(Holiday, hol_entry)
# Hasn't happened yet, don't care
if holiday.start_date_parsed > self.day:
continue
# In the past: add the offset
if holiday.has_passed(self.day):
# Add 1 because Monday-Sunday is only 6 days, but should be counted as a week
self.holiday_offset += (holiday.duration.days + 1) // 7
elif holiday.start_date_parsed <= self.day <= holiday.end_date_parsed:
self.current_holiday = holiday
def load_schedule_file(self) -> Dict:
"""
Load the schedule from the JSON file
"""
with open(f"files/schedules/{self.year}{self.semester}.json", "r") as fp:
return json.load(fp)
def get_week(self) -> int:
"""
Get the current week of the semester
"""
diff: timedelta = self.day - self.start_date
# Hasn't started yet, show week 1
if diff.days < 0:
return 1
# Add +1 at the end because week 1 would be 0 as it's not over yet
# Every week would be one behind
# Also subtract all passed holidays
return (diff.days // 7) - self.holiday_offset + 1
def find_slots_for_course(self, course_dict: Dict) -> List[Timeslot]:
"""
Create time timeslots for a course
"""
slots_today = []
# First create a list of all slots of today
for slot in course_dict["slots"]:
# This slot is for a different day
if slot["time"]["day"] != self.weekday_str.lower():
continue
slots_today.append(slot)
# Create Timeslots
slots_today = list(map(lambda x: Timeslot.from_slot_dict(x, course_dict, self.week), slots_today))
return slots_today
def create_schedule(self, minor_roles: Optional[List[int]] = None):
"""
Create the schedule for the current week
"""
if self.current_holiday is not None:
return HolidayEmbed(self)
slots: List[List[Timeslot]] = [self.find_slots_for_course(course) for course in self.schedule_dict["schedule"]]
minor_slots = {}
# Find minor slots
for minor in self.schedule_dict["minors"]:
# Customized schedule
if minor_roles is not None and minor["role"] not in minor_roles:
continue
m_slots = []
for course in minor["schedule"]:
# Go over every course
m_slots.append(self.find_slots_for_course(course))
# Flatten list
m_slots = [item for sublist in m_slots for item in sublist]
# Sort by timestamp
m_slots.sort(key=lambda x: x.start_time)
minor_slots[minor["name"]] = m_slots
slots_flattened = [item for sublist in slots for item in sublist]
# Sort by timestamp
slots_flattened.sort(key=lambda x: x.start_time)
not_canceled = list(filter(lambda x: not x.canceled, slots_flattened))
# All classes are canceled
if not not_canceled:
return NoClassEmbed(self, slots_flattened)
return ScheduleEmbed(self, slots_flattened, not_canceled, minor_slots)
@dataclass
class LesEmbed(ABC):
"""
Abstract base class for Les embeds
"""
schedule: Schedule
def get_author(self) -> str:
level = "Bachelor" if self.schedule.year < 4 else "Master"
year = self.schedule.year if self.schedule.year < 4 else self.schedule.year - 3
suffix = "ste" if self.schedule.year == 1 else "de"
return f"Lessenrooster voor {year}{suffix} {level}"
def get_title(self) -> str:
date = self.schedule.day.strftime("%d/%m/%Y")
return f"{self.schedule.weekday_str} {date}"
def get_footer(self) -> str:
return f"Semester {self.schedule.semester} | Lesweek {self.schedule.week}"
def get_extras(self) -> str:
return ""
def add_minors(self, embed: Embed):
pass
def get_online_links(self) -> str:
return ""
@abstractmethod
def get_description(self) -> str:
pass
def to_embed(self) -> Embed:
embed = Embed(title=self.get_title(), colour=Colour.blue())
embed.set_author(name=self.get_author())
embed.set_footer(text=self.get_footer())
embed.description = self.get_description()
# Add links if there are any
links = self.get_online_links()
if links:
embed.add_field(name="Online links", value=links, inline=False)
self.add_minors(embed)
# Add extras if there are any
extras = self.get_extras()
if extras:
embed.add_field(name="Extra", value=extras, inline=False)
return embed
@dataclass
class HolidayEmbed(LesEmbed):
"""
Class for a Les embed sent during holidays
"""
def get_description(self) -> str:
date = self.schedule.current_holiday.end_date_parsed.strftime("%d/%m/%Y")
return f"Het is momenteel **vakantie** tot en met **{date}**."
@dataclass
class NoClassEmbed(LesEmbed):
"""
Class for a Les embed when all classes are canceled or there are none at all
"""
slots: List[Timeslot]
def get_description(self) -> str:
return "Geen les"
def get_extras(self) -> str:
canceled = list(filter(lambda x: x.canceled, self.slots))
if not canceled:
return ""
return "\n".join(list(entry.get_special_fmt_str() for entry in canceled))
@dataclass
class ScheduleEmbed(LesEmbed):
"""
Class for a successful schedule
"""
slots: List[Timeslot]
slots_not_canceled: List[Timeslot]
minor_slots: Dict[str, List[Timeslot]]
def get_description(self) -> str:
return "\n".join(list(f"{entry}" for entry in self.slots_not_canceled))
def add_minors(self, embed: Embed):
for minor, slots in self.minor_slots.items():
if not slots:
continue
not_canceled = list(filter(lambda x: not x.canceled, slots))
info = "\n".join(list(str(entry) for entry in not_canceled))
special = list(filter(lambda x: x.is_special or x.canceled, slots))
# Add extra info about this minor
if special:
info += "\n" + "\n".join(list(entry.get_special_fmt_str() for entry in special))
embed.add_field(name=f"Minor {minor}", value=info, inline=False)
def get_extras(self) -> str:
special = list(filter(lambda x: x.is_special or x.canceled, self.slots))
if not special:
return ""
return "\n".join(list(entry.get_special_fmt_str() for entry in special))
def get_online_links(self) -> str:
has_link = list(filter(lambda x: x.online_link is not None, self.slots))
if not has_link:
return ""
# Store in a set first to remove duplicates
entries = list(set(f"{entry.course}: **{entry.get_link_str()}**" for entry in has_link))
return "\n".join(list(sorted(entries)))
def find_minor(client: commands.Bot, userid: int) -> Tuple[Optional[int]]:
guild = client.get_guild(COC_ID)
user = guild.get_member(userid)
minors_ids = [891744461405687808, 891744390035415111]
for role in user.roles:
if role.id in minors_ids:
return role.id,
return None,
def find_week(week: str, slot_dict: Dict) -> Optional[Dict]:
"""Find a week in a slot_dict"""
if "weeks" not in slot_dict:
return None
# Split all entries based on commas
# to allow grouping weeks together
for w in slot_dict["weeks"]:
weeks = w.split(",")
if week in weeks:
return slot_dict["weeks"][w]
# Week was not in any of the lists
return None

View File

@ -1,39 +0,0 @@
from attr import dataclass
from data import regexes
import discord
from enum import Enum
class Action(Enum):
"""
Enum to indicate what action was performed by the user
"""
Edit = 0
Remove = 1
@dataclass
class Snipe:
"""
Dataclass to store Snipe info
"""
user: int
channel: int
guild: int
action: Action
old: str
new: str = None
def should_snipe(message: discord.Message) -> bool:
"""
Check if a message should be sniped or not
This could be a oneliner but that makes it unreadable
"""
if message.guild is None:
return False
if message.author.bot:
return False
return not regexes.contains(message.content, regexes.STEAM_CODE)

View File

@ -0,0 +1,2 @@
WORDLE_GUESS_COUNT = 6
WORDLE_WORD_LENGTH = 5

View File

@ -0,0 +1,45 @@
import datetime
from datetime import date
from typing import Optional
from sqlalchemy import extract, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from database.crud import users
from database.schemas import Birthday, User
__all__ = ["add_birthday", "get_birthday_for_user", "get_birthdays_on_day"]
async def add_birthday(session: AsyncSession, user_id: int, birthday: date):
"""Add a user's birthday into the database
If already present, overwrites the existing one
"""
user = await users.get_or_add_user(session, user_id, options=[selectinload(User.birthday)])
if user.birthday is not None:
bd = user.birthday
await session.refresh(bd)
bd.birthday = birthday
else:
bd = Birthday(user_id=user_id, birthday=birthday)
session.add(bd)
await session.commit()
async def get_birthday_for_user(session: AsyncSession, user_id: int) -> Optional[Birthday]:
"""Find a user's birthday"""
statement = select(Birthday).where(Birthday.user_id == user_id)
return (await session.execute(statement)).scalar_one_or_none()
async def get_birthdays_on_day(session: AsyncSession, day: datetime.date) -> list[Birthday]:
"""Get all birthdays that happen on a given day"""
days = extract("day", Birthday.birthday)
months = extract("month", Birthday.birthday)
statement = select(Birthday).where((days == day.day) & (months == day.month))
return list((await session.execute(statement)).scalars().all())

View File

@ -0,0 +1,73 @@
from typing import Optional
import sqlalchemy.exc
from sqlalchemy import delete, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from database.crud.users import get_or_add_user
from database.exceptions import (
DuplicateInsertException,
Forbidden,
ForbiddenNameException,
NoResultFoundException,
)
from database.schemas import Bookmark
__all__ = ["create_bookmark", "get_bookmarks", "get_bookmark_by_name"]
async def create_bookmark(session: AsyncSession, user_id: int, label: str, jump_url: str) -> Bookmark:
"""Create a new bookmark to a message"""
# Don't allow bookmarks with names of subcommands
if label.lower() in ["create", "delete", "ls", "list", "Rm", "search"]:
raise ForbiddenNameException
await get_or_add_user(session, user_id)
try:
bookmark = Bookmark(label=label, jump_url=jump_url, user_id=user_id)
session.add(bookmark)
await session.commit()
await session.refresh(bookmark)
except sqlalchemy.exc.IntegrityError as e:
raise DuplicateInsertException from e
return bookmark
async def delete_bookmark_by_id(session: AsyncSession, user_id: int, bookmark_id: int):
"""Find a bookmark by its id & delete it
This fails if you don't own this bookmark
"""
select_statement = select(Bookmark).where(Bookmark.bookmark_id == bookmark_id)
bookmark = (await session.execute(select_statement)).scalar_one_or_none()
# No bookmark with this id
if bookmark is None:
raise NoResultFoundException
# You don't own this bookmark
if bookmark.user_id != user_id:
raise Forbidden
# Delete it
delete_statement = delete(Bookmark).where(Bookmark.bookmark_id == bookmark_id)
await session.execute(delete_statement)
await session.commit()
async def get_bookmarks(session: AsyncSession, user_id: int, *, query: Optional[str] = None) -> list[Bookmark]:
"""Get all a user's bookmarks"""
statement = select(Bookmark).where(Bookmark.user_id == user_id)
if query is not None:
statement = statement.where(Bookmark.label.ilike(f"%{query.lower()}%"))
return (await session.execute(statement)).scalars().all()
async def get_bookmark_by_name(session: AsyncSession, user_id: int, query: str) -> Optional[Bookmark]:
"""Try to find a bookmark by its name"""
statement = select(Bookmark).where(Bookmark.user_id == user_id).where(func.lower(Bookmark.label) == query.lower())
return (await session.execute(statement)).scalar_one_or_none()

View File

@ -0,0 +1,41 @@
from datetime import datetime
from typing import Optional, Union
from discord import app_commands
from discord.ext import commands
from sqlalchemy.ext.asyncio import AsyncSession
from database.crud.users import get_or_add_user
from database.schemas import CommandStats
__all__ = ["register_command_invocation"]
CommandT = Union[commands.Command, app_commands.Command, app_commands.ContextMenu]
async def register_command_invocation(
session: AsyncSession, ctx: commands.Context, command: Optional[CommandT], timestamp: datetime
):
"""Create an entry for a command invocation"""
if command is None:
return
await get_or_add_user(session, ctx.author.id)
# Check the type of invocation
context_menu = isinstance(command, app_commands.ContextMenu)
# (This is a bit uglier but it accounts for hybrid commands)
slash = isinstance(command, app_commands.Command) or (ctx.interaction is not None and not context_menu)
stats = CommandStats(
command=command.qualified_name.lower(),
timestamp=timestamp,
user_id=ctx.author.id,
slash=slash,
context_menu=context_menu,
)
session.add(stats)
await session.commit()

View File

@ -0,0 +1,137 @@
from datetime import date
from typing import Union
from sqlalchemy.ext.asyncio import AsyncSession
from database.crud import users
from database.exceptions import currency as exceptions
from database.schemas import Bank, NightlyData
from database.utils.math.currency import (
capacity_upgrade_price,
interest_upgrade_price,
rob_upgrade_price,
)
__all__ = [
"add_dinks",
"claim_nightly",
"get_bank",
"get_nightly_data",
"invest",
"upgrade_capacity",
"upgrade_interest",
"upgrade_rob",
"NIGHTLY_AMOUNT",
]
NIGHTLY_AMOUNT = 420
async def get_bank(session: AsyncSession, user_id: int) -> Bank:
"""Get a user's bank info"""
user = await users.get_or_add_user(session, user_id)
return user.bank
async def get_nightly_data(session: AsyncSession, user_id: int) -> NightlyData:
"""Get a user's nightly info"""
user = await users.get_or_add_user(session, user_id)
return user.nightly_data
async def invest(session: AsyncSession, user_id: int, amount: Union[str, int]) -> int:
"""Invest all your Dinks"""
bank = await get_bank(session, user_id)
if amount == "all":
amount = bank.dinks
# Don't allow investing more dinks than you own
amount = min(bank.dinks, int(amount))
bank.dinks -= amount
bank.invested += amount
session.add(bank)
await session.commit()
return amount
async def add_dinks(session: AsyncSession, user_id: int, amount: int):
"""Increase the Dinks counter for a user"""
bank = await get_bank(session, user_id)
bank.dinks += amount
session.add(bank)
await session.commit()
async def claim_nightly(session: AsyncSession, user_id: int):
"""Claim daily Dinks"""
nightly_data = await get_nightly_data(session, user_id)
now = date.today()
if nightly_data.last_nightly is not None and nightly_data.last_nightly == now:
raise exceptions.DoubleNightly
bank = await get_bank(session, user_id)
bank.dinks += NIGHTLY_AMOUNT
nightly_data.last_nightly = now
session.add(bank)
session.add(nightly_data)
await session.commit()
async def upgrade_capacity(session: AsyncSession, user_id: int) -> int:
"""Upgrade capacity level"""
bank = await get_bank(session, user_id)
upgrade_price = capacity_upgrade_price(bank.capacity_level)
# Can't afford this upgrade
if upgrade_price > bank.dinks:
raise exceptions.NotEnoughDinks
bank.dinks -= upgrade_price
bank.capacity_level += 1
session.add(bank)
await session.commit()
return bank.capacity_level
async def upgrade_interest(session: AsyncSession, user_id: int) -> int:
"""Upgrade interest level"""
bank = await get_bank(session, user_id)
upgrade_price = interest_upgrade_price(bank.interest_level)
# Can't afford this upgrade
if upgrade_price > bank.dinks:
raise exceptions.NotEnoughDinks
bank.dinks -= upgrade_price
bank.interest_level += 1
session.add(bank)
await session.commit()
return bank.interest_level
async def upgrade_rob(session: AsyncSession, user_id: int) -> int:
"""Upgrade rob level"""
bank = await get_bank(session, user_id)
upgrade_price = rob_upgrade_price(bank.rob_level)
# Can't afford this upgrade
if upgrade_price > bank.dinks:
raise exceptions.NotEnoughDinks
bank.dinks -= upgrade_price
bank.rob_level += 1
session.add(bank)
await session.commit()
return bank.rob_level

View File

@ -0,0 +1,105 @@
from typing import Optional
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from database.exceptions.constraints import DuplicateInsertException
from database.exceptions.not_found import NoResultFoundException
from database.schemas import CustomCommand, CustomCommandAlias
__all__ = [
"clean_name",
"create_alias",
"create_command",
"edit_command",
"get_all_commands",
"get_command",
"get_command_by_alias",
"get_command_by_name",
]
def clean_name(name: str) -> str:
"""Convert a name to lowercase & remove spaces to allow easier matching"""
return name.lower().replace(" ", "")
async def create_command(session: AsyncSession, name: str, response: str) -> CustomCommand:
"""Create a new custom command"""
# Check if command or alias already exists
command = await get_command(session, name)
if command is not None:
raise DuplicateInsertException
command = CustomCommand(name=name, indexed_name=clean_name(name), response=response)
session.add(command)
await session.commit()
return command
async def create_alias(session: AsyncSession, command: str, alias: str) -> CustomCommandAlias:
"""Create an alias for a command"""
# Check if the command exists
command_instance = await get_command(session, command)
if command_instance is None:
raise NoResultFoundException
# Check if the alias exists (either as an alias or as a name)
if await get_command(session, alias) is not None:
raise DuplicateInsertException
alias_instance = CustomCommandAlias(alias=alias, indexed_alias=clean_name(alias), command=command_instance)
session.add(alias_instance)
await session.commit()
return alias_instance
async def get_all_commands(session: AsyncSession) -> list[CustomCommand]:
"""Get a list of all commands"""
statement = select(CustomCommand)
return (await session.execute(statement)).scalars().all()
async def get_command(session: AsyncSession, message: str) -> Optional[CustomCommand]:
"""Try to get a command out of a message"""
# Search lowercase & without spaces
message = clean_name(message)
return (await get_command_by_name(session, message)) or (await get_command_by_alias(session, message))
async def get_command_by_name(session: AsyncSession, message: str) -> Optional[CustomCommand]:
"""Try to get a command by its name"""
statement = select(CustomCommand).where(CustomCommand.indexed_name == message)
return (await session.execute(statement)).scalar_one_or_none()
async def get_command_by_alias(session: AsyncSession, message: str) -> Optional[CustomCommand]:
"""Try to get a command by its alias"""
statement = select(CustomCommandAlias).where(CustomCommandAlias.indexed_alias == message)
alias = (await session.execute(statement)).scalar_one_or_none()
if alias is None:
return None
return alias.command
async def edit_command(
session: AsyncSession, original_name: str, new_name: Optional[str] = None, new_response: Optional[str] = None
) -> CustomCommand:
"""Edit an existing command"""
# Check if the command exists
command = await get_command(session, original_name)
if command is None:
raise NoResultFoundException
if new_name is not None:
command.name = new_name
if new_response is not None:
command.response = new_response
session.add(command)
await session.commit()
return command

View File

@ -0,0 +1,26 @@
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from database.exceptions.not_found import NoResultFoundException
from database.schemas import DadJoke
__all__ = ["add_dad_joke", "get_random_dad_joke"]
async def add_dad_joke(session: AsyncSession, joke: str) -> DadJoke:
"""Add a new dad joke to the database"""
dad_joke = DadJoke(joke=joke)
session.add(dad_joke)
await session.commit()
return dad_joke
async def get_random_dad_joke(session: AsyncSession) -> DadJoke: # pragma: no cover # randomness is untestable
"""Return a random database entry"""
statement = select(DadJoke).order_by(func.random())
row = (await session.execute(statement)).first()
if row is None:
raise NoResultFoundException
return row[0]

View File

@ -0,0 +1,38 @@
from typing import Optional
from zoneinfo import ZoneInfo
from dateutil.parser import parse
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from database.schemas import Deadline, UforaCourse
__all__ = ["add_deadline", "get_deadlines"]
async def add_deadline(session: AsyncSession, course_id: int, name: str, date_str: str):
"""Add a new deadline"""
date_dt = parse(date_str, dayfirst=True).replace(tzinfo=ZoneInfo("Europe/Brussels"))
# If we only have a day, assume it's the end of the day
if date_dt.hour == date_dt.minute == date_dt.second == 0:
date_dt.replace(hour=23, minute=59, second=59)
deadline = Deadline(course_id=course_id, name=name, deadline=date_dt)
session.add(deadline)
await session.commit()
async def get_deadlines(session: AsyncSession, *, course: Optional[UforaCourse] = None) -> list[Deadline]:
"""Get a list of all deadlines that are currently known
This includes deadlines that have passed already
"""
statement = select(Deadline)
if course is not None:
statement = statement.where(Deadline.course_id == course.course_id)
statement = statement.options(selectinload(Deadline.course))
return (await session.execute(statement)).scalars().all()

View File

@ -0,0 +1,12 @@
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from database.schemas import EasterEgg
__all__ = ["get_all_easter_eggs"]
async def get_all_easter_eggs(session: AsyncSession) -> list[EasterEgg]:
"""Return a list of all easter eggs"""
statement = select(EasterEgg)
return (await session.execute(statement)).scalars().all()

View File

@ -0,0 +1,51 @@
from typing import Optional
import sqlalchemy.exc
from sqlalchemy import delete, select
from sqlalchemy.ext.asyncio import AsyncSession
from database.exceptions import (
DuplicateInsertException,
Forbidden,
NoResultFoundException,
)
from database.schemas import GitHubLink
__all__ = ["add_github_link", "delete_github_link_by_id", "get_github_links"]
async def add_github_link(session: AsyncSession, user_id: int, url: str) -> GitHubLink:
"""Add a new GitHub link into the database"""
try:
gh_link = GitHubLink(user_id=user_id, url=url)
session.add(gh_link)
await session.commit()
await session.refresh(gh_link)
except sqlalchemy.exc.IntegrityError:
raise DuplicateInsertException
return gh_link
async def delete_github_link_by_id(session: AsyncSession, user_id: int, link_id: int):
"""Remove an existing link from the database
You can only remove links owned by you
"""
select_statement = select(GitHubLink).where(GitHubLink.github_link_id == link_id)
gh_link: Optional[GitHubLink] = (await session.execute(select_statement)).scalar_one_or_none()
if gh_link is None:
raise NoResultFoundException
if gh_link.user_id != user_id:
raise Forbidden
delete_statement = delete(GitHubLink).where(GitHubLink.github_link_id == gh_link.github_link_id)
await session.execute(delete_statement)
await session.commit()
async def get_github_links(session: AsyncSession, user_id: int) -> list[GitHubLink]:
"""Get a user's GitHub links"""
statement = select(GitHubLink).where(GitHubLink.user_id == user_id)
return (await session.execute(statement)).scalars().all()

View File

@ -0,0 +1,45 @@
from typing import Optional
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from database.exceptions import NoResultFoundException
from database.schemas import Link
__all__ = ["add_link", "edit_link", "get_all_links", "get_link_by_name"]
async def get_all_links(session: AsyncSession) -> list[Link]:
"""Get a list of all links"""
statement = select(Link)
return (await session.execute(statement)).scalars().all()
async def add_link(session: AsyncSession, name: str, url: str) -> Link:
"""Add a new link into the database"""
if name.islower():
name = name.capitalize()
instance = Link(name=name, url=url)
session.add(instance)
await session.commit()
return instance
async def get_link_by_name(session: AsyncSession, name: str) -> Optional[Link]:
"""Get a link by its name"""
statement = select(Link).where(func.lower(Link.name) == name.lower())
return (await session.execute(statement)).scalar_one_or_none()
async def edit_link(session: AsyncSession, name: str, new_url: str):
"""Edit an existing link"""
link: Optional[Link] = await get_link_by_name(session, name)
if link is None:
raise NoResultFoundException
link.url = new_url
session.add(link)
await session.commit()

View File

@ -0,0 +1,35 @@
from typing import Optional
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from database.schemas import MemeTemplate
__all__ = ["add_meme", "get_all_memes", "get_meme_by_name"]
async def add_meme(session: AsyncSession, name: str, template_id: int, field_count: int) -> Optional[MemeTemplate]:
"""Add a new meme into the database"""
try:
meme = MemeTemplate(name=name, template_id=template_id, field_count=field_count)
session.add(meme)
await session.commit()
return meme
except IntegrityError:
return None
async def get_all_memes(session: AsyncSession) -> list[MemeTemplate]:
"""Get a list of all memes"""
statement = select(MemeTemplate)
return (await session.execute(statement)).scalars().all()
async def get_meme_by_name(session: AsyncSession, query: str) -> Optional[MemeTemplate]:
"""Try to find a meme by its name
Returns the first match found by PSQL
"""
statement = select(MemeTemplate).where(MemeTemplate.name.ilike(f"%{query.lower()}%"))
return (await session.execute(statement)).scalar()

View File

@ -0,0 +1,42 @@
from typing import Optional
from sqlalchemy import delete, select
from sqlalchemy.ext.asyncio import AsyncSession
from database.crud.users import get_or_add_user
from database.enums import ReminderCategory
from database.schemas import Reminder
__all__ = ["get_all_reminders_for_category", "toggle_reminder"]
async def get_all_reminders_for_category(session: AsyncSession, category: ReminderCategory) -> list[Reminder]:
"""Get a list of all Reminders for a given category"""
statement = select(Reminder).where(Reminder.category == category)
return (await session.execute(statement)).scalars().all()
async def toggle_reminder(session: AsyncSession, user_id: int, category: ReminderCategory) -> bool:
"""Switch a category on/off
Returns the new value for the category
"""
await get_or_add_user(session, user_id)
select_statement = select(Reminder).where(Reminder.user_id == user_id).where(Reminder.category == category)
reminder: Optional[Reminder] = (await session.execute(select_statement)).scalar_one_or_none()
# No reminder set yet
if reminder is None:
reminder = Reminder(user_id=user_id, category=category)
session.add(reminder)
await session.commit()
return True
# Reminder found -> delete it
delete_statement = delete(Reminder).where(Reminder.reminder_id == reminder.reminder_id)
await session.execute(delete_statement)
await session.commit()
return False

View File

@ -0,0 +1,32 @@
import datetime
from typing import Optional
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from database.enums import TaskType
from database.schemas import Task
from database.utils.datetime import LOCAL_TIMEZONE
__all__ = ["get_task_by_enum", "set_last_task_execution_time"]
async def get_task_by_enum(session: AsyncSession, task: TaskType) -> Optional[Task]:
"""Get a task by its enum value, if it exists
Returns None if the task does not exist
"""
statement = select(Task).where(Task.task == task)
return (await session.execute(statement)).scalar_one_or_none()
async def set_last_task_execution_time(session: AsyncSession, task: TaskType):
"""Set the last time a specific task was executed"""
_task = await get_task_by_enum(session, task)
if _task is None:
_task = Task(task=task)
_task.previous_run = datetime.datetime.now(tz=LOCAL_TIMEZONE)
session.add(_task)
await session.commit()

View File

@ -0,0 +1,38 @@
import datetime
from sqlalchemy import delete, select
from sqlalchemy.ext.asyncio import AsyncSession
from database.schemas import UforaAnnouncement, UforaCourse
__all__ = ["create_new_announcement", "get_courses_with_announcements", "remove_old_announcements"]
async def get_courses_with_announcements(session: AsyncSession) -> list[UforaCourse]:
"""Get all courses where announcements are enabled"""
statement = select(UforaCourse).where(UforaCourse.log_announcements)
return (await session.execute(statement)).scalars().all()
async def create_new_announcement(
session: AsyncSession, announcement_id: int, course: UforaCourse, publication_date: datetime.datetime
) -> UforaAnnouncement:
"""Add a new announcement to the database"""
new_announcement = UforaAnnouncement(
announcement_id=announcement_id, course=course, publication_date=publication_date
)
session.add(new_announcement)
await session.commit()
return new_announcement
async def remove_old_announcements(session: AsyncSession):
"""Delete all announcements that are > 8 days old
The RSS feed only goes back 7 days, so all of these old announcements never have to
be checked again when checking if an announcement is fresh or not.
"""
limit = datetime.datetime.utcnow() - datetime.timedelta(days=8)
statement = delete(UforaAnnouncement).where(UforaAnnouncement.publication_date < limit)
await session.execute(statement)
await session.commit()

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