39 Commits
24 changed files with 325 additions and 47 deletions

No files matched your search

+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`
+34
View File
@@ -1,7 +1,11 @@
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
@@ -14,6 +18,7 @@ class HoloBot(PycordBot):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._schedule_jobs()
self._set_cache_engine()
def _set_cache_engine(self) -> None:
@@ -26,3 +31,32 @@ class HoloBot(PycordBot):
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("* * * * *"),
)
+17 -19
View File
@@ -1,9 +1,9 @@
import logging
from logging import Logger
from typing import Any, Dict, Optional
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
@@ -33,7 +33,7 @@ class HoloUser:
cls,
user: User | Member,
allow_creation: bool = True,
cache: Optional[Cache] = None,
cache: Cache | None = None,
) -> "HoloUser":
"""Get an object that has a proper binding between Discord ID and database
@@ -46,12 +46,12 @@ class HoloUser:
* `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:
@@ -72,7 +72,7 @@ class HoloUser:
async def from_id(cls, user_id: int) -> "HoloUser":
raise NotImplementedError()
async def _set(self, key: str, value: Any, cache: Optional[Cache] = 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:
@@ -93,7 +93,7 @@ class HoloUser:
logger.info("Set attribute '%s' of user %s to '%s'", key, self.id, value)
async def _remove(self, key: str, cache: Optional[Cache] = None) -> None:
async def _remove(self, key: str, cache: Cache | None = None) -> None:
"""Remove attribute data and save it into the database
### Args:
@@ -118,25 +118,25 @@ class HoloUser:
def _get_cache_key(self) -> str:
return f"user_{self.id}"
def _update_cache(self, cache: Optional[Cache] = 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: Optional[Cache] = 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,
@@ -150,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,
@@ -159,22 +159,20 @@ class HoloUser:
}
async def set_custom_channel(
self, channel_id: int, cache: Optional[Cache] = 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: Optional[Cache] = 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: Optional[Cache] = 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: Optional[Cache] = 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: Optional[Cache] = None) -> None:
async def purge(self, cache: Cache | None = None) -> None:
"""Completely remove user data from database. Only removes the user record from users collection.
### Args:
+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"])
+3 -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(
+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
+5
View File
@@ -33,6 +33,11 @@
"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__)
+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__)
+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,
)
+2 -2
View File
@@ -1,9 +1,9 @@
from typing import Dict, Any
from typing import Any
from async_pymongo import AsyncClient, AsyncCollection, AsyncDatabase
from libbot.utils import config_get
db_config: Dict[str, Any] = config_get("database")
db_config: dict[str, Any] = config_get("database")
con_string: str = (
"mongodb://{0}:{1}/{2}".format(
+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,cache]==4.4.0
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~=6.2.0
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