61 Commits

31 changed files with 382 additions and 407 deletions
+40
View File
@@ -0,0 +1,40 @@
---
name: Bug Report
about: Create a report to help us improve
title: '[BUG] '
labels: ['Kind/Bug']
assignees: ''
---
<!--- Provide a general summary of the issue in the Title above -->
## Description
<!--- Provide a more detailed introduction to the issue itself, and why you consider it to be a bug -->
## Expected Behavior
<!--- Tell us what should happen -->
## Current Behavior
<!--- Tell us what happens instead -->
## Possible Fix
<!--- Not obligatory, but suggest a fix or reason for the bug -->
## Steps to Reproduce
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
<!--- reproduce this bug. Include code to reproduce, if relevant -->
1.
2.
3.
4.
## Context
<!--- How has this bug affected you? What were you trying to accomplish? -->
## Your Environment
<!--- Include as many relevant details about the environment you experienced the bug in -->
* Version used:
* Environment name and version (e.g., Python 3.12):
* Server type and version:
* Operating System and version:
* Link to your project or a reproduction:
+27
View File
@@ -0,0 +1,27 @@
---
name: Feature Request
about: Suggest an idea for this project
title: '[FEAT] '
labels: ['Kind/Feature']
assignees: ''
---
<!--- Provide a general summary of the issue in the Title above -->
## Detailed Description
<!--- Provide a detailed description of the change or addition you are proposing -->
## Context
<!--- Why is this change important to you? How would you use it? -->
<!--- How can it benefit other users? -->
## Possible Implementation
<!--- Not obligatory, but suggest an idea for implementing addition or change -->
## Your Environment
<!--- Include as many relevant details about the environment you experienced the bug in -->
* Version used:
* Environment name and version (e.g., Python 3.12):
* Server type and version:
* Operating System and version:
* Link to your project or a reproduction:
+31
View File
@@ -0,0 +1,31 @@
<!--- Provide a general summary of your changes in the Title above -->
## Description
<!--- Describe your changes in detail -->
## Motivation and Context
<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->
## How Has This Been Tested?
<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, the tests you ran to -->
<!--- see how your change affects other areas of the code, etc. -->
## Screenshots (if appropriate)
## Types of Changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Refactor (non-breaking change that neither fixes nor adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Documentation change (non-breaking change that only affected the documentation)
- [ ] Other change (non-breaking change with clear description in other sections)
## Checklist
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [ ] My code follows the code style of this project.
- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
+32
View File
@@ -0,0 +1,32 @@
name: build-docker
on:
push:
branches:
- main
- staging
- dev
jobs:
docker:
runs-on: ubuntu-24.04
steps:
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
registry: ${{ vars.DOCKER_REGISTRY_FQDN }}
username: ${{ vars.DOCKER_REGISTRY_USERNAME }}
password: ${{ secrets.DOCKER_REGISTRY_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver: docker
- name: Build and push
uses: docker/build-push-action@v6
with:
push: true
tags: ${{ vars.DOCKER_REGISTRY_FQDN }}/${{ vars.DOCKER_IMAGE_USERNAME }}/${{ vars.DOCKER_IMAGE_NAME }}:${{ gitea.REF_NAME }}
secrets: |
GIT_AUTH_TOKEN=${{ secrets.DOCKER_REGISTRY_TOKEN }}
+30
View File
@@ -0,0 +1,30 @@
name: build-docker-release
on:
release:
types:
- published
jobs:
docker:
runs-on: ubuntu-24.04
steps:
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
registry: ${{ vars.DOCKER_REGISTRY_FQDN }}
username: ${{ vars.DOCKER_REGISTRY_USERNAME }}
password: ${{ secrets.DOCKER_REGISTRY_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver: docker
- name: Build and push
uses: docker/build-push-action@v6
with:
push: true
tags: ${{ vars.DOCKER_REGISTRY_FQDN }}/${{ vars.DOCKER_IMAGE_USERNAME }}/${{ vars.DOCKER_IMAGE_NAME }}:${{ gitea.REF_NAME }},${{ vars.DOCKER_REGISTRY_FQDN }}/${{ vars.DOCKER_IMAGE_USERNAME }}/${{ vars.DOCKER_IMAGE_NAME }}:latest
secrets: |
GIT_AUTH_TOKEN=${{ secrets.DOCKER_REGISTRY_TOKEN }}
+28
View File
@@ -0,0 +1,28 @@
name: build-docker
on:
pull_request:
branches:
- dev
jobs:
docker:
runs-on: ubuntu-24.04
steps:
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
registry: ${{ vars.DOCKER_REGISTRY_FQDN }}
username: ${{ vars.DOCKER_REGISTRY_USERNAME }}
password: ${{ secrets.DOCKER_REGISTRY_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver: docker
- name: Build
uses: docker/build-push-action@v6
with:
push: false
tags: ${{ vars.DOCKER_REGISTRY_FQDN }}/${{ vars.DOCKER_IMAGE_USERNAME }}/${{ vars.DOCKER_IMAGE_NAME }}:${{ gitea.REF_NAME }}
+17 -1
View File
@@ -6,6 +6,11 @@
<a href="https://git.end-play.xyz/HoloUA/Discord/src/branch/master/LICENSE"><img alt="License: GPL" src="https://img.shields.io/badge/License-GPL-blue"></a>
<a href="https://git.end-play.xyz/HoloUA/Discord"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
</p>
<p align="center">
<a href="https://status.end-play.xyz/endpoints/holoua_discord"><img alt="Health" src="https://status.end-play.xyz/api/v1/endpoints/holoua_discord/health/badge.svg"></a>
<a href="https://status.end-play.xyz/endpoints/holoua_discord"><img alt="Uptime 24h" src="https://status.end-play.xyz/api/v1/endpoints/holoua_discord/uptimes/24h/badge.svg"></a>
<a href="https://status.end-play.xyz/endpoints/holoua_discord"><img alt="Uptime 7d" src="https://status.end-play.xyz/api/v1/endpoints/holoua_discord/uptimes/7d/badge.svg"></a>
</p>
## Installation from release
@@ -105,6 +110,17 @@ contain data that shouldn't normally be logged.
As an experiment, Docker deployment option has been added.
### Starting the bot
1. Install MongoDB using the [official installation manual](https://www.mongodb.com/docs/manual/installation)
2. Download
the [configuration example file](https://git.end-play.xyz/HoloUA/Discord/src/branch/main/config_example.json) and
store it somewhere you would like your bot to access it
3. Complete the [configuration](#Configuration) step for this file
4. `docker run -d -v /path/to/config.json:/app/config.json --name holoua-discord git.end-play.xyz/holoua/holoua-discord`
## Building Docker images [Experimental]
### Building the image
1. `git clone https://git.end-play.xyz/HoloUA/Discord.git`
@@ -118,4 +134,4 @@ As an experiment, Docker deployment option has been added.
the [configuration example file](https://git.end-play.xyz/HoloUA/Discord/src/branch/main/config_example.json) and
store it somewhere you would like your bot to access it
3. Complete the [configuration](#Configuration) step for this file
4. `docker run -d -v /path/to/config.json:/app/config.json holoua-discord`
4. `docker run -d -v /path/to/config.json:/app/config.json holoua-discord`
-3
View File
@@ -1,3 +0,0 @@
from .holo_cache import HoloCache
from .holo_cache_memcached import HoloCacheMemcached
from .holo_cache_redis import HoloCacheRedis
-44
View File
@@ -1,44 +0,0 @@
from abc import ABC, abstractmethod
from typing import Any, Dict
import pymemcache
import redis
class HoloCache(ABC):
client: pymemcache.Client | redis.Redis
@classmethod
@abstractmethod
def from_config(cls, engine_config: Dict[str, Any]) -> Any:
pass
@abstractmethod
def get_json(self, key: str) -> Any | None:
# TODO This method must also carry out ObjectId conversion!
pass
@abstractmethod
def get_string(self, key: str) -> str | None:
pass
@abstractmethod
def get_object(self, key: str) -> Any | None:
pass
@abstractmethod
def set_json(self, key: str, value: Any) -> None:
# TODO This method must also carry out ObjectId conversion!
pass
@abstractmethod
def set_string(self, key: str, value: str) -> None:
pass
@abstractmethod
def set_object(self, key: str, value: Any) -> None:
pass
@abstractmethod
def delete(self, key: str) -> None:
pass
-89
View File
@@ -1,89 +0,0 @@
import logging
from logging import Logger
from typing import Dict, Any
from pymemcache import Client
from modules.cache_utils import string_to_json, json_to_string
from . import HoloCache
logger: Logger = logging.getLogger(__name__)
class HoloCacheMemcached(HoloCache):
client: Client
def __init__(self, client: Client):
self.client = client
logger.info("Initialized Memcached for caching")
@classmethod
def from_config(cls, engine_config: Dict[str, Any]) -> "HoloCacheMemcached":
if "uri" not in engine_config:
raise KeyError(
"Cache configuration is invalid. Please check if all keys are set (engine: memcached)"
)
return cls(Client(engine_config["uri"], default_noreply=True))
def get_json(self, key: str) -> Any | None:
try:
result: Any | None = self.client.get(key, None)
logger.debug(
"Got json cache key '%s'%s",
key,
"" if result is not None else " (not found)",
)
except Exception as exc:
logger.error("Could not get json cache key '%s' due to: %s", key, exc)
return None
return None if result is None else string_to_json(result)
def get_string(self, key: str) -> str | None:
try:
result: str | None = self.client.get(key, None)
logger.debug(
"Got string cache key '%s'%s",
key,
"" if result is not None else " (not found)",
)
return result
except Exception as exc:
logger.error("Could not get string cache key '%s' due to: %s", key, exc)
return None
# TODO Implement binary deserialization
def get_object(self, key: str) -> Any | None:
raise NotImplementedError()
def set_json(self, key: str, value: Any) -> None:
try:
self.client.set(key, json_to_string(value))
logger.debug("Set json cache key '%s'", key)
except Exception as exc:
logger.error("Could not set json cache key '%s' due to: %s", key, exc)
return None
def set_string(self, key: str, value: str) -> None:
try:
self.client.set(key, value)
logger.debug("Set string cache key '%s'", key)
except Exception as exc:
logger.error("Could not set string cache key '%s' due to: %s", key, exc)
return None
# TODO Implement binary serialization
def set_object(self, key: str, value: Any) -> None:
raise NotImplementedError()
def delete(self, key: str) -> None:
try:
self.client.delete(key)
logger.debug("Deleted cache key '%s'", key)
except Exception as exc:
logger.error("Could not delete cache key '%s' due to: %s", key, exc)
-89
View File
@@ -1,89 +0,0 @@
import logging
from logging import Logger
from typing import Dict, Any
from redis import Redis
from classes.cache import HoloCache
from modules.cache_utils import string_to_json, json_to_string
logger: Logger = logging.getLogger(__name__)
class HoloCacheRedis(HoloCache):
client: Redis
def __init__(self, client: Redis):
self.client = client
logger.info("Initialized Redis for caching")
@classmethod
def from_config(cls, engine_config: Dict[str, Any]) -> Any:
if "uri" not in engine_config:
raise KeyError(
"Cache configuration is invalid. Please check if all keys are set (engine: memcached)"
)
return cls(Redis.from_url(engine_config["uri"]))
def get_json(self, key: str) -> Any | None:
try:
result: Any | None = self.client.get(key)
logger.debug(
"Got json cache key '%s'%s",
key,
"" if result is not None else " (not found)",
)
except Exception as exc:
logger.error("Could not get json cache key '%s' due to: %s", key, exc)
return None
return None if result is None else string_to_json(result)
def get_string(self, key: str) -> str | None:
try:
result: str | None = self.client.get(key)
logger.debug(
"Got string cache key '%s'%s",
key,
"" if result is not None else " (not found)",
)
return result
except Exception as exc:
logger.error("Could not get string cache key '%s' due to: %s", key, exc)
return None
# TODO Implement binary deserialization
def get_object(self, key: str) -> Any | None:
raise NotImplementedError()
def set_json(self, key: str, value: Any) -> None:
try:
self.client.set(key, json_to_string(value))
logger.debug("Set json cache key '%s'", key)
except Exception as exc:
logger.error("Could not set json cache key '%s' due to: %s", key, exc)
return None
def set_string(self, key: str, value: str) -> None:
try:
self.client.set(key, value)
logger.debug("Set string cache key '%s'", key)
except Exception as exc:
logger.error("Could not set string cache key '%s' due to: %s", key, exc)
return None
# TODO Implement binary serialization
def set_object(self, key: str, value: Any) -> None:
raise NotImplementedError()
def delete(self, key: str) -> None:
try:
self.client.delete(key)
logger.debug("Deleted cache key '%s'", key)
except Exception as exc:
logger.error("Could not delete cache key '%s' due to: %s", key, exc)
+47 -7
View File
@@ -1,22 +1,62 @@
import logging
from datetime import datetime, timedelta, timezone
from logging import Logger
from typing import Literal
from aiohttp import ClientSession, ClientTimeout
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.date import DateTrigger
from libbot.cache.classes import CacheMemcached, CacheRedis
from libbot.cache.manager import create_cache_client
from libbot.pycord.classes import PycordBot
from classes.cache.holo_cache_memcached import HoloCacheMemcached
from classes.cache.holo_cache_redis import HoloCacheRedis
from modules.cache_manager import create_cache_client
logger: Logger = logging.getLogger(__name__)
class HoloBot(PycordBot):
cache: HoloCacheMemcached | HoloCacheRedis | None = None
cache: CacheMemcached | CacheRedis | None = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._schedule_jobs()
self._set_cache_engine()
def _set_cache_engine(self) -> None:
if "cache" in self.config and self.config["cache"]["type"] is not None:
self.cache = create_cache_client(self.config, self.config["cache"]["type"])
cache_type: Literal["redis", "memcached"] | None = self.config["cache"]["type"]
if "cache" in self.config and cache_type is not None:
self.cache = create_cache_client(
self.config,
cache_type,
prefix=self.config["cache"][cache_type]["prefix"],
default_ttl_seconds=3600,
)
async def _send_heartbeat(self) -> None:
if self.config["monitoring"]["enabled"] is False:
return
async with ClientSession() as session:
await session.post(
self.config["monitoring"]["url"],
headers={
"Authorization": f"Bearer {self.config['monitoring']['token']}"
},
timeout=ClientTimeout(3.0),
)
def _schedule_jobs(self) -> None:
if self.scheduler is None:
return
self.scheduler.add_job(
self._send_heartbeat,
trigger=DateTrigger(
datetime.now(tz=timezone.utc) + timedelta(seconds=5),
timezone=timezone.utc,
),
)
self.scheduler.add_job(
self._send_heartbeat,
trigger=CronTrigger.from_crontab("* * * * *"),
)
+25 -61
View File
@@ -1,16 +1,15 @@
import logging
from logging import Logger
from typing import Any, Dict
from typing import Any
from bson import ObjectId
from discord import User, Member
from discord import Member, User
from libbot.cache.classes import Cache
from libbot.utils import config_get
from pymongo.results import InsertOneResult
from typing_extensions import deprecated
from classes.cache import HoloCache
from errors import UserNotFoundError
from modules.database import col_warnings, col_users
from modules.database import col_users
logger: Logger = logging.getLogger(__name__)
@@ -34,25 +33,25 @@ class HoloUser:
cls,
user: User | Member,
allow_creation: bool = True,
cache: HoloCache | None = None,
cache: Cache | None = None,
) -> "HoloUser":
"""Get an object that has a proper binding between Discord ID and database
### Args:
* `user` (User | Member): Object from which an ID can be extracted
* `allow_creation` (bool, optional): Whether to allow creation of a new user record if none found. Defaults to True.
* `cache` (HoloCache | None, optional): Cache engine to get the cache from
* `cache` (Cache, optional): Cache engine to get the cache from
### Raises:
* `UserNotFoundError`: User with such ID does not seem to exist in database
"""
if cache is not None:
cached_entry: Dict[str, Any] | None = cache.get_json(f"user_{user.id}")
cached_entry: dict[str, Any] | None = cache.get_json(f"user_{user.id}")
if cached_entry is not None:
return cls(**cached_entry)
db_entry: Dict[str, Any] | None = await col_users.find_one({"id": user.id})
db_entry: dict[str, Any] | None = await col_users.find_one({"id": user.id})
if db_entry is None:
if not allow_creation:
@@ -71,48 +70,15 @@ class HoloUser:
@classmethod
async def from_id(cls, user_id: int) -> "HoloUser":
return NotImplemented
raise NotImplementedError()
# TODO Deprecate and remove warnings
@deprecated("Warnings are deprecated")
async def get_warnings(self) -> int:
"""Get number of warnings user has
### Returns:
* `int`: Number of warnings
"""
warns: Dict[str, Any] | None = await col_warnings.find_one({"id": self.id})
return 0 if warns is None else warns["warns"]
# TODO Deprecate and remove warnings
@deprecated("Warnings are deprecated")
async def warn(self, count: int = 1, reason: str = "Reason not provided") -> None:
"""Warn and add count to warns number
### Args:
* `count` (int, optional): Count of warnings to be added. Defaults to 1.
* `reason` (int, optional): Count of warnings to be added. Defaults to 1.
"""
warns: Dict[str, Any] | None = await col_warnings.find_one({"id": self.id})
if warns is not None:
await col_warnings.update_one(
{"_id": self._id},
{"$set": {"warns": warns["warns"] + count}},
)
else:
await col_warnings.insert_one(document={"id": self.id, "warns": count})
logger.info("User %s was warned %s times due to: %s", self.id, count, reason)
async def _set(self, key: str, value: Any, cache: HoloCache | None = None) -> None:
async def _set(self, key: str, value: Any, cache: Cache | None = None) -> None:
"""Set attribute data and save it into the database
### Args:
* `key` (str): Attribute to be changed
* `value` (Any): Value to set
* `cache` (HoloCache | None, optional): Cache engine to write the update into
* `cache` (Cache, optional): Cache engine to write the update into
"""
if not hasattr(self, key):
raise AttributeError()
@@ -127,12 +93,12 @@ class HoloUser:
logger.info("Set attribute '%s' of user %s to '%s'", key, self.id, value)
async def _remove(self, key: str, cache: HoloCache | None = None) -> None:
async def _remove(self, key: str, cache: Cache | None = None) -> None:
"""Remove attribute data and save it into the database
### Args:
* `key` (str): Attribute to be removed
* `cache` (HoloCache | None, optional): Cache engine to write the update into
* `cache` (Cache, optional): Cache engine to write the update into
"""
if not hasattr(self, key):
raise AttributeError()
@@ -152,25 +118,25 @@ class HoloUser:
def _get_cache_key(self) -> str:
return f"user_{self.id}"
def _update_cache(self, cache: HoloCache | None = None) -> None:
def _update_cache(self, cache: Cache | None = None) -> None:
if cache is None:
return
user_dict: Dict[str, Any] = self._to_dict()
user_dict: dict[str, Any] = self._to_dict()
if user_dict is not None:
cache.set_json(self._get_cache_key(), user_dict)
else:
self._delete_cache(cache)
def _delete_cache(self, cache: HoloCache | None = None) -> None:
def _delete_cache(self, cache: Cache | None = None) -> None:
if cache is None:
return
cache.delete(self._get_cache_key())
@staticmethod
def get_defaults(user_id: int | None = None) -> Dict[str, Any]:
def get_defaults(user_id: int | None = None) -> dict[str, Any]:
return {
"id": user_id,
"custom_role": None,
@@ -184,7 +150,7 @@ class HoloUser:
return HoloUser.get_defaults()[key]
def _to_dict(self) -> Dict[str, Any]:
def _to_dict(self) -> dict[str, Any]:
return {
"_id": self._id,
"id": self.id,
@@ -193,26 +159,24 @@ class HoloUser:
}
async def set_custom_channel(
self, channel_id: int, cache: HoloCache | None = None
self, channel_id: int, cache: Cache | None = None
) -> None:
await self._set("custom_channel", channel_id, cache=cache)
async def set_custom_role(
self, role_id: int, cache: HoloCache | None = None
) -> None:
async def set_custom_role(self, role_id: int, cache: Cache | None = None) -> None:
await self._set("custom_role", role_id, cache=cache)
async def remove_custom_channel(self, cache: HoloCache | None = None) -> None:
async def remove_custom_channel(self, cache: Cache | None = None) -> None:
await self._remove("custom_channel", cache=cache)
async def remove_custom_role(self, cache: HoloCache | None = None) -> None:
async def remove_custom_role(self, cache: Cache | None = None) -> None:
await self._remove("custom_role", cache=cache)
async def purge(self, cache: HoloCache | None = None) -> None:
"""Completely remove user data from database. Will not remove transactions logs and warnings.
async def purge(self, cache: Cache | None = None) -> None:
"""Completely remove user data from database. Only removes the user record from users collection.
### Args:
* `cache` (HoloCache | None, optional): Cache engine to write the update into
* `cache` (Cache, optional): Cache engine to write the update into
"""
await col_users.delete_one({"_id": self._id})
self._delete_cache(cache)
+2 -2
View File
@@ -5,11 +5,11 @@ from logging import Logger
from discord import (
ApplicationContext,
Embed,
Role,
TextChannel,
User,
option,
slash_command,
Role,
TextChannel,
)
from discord import utils as ds_utils
from discord.ext import commands
+3 -3
View File
@@ -1,6 +1,6 @@
import logging
from logging import Logger
from typing import Dict, List, Any
from typing import Any
from discord import Cog, Message
from discord.ext import commands
@@ -24,7 +24,7 @@ class Analytics(commands.Cog):
and (message.author.system is False)
):
# Handle stickers
stickers: List[Dict[str, Any]] = []
stickers: list[dict[str, Any]] = []
for sticker in message.stickers:
stickers.append(
@@ -37,7 +37,7 @@ class Analytics(commands.Cog):
)
# Handle attachments
attachments: List[Dict[str, Any]] = []
attachments: list[dict[str, Any]] = []
for attachment in message.attachments:
attachments.append(
+3 -3
View File
@@ -1,8 +1,8 @@
import logging
from logging import Logger
from typing import Any, Dict
from typing import Any
from discord import ApplicationContext, Embed, option, TextChannel, Role
from discord import ApplicationContext, Embed, Role, TextChannel, option
from discord import utils as ds_utils
from discord.abc import GuildChannel
from discord.commands import SlashCommandGroup
@@ -114,7 +114,7 @@ class CustomChannels(commands.Cog):
)
)
bots: Dict[str, Any] = await config_get("bots")
bots: dict[str, Any] = await config_get("bots")
for bot in bots:
role: Role | None = ds_utils.get(ctx.user.guild.roles, id=bots[bot]["role"])
+4 -3
View File
@@ -2,10 +2,10 @@ import logging
from logging import Logger
from os import makedirs
from pathlib import Path
from typing import List, Dict, Any
from typing import Any
from uuid import uuid4
from discord import ApplicationContext, Embed, File, option, Role, TextChannel
from discord import ApplicationContext, Embed, File, Role, TextChannel, option
from discord import utils as ds_utils
from discord.commands import SlashCommandGroup
from discord.ext import commands
@@ -82,7 +82,7 @@ class Data(commands.Cog):
uuid: str = str(uuid4())
if kind == "Користувачі":
users: List[Dict[str, Any]] = []
users: list[dict[str, Any]] = []
for member in ctx.guild.members:
users.append(
@@ -96,6 +96,7 @@ class Data(commands.Cog):
# Temporary file must be written synchronously,
# otherwise it will not be there when ctx.respond() is be called
# TODO Find a way to give this file to Pycord without FS operations
json_write(users, Path(f"tmp/{uuid}"))
await ctx.respond(file=File(Path(f"tmp/{uuid}"), filename="users.json"))
+1 -1
View File
@@ -1,7 +1,7 @@
import logging
from logging import Logger
from discord import Member, Message, TextChannel, MessageType
from discord import Member, Message, MessageType, TextChannel
from discord import utils as ds_utils
from discord.ext import commands
from libbot.utils import config_get
+9 -2
View File
@@ -25,12 +25,19 @@
"cache": {
"type": null,
"memcached": {
"uri": "127.0.0.1:11211"
"uri": "127.0.0.1:11211",
"prefix": null
},
"redis": {
"uri": "redis://127.0.0.1:6379/0"
"uri": "redis://127.0.0.1:6379/0",
"prefix": null
}
},
"monitoring": {
"enabled": false,
"url": "https://status.example.org/api/v1/endpoints/my-group_my-service/external?success=true",
"token": "12345678"
},
"logging": {
"size": 512,
"location": "logs"
+2
View File
@@ -1 +1,3 @@
from .colors import Color
__all__: list[str] = ["Color"]
+2
View File
@@ -1 +1,3 @@
from .user import UserNotFoundError
__all__: list[str] = ["UserNotFoundError"]
+1 -1
View File
@@ -5,7 +5,7 @@ from argparse import ArgumentParser
from logging import Logger
from pathlib import Path
from discord import LoginFailure, Intents
from discord import Intents, LoginFailure
from libbot.utils import config_get
from classes.holo_bot import HoloBot
+1 -1
View File
@@ -1,7 +1,7 @@
import logging
from logging import Logger
from libbot.utils import config_get, config_set, config_delete
from libbot.utils import config_delete, config_get, config_set
from mongodb_migrations.base import BaseMigration
logger: Logger = logging.getLogger(__name__)
+1 -1
View File
@@ -1,7 +1,7 @@
import logging
from logging import Logger
from libbot.utils import config_set, config_delete
from libbot.utils import config_delete, config_set
from mongodb_migrations.base import BaseMigration
logger: Logger = logging.getLogger(__name__)
+31
View File
@@ -0,0 +1,31 @@
import logging
from logging import Logger
from libbot.utils import config_delete, config_set
from mongodb_migrations.base import BaseMigration
logger: Logger = logging.getLogger(__name__)
class Migration(BaseMigration):
def upgrade(self):
try:
config_set("prefix", None, "cache", "memcached")
config_set("prefix", None, "cache", "redis")
except Exception as exc:
logger.error(
"Could not upgrade the config during migration '%s' due to: %s",
__name__,
exc,
)
def downgrade(self):
try:
config_delete("prefix", "cache", "redis")
config_delete("prefix", "cache", "memcached")
except Exception as exc:
logger.error(
"Could not downgrade the config during migration '%s' due to: %s",
__name__,
exc,
)
+33
View File
@@ -0,0 +1,33 @@
import logging
from logging import Logger
from libbot.utils import config_delete, config_set
from mongodb_migrations.base import BaseMigration
logger: Logger = logging.getLogger(__name__)
class Migration(BaseMigration):
def upgrade(self):
try:
config_set("enabled", False, "monitoring")
config_set("url", "", "monitoring")
config_set("token", "", "monitoring")
except Exception as exc:
logger.error(
"Could not upgrade the config during migration '%s' due to: %s",
__name__,
exc,
)
def downgrade(self):
try:
config_delete("monitoring", "enabled")
config_delete("monitoring", "url")
config_delete("monitoring", "token")
except Exception as exc:
logger.error(
"Could not downgrade the config during migration '%s' due to: %s",
__name__,
exc,
)
-29
View File
@@ -1,29 +0,0 @@
from typing import Dict, Any, Literal
from classes.cache.holo_cache_memcached import HoloCacheMemcached
from classes.cache.holo_cache_redis import HoloCacheRedis
def create_cache_client(
config: Dict[str, Any],
engine: Literal["memcached", "redis"] | None = None,
) -> HoloCacheMemcached | HoloCacheRedis:
if engine not in ["memcached", "redis"] or engine is None:
raise KeyError(
f"Incorrect cache engine provided. Expected 'memcached' or 'redis', got '{engine}'"
)
if "cache" not in config or engine not in config["cache"]:
raise KeyError(
f"Cache configuration is invalid. Please check if all keys are set (engine: '{engine}')"
)
match engine:
case "memcached":
return HoloCacheMemcached.from_config(config["cache"][engine])
case "redis":
return HoloCacheRedis.from_config(config["cache"][engine])
case _:
raise KeyError(
f"Cache implementation for the engine '{engine}' is not present."
)
-25
View File
@@ -1,25 +0,0 @@
from copy import deepcopy
from typing import Any
from bson import ObjectId
from ujson import dumps, loads
def json_to_string(json_object: Any) -> str:
json_object_copy: Any = deepcopy(json_object)
if isinstance(json_object_copy, dict) and "_id" in json_object_copy:
json_object_copy["_id"] = str(json_object_copy["_id"])
return dumps(
json_object_copy, ensure_ascii=False, indent=0, escape_forward_slashes=False
)
def string_to_json(json_string: str) -> Any:
json_object: Any = loads(json_string)
if "_id" in json_object:
json_object["_id"] = ObjectId(json_object["_id"])
return json_object
+3 -15
View File
@@ -1,12 +1,9 @@
from typing import Dict, Any
from typing import Any
from async_pymongo import AsyncClient, AsyncCollection, AsyncDatabase
from libbot.utils import config_get
from pymongo import MongoClient
from pymongo.synchronous.collection import Collection
from pymongo.synchronous.database import Database
db_config: Dict[str, Any] = config_get("database")
db_config: dict[str, Any] = config_get("database")
con_string: str = (
"mongodb://{0}:{1}/{2}".format(
@@ -23,21 +20,12 @@ con_string: str = (
)
db_client: AsyncClient = AsyncClient(con_string)
db_client_sync: MongoClient = MongoClient(con_string)
# Async declarations per default
db: AsyncDatabase = db_client.get_database(name=db_config["name"])
col_users: AsyncCollection = db.get_collection("users")
col_warnings: AsyncCollection = db.get_collection("warnings")
col_analytics: AsyncCollection = db.get_collection("analytics")
# Sync declarations as a fallback
sync_db: Database = db_client_sync.get_database(name=db_config["name"])
sync_col_users: Collection = sync_db.get_collection("users")
sync_col_warnings: Collection = sync_db.get_collection("warnings")
sync_col_analytics: Collection = sync_db.get_collection("analytics")
# Update indexes
sync_col_users.create_index(["id"], unique=True)
db.dispatch.get_collection("users").create_index("id", unique=True)
+9 -9
View File
@@ -1,13 +1,13 @@
# Waifu pics related dependencies (not listed directly by waifupics)
aiohttp>=3.10.0
requests>=2.32.2
aiohttp>=3.10.0,<4.0.0
requests>=2.32.2,<3.0.0
aiofiles~=24.1.0
apscheduler>=3.10.0
async_pymongo==0.1.11
libbot[speed,pycord]==4.0.2
aiofiles>=23.0.0,<26.0.0
apscheduler>=3.0.0,<4.0.0
async_pymongo==0.1.12
libbot[speed,pycord,cache]==4.6.1
mongodb-migrations==1.3.1
pymemcache~=4.0.0
redis~=5.2.1
ujson~=5.10.0
pymemcache>=4.0.0,<5.0.0
redis>=7.0.0,<9.0.0
ujson>=5.0.0,<6.0.0
WaifuPicsPython==0.2.0
-18
View File
@@ -1,18 +0,0 @@
{
"$jsonSchema": {
"required": [
"user_id",
"warnings"
],
"properties": {
"user_id": {
"bsonType": "long",
"description": "Discord ID of user"
},
"warnings": {
"bsonType": "int",
"description": "Number of warnings on count"
}
}
}
}