diff --git a/.gitignore b/.gitignore index b6e4761..e92f24c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# CUSTOM +config.toml + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -20,7 +23,6 @@ parts/ sdist/ var/ wheels/ -pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg @@ -50,6 +52,7 @@ coverage.xml *.py,cover .hypothesis/ .pytest_cache/ +cover/ # Translations *.mo @@ -72,6 +75,7 @@ instance/ docs/_build/ # PyBuilder +.pybuilder/ target/ # Jupyter Notebook @@ -82,7 +86,9 @@ profile_default/ ipython_config.py # pyenv -.python-version +# 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. @@ -91,7 +97,22 @@ ipython_config.py # install all needed dependencies. #Pipfile.lock -# PEP 582; used by e.g. github.com/David-OConnor/pyflow +# 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 @@ -127,3 +148,16 @@ dmypy.json # Pyre type checker .pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/README.md b/README.md index 2ab7f67..0957a7e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,77 @@ # Watcher -Discord bot for inactive roles +An experimental [Hikari](https://www.hikari-py.dev) Discord bot for assigning active and inactive roles based on member activity + +[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/B0B1AUB66) + +## Prerequisites +A Unix-based operating system is used for the following commands\ +[WSL](https://docs.microsoft.com/en-us/windows/wsl) can be used to run Linux on Windows, but is not required to run the bot +## Requirements +[Git](https://git-scm.com/downloads)\ +[Python](https://www.python.org) 3.10+\ +[Poetry](https://python-poetry.org/docs/master) +## Installing +Clone this repository +``` +git clone https://github.com/Myned/Watcher.git +``` +Go to the project folder +``` +cd Watcher +``` +Create a virtual environment and install dependencies +``` +poetry install +``` +## Usage +Go to the project folder +``` +cd Watcher +``` +Run with optimizations +``` +poetry run python -OO run.py +``` +## Setup +Run to create `config.toml`\ +The file will automatically generate if it does not exist +``` +client = 0 # bot application id +token = "" # bot token +activity = "you" # bot status +db = "watcher.db" # sqlite3 db filepath +guild = 0 # guild id to watch +active = 0 # active role id +inactive = 0 # inactive role id +duration = 0 # time in seconds before considered inactive +``` +## Updating +Go to the project folder +``` +cd Watcher +``` +Pull changes from the repository +``` +git pull +``` +Update the virtual environment +``` +poetry update +``` +## Uninstalling +Remove the project folder +``` +rm -rf Watcher +``` +## Contributing +1. [Fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo) this repository on GitHub +2. Make changes to the code +3. Format the code with [Black](https://black.readthedocs.io/en/stable) inside the project folder + ``` + poetry run python black . + ``` +4. [Commit](https://github.com/git-guides/git-commit) the changes to the fork +5. Create a [pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request) from the fork +## Credits +[hikari](https://github.com/hikari-py/hikari)\ +[hikari-lightbulb](https://github.com/tandemdude/hikari-lightbulb) diff --git a/config.py b/config.py new file mode 100644 index 0000000..22c0673 --- /dev/null +++ b/config.py @@ -0,0 +1,27 @@ +import toml +import hikari + + +# Hikari activity type +# https://www.hikari-py.dev/hikari/presences.html#hikari.presences.ActivityType +ACTIVITY = hikari.ActivityType.WATCHING +# Default bot configuration +CONFIG = """\ +client = 0 # bot application id +token = "" # bot token +activity = "you" # bot status +db = "watcher.db" # sqlite3 db filepath +guild = 0 # guild id to watch +active = 0 # active role id +inactive = 0 # inactive role id +duration = 0 # time in seconds before considered inactive +""" + +# Load or create config.toml +try: + config = toml.load("config.toml") +except FileNotFoundError: + with open("config.toml", "w") as f: + f.write(CONFIG) + print("config.toml created with default values. Restart when modified") + exit() diff --git a/poetry.toml b/poetry.toml new file mode 100644 index 0000000..ab1033b --- /dev/null +++ b/poetry.toml @@ -0,0 +1,2 @@ +[virtualenvs] +in-project = true diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..253ad53 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,24 @@ +[tool.poetry] +name = "watcher" +version = "0.1.0" +description = "Watcher Discord Bot" +authors = ["Myned "] +license = "MIT" + +[tool.poetry.dependencies] +python = "~3.10" +toml = "*" +uvloop = "*" +sqlitedict = "*" +hikari = {extras = ["speedups"], version = "*"} +hikari-lightbulb = "*" + +[tool.poetry.dev-dependencies] +black = "*" + +[tool.black] +line-length = 120 + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/run.py b/run.py new file mode 100644 index 0000000..23d6bb1 --- /dev/null +++ b/run.py @@ -0,0 +1,32 @@ +import os +import hikari +import lightbulb +from lightbulb.ext import tasks + +import config as c + + +# Unix optimizations +# https://github.com/hikari-py/hikari#uvloop +if os.name != "nt": + import uvloop + + uvloop.install() + + +bot = lightbulb.BotApp(token=c.config["token"], intents=hikari.Intents.ALL_GUILDS) + + +# Listener for global exceptions +@bot.listen(hikari.ExceptionEvent) +async def on_error(event): + exception = event.exception.__cause__ or event.exception + + await (await bot.rest.fetch_application()).owner.send(f"```❗ {type(exception).__name__}: {exception}```") + + raise event.exception + + +tasks.load(bot) +bot.load_extensions_from("tasks") +bot.run(activity=hikari.Activity(name=c.config["activity"], type=c.ACTIVITY) if c.config["activity"] else None) diff --git a/tasks/activity.py b/tasks/activity.py new file mode 100644 index 0000000..92bbcee --- /dev/null +++ b/tasks/activity.py @@ -0,0 +1,52 @@ +import datetime as dt +import sqlitedict +import hikari +import lightbulb +from lightbulb.ext import tasks + +import config as c + + +plugin = lightbulb.Plugin("activity") +db = sqlitedict.SqliteDict(c.config["db"], tablename=str(c.config["guild"]), autocommit=True) + + +# Check every minute if inactive +@tasks.task(s=60) +async def check_activity(): + for author_id, timestamp in db.items(): + if dt.datetime.now(dt.timezone.utc) - timestamp >= dt.timedelta(seconds=c.config["duration"]): + member = plugin.bot.cache.get_member(c.config["guild"], author_id) + + if c.config["active"] and c.config["active"] in member.role_ids: + await member.remove_role(c.config["active"]) + if c.config["inactive"] and c.config["inactive"] not in member.role_ids: + await member.add_role(c.config["inactive"]) + + +# Listener for bot ready +@plugin.listener(hikari.StartedEvent) +async def on_ready(event): + check_activity.start() + + +# Listener for guild messages +@plugin.listener(hikari.GuildMessageCreateEvent) +async def on_message(event): + if event.is_bot or event.guild_id != c.config["guild"]: + return + + db[event.author_id] = event.message.timestamp # or datetime.datetime.utcnow() + + if c.config["active"] and c.config["active"] not in event.member.role_ids: + await event.member.add_role(c.config["active"]) + if c.config["inactive"] and c.config["inactive"] in event.member.role_ids: + await event.member.remove_role(c.config["inactive"]) + + +def load(bot): + bot.add_plugin(plugin) + + +def unload(bot): + bot.remove_plugin(plugin)