82 Commits

Author SHA1 Message Date
2cbe2a07e1 Update dependency deepl to v1.22.0 (#46) 2025-04-30 21:42:36 +03:00
9f99a2d507 Update dependency deepl to v1.22.0 2025-04-30 21:26:36 +03:00
187abbbbb4 Update dependency deepl to v1.21.1 (#45) 2025-03-12 20:18:21 +02:00
ab67e610d4 Update dependency deepl to v1.21.1 2025-03-12 19:41:36 +02:00
c6f971b39e WIP: Modified i18n usage 2025-02-24 21:36:01 +01:00
fcb09303ec WIP: Wallet Cog and i18n 2025-02-23 23:41:42 +01:00
1c8365b11f Added API version 2025-02-20 22:55:08 +01:00
bf6ca24eed WIP: Guilds and Wallets 2025-02-20 22:51:01 +01:00
65b0e30c75 WIP: Transactions 2025-02-20 14:39:19 +01:00
8e2003b7df WIP: Added stubs for Guilds and fixed formatting 2025-02-18 21:20:14 +01:00
kku
3ffea8b46b Added withdrawals and deposits 2025-02-18 20:25:57 +01:00
f3bb1ff79a WIP: Wallets, added missing changes 2025-02-18 20:19:09 +01:00
8883c8eda8 WIP: Wallets 2025-02-18 08:04:02 +01:00
654034491a Added stubs for custom channel, custom role and wallet 2025-02-16 22:36:18 +01:00
222a618591 Fixed a typo and removed an old class 2025-02-16 21:07:52 +01:00
a1bfbb537a Replaced built-in caching with the one from libbot 2025-02-16 20:38:41 +01:00
e0e307e35f Merge pull request 'Update dependency libbot to v4.1.0' (#41) from renovate/libbot-4.x into dev
Reviewed-on: Hessenuk/Javelina#41
2025-02-16 20:01:44 +02:00
e0564e150c Update dependency libbot to v4.1.0 2025-02-16 19:21:12 +02:00
4b401e878b Replaced UserNotFoundException with UserNotFoundError in a docstring 2025-02-16 14:09:32 +01:00
4ad79f1445 Improved type hints and added a placeholder for guilds 2025-02-16 13:41:23 +01:00
ffcfbbfc3b Added caching, updated libbot, refactored PycordUser 2025-02-16 13:11:48 +01:00
8154394539 Merge pull request 'Update dependency pytz to v2025' (#40) from renovate/pytz-2025.x into dev
Reviewed-on: Hessenuk/Javelina#40
2025-01-31 09:55:34 +02:00
e9ac435b40 Update dependency pytz to v2025 2025-01-31 04:27:32 +02:00
a5f18e9a4e Merge pull request 'Update dependency deepl to v1.21.0' (#39) from renovate/deepl-1.x into dev
Reviewed-on: Hessenuk/Javelina#39
2025-01-16 12:29:51 +02:00
faa0537c35 Update dependency deepl to v1.21.0 2025-01-15 20:52:46 +02:00
3794ad5aae Update requirements.txt 2024-12-27 01:54:13 +02:00
f952aa8c9d Merge pull request 'Update dependency libbot to v3.3.1' (#37) from renovate/libbot-3.x into dev
Reviewed-on: Hessenuk/Javelina#37
2024-12-17 00:03:28 +02:00
42293719e4 Update dependency libbot to v3.3.1 2024-12-17 00:00:45 +02:00
7f05cd79d9 Merge pull request 'Update dependency deepl to v1.20.0' (#35) from renovate/deepl-1.x into dev
Reviewed-on: Hessenuk/Javelina#35
2024-11-30 21:15:42 +02:00
8035610111 Update dependency deepl to v1.20.0 2024-11-24 22:57:57 +02:00
145357f487 Merge pull request 'Update dependency apscheduler to ~=3.11.0' (#36) from renovate/apscheduler-3.x into dev
Reviewed-on: Hessenuk/Javelina#36
2024-11-24 22:17:57 +02:00
c9a3943bca Update dependency apscheduler to ~=3.11.0 2024-11-24 21:55:07 +02:00
2b017c02d6 Merge pull request 'Update dependency async_pymongo to v0.1.11' (#34) from renovate/async_pymongo-0.x into dev
Reviewed-on: Hessenuk/Javelina#34
2024-10-17 09:56:02 +03:00
d632201f65 Update dependency async_pymongo to v0.1.11 2024-10-16 20:14:07 +03:00
3b3f39a8f6 Merge pull request 'Update dependency async_pymongo to v0.1.10' (#33) from renovate/async_pymongo-0.x into dev
Reviewed-on: Hessenuk/Javelina#33
2024-10-15 16:25:26 +03:00
dd1ce61cd1 Update dependency async_pymongo to v0.1.10 2024-10-15 13:09:26 +03:00
a4a95a61e2 Merge pull request 'Update dependency async_pymongo to v0.1.9' (#32) from renovate/async_pymongo-0.x into dev
Reviewed-on: Hessenuk/Javelina#32
2024-10-08 16:49:41 +03:00
9b4df44564 Update dependency async_pymongo to v0.1.9 2024-10-08 16:29:37 +03:00
247c670b2e Merge pull request 'Update dependency async_pymongo to v0.1.8' (#31) from renovate/async_pymongo-0.x into dev
Reviewed-on: Hessenuk/Javelina#31
2024-09-25 22:25:38 +03:00
00d6418c88 Update dependency async_pymongo to v0.1.8 2024-09-25 17:13:25 +03:00
a559f4c319 Merge pull request 'Update dependency async_pymongo to v0.1.7' (#30) from renovate/async_pymongo-0.x into dev
Reviewed-on: Hessenuk/Javelina#30
2024-09-21 01:52:50 +03:00
91ad1baafa Update dependency async_pymongo to v0.1.7 2024-09-20 17:24:42 +03:00
8832ba89e4 Merge pull request 'Update dependency fastapi to ~=0.115.0' (#29) from renovate/fastapi-0.x into dev
Reviewed-on: Hessenuk/Javelina#29
2024-09-18 09:24:41 +03:00
7102ba5922 Update dependency fastapi to ~=0.115.0 2024-09-18 00:37:16 +03:00
c679af095d Merge pull request 'Update dependency deepl to v1.19.1' (#28) from renovate/deepl-1.x into dev
Reviewed-on: Hessenuk/Javelina#28
2024-09-18 00:01:50 +03:00
6f644b5236 Update dependency deepl to v1.19.1 2024-09-17 13:06:18 +03:00
c85140ee8b Update dependency pyrmv to v0.4.0 (#27) 2024-09-08 03:20:28 +03:00
63ac55d831 Update dependency pyrmv to v0.4.0 2024-09-08 03:05:11 +03:00
a2ebfe5867 Merge pull request 'Update dependency fastapi to ~=0.114.0' (#26) from renovate/fastapi-0.x into dev
Reviewed-on: Hessenuk/Javelina#26
2024-09-07 11:31:01 +03:00
76074a46b8 Update dependency fastapi to ~=0.114.0 2024-09-06 20:51:34 +03:00
3d1d7e2701 Merge pull request 'Update dependency fastapi to ~=0.112.0' (#25) from renovate/fastapi-0.x into dev
Reviewed-on: Hessenuk/Javelina#25
2024-08-03 00:58:30 +03:00
46bb3db995 Update dependency fastapi to ~=0.112.0 2024-08-02 09:57:19 +03:00
b6fb7e51b4 Merge pull request 'Update dependency libbot to v3.2.3' (#24) from renovate/libbot-3.x into dev
Reviewed-on: Hessenuk/Javelina#24
2024-07-10 08:12:41 +03:00
f91ed1fba4 Update dependency libbot to v3.2.3 2024-07-10 00:44:02 +03:00
dc63cbb563 Merge pull request 'Update dependency async_pymongo to v0.1.6' (#23) from renovate/async_pymongo-0.x into dev
Reviewed-on: Hessenuk/Javelina#23
2024-06-23 14:33:54 +03:00
87c3a3cea2 Update dependency async_pymongo to v0.1.6 2024-06-23 13:30:25 +03:00
80b2f47403 Merge pull request 'Update dependency async_pymongo to v0.1.5' (#22) from renovate/async_pymongo-0.x into dev
Reviewed-on: Hessenuk/Javelina#22
2024-06-02 12:57:29 +03:00
9708fd6c2f Selected async_pymongo from PyPi 2024-06-02 12:56:30 +03:00
1bc84f0fcb Update dependency async_pymongo to v0.1.5 2024-06-01 15:32:29 +03:00
beb542b834 Merge pull request 'Update dependency libbot to v3.2.2' (#21) from renovate/libbot-3.x into dev
Reviewed-on: Hessenuk/Javelina#21
2024-05-26 23:58:34 +03:00
8f599c776a Update dependency libbot to v3.2.2 2024-05-26 23:13:04 +03:00
a352da2f3e Merge pull request 'Update dependency libbot to v3.2.0' (#20) from renovate/libbot-3.x into dev
Reviewed-on: Hessenuk/Javelina#20
2024-05-26 19:40:28 +03:00
2a7f582dd8 Update dependency libbot to v3.2.1 2024-05-26 19:01:56 +03:00
1e09ea7ec6 Merge pull request 'Update dependency libbot to v3.1.0' (#19) from renovate/libbot-3.x into dev
Reviewed-on: Hessenuk/Javelina#19
2024-05-24 22:46:52 +03:00
c6e048177e Update dependency libbot to v3.1.0 2024-05-24 22:42:35 +03:00
331184a7fd Update dependency mongodb-migrations to v1.3.1 (#17) 2024-05-14 01:48:11 +03:00
00642835bd Update dependency mongodb-migrations to v1.3.1 2024-05-14 01:26:11 +03:00
bbe72f2fdf Update dependency fastapi to ~=0.111.0 (#16) 2024-05-03 11:42:55 +03:00
c7c46060e8 Update dependency fastapi to ~=0.111.0 2024-05-03 04:06:30 +03:00
da969dad58 Update dependency deepl to v1.18.0 (#15) 2024-04-26 16:54:19 +03:00
dd368733d4 Update dependency deepl to v1.18.0 2024-04-26 14:00:04 +03:00
6bfc329666 Merge pull request 'Update dependency fastapi to ~=0.110.0' (#14) from renovate/fastapi-0.x into dev
Reviewed-on: Hessenuk/Javelina#14
2024-02-25 22:31:42 +02:00
edfd739023 Update dependency fastapi to ~=0.110.0 2024-02-25 02:13:58 +02:00
e2c05f3bf6 WIP: #9 2024-02-19 23:09:00 +01:00
fe4dcc4a92 Message events initialized 2024-02-19 23:08:41 +01:00
d5691c2bbb Fixed scheduler-related issues 2024-02-19 23:07:38 +01:00
40376d2e6d Merge pull request 'Update dependency deepl to v1.17.0' (#7) from renovate/deepl-1.x into dev
Reviewed-on: Hessenuk/Javelina#7
2024-02-13 19:05:55 +02:00
507c9dc9ed Update dependency deepl to v1.17.0 2024-02-07 13:53:18 +02:00
4d178bc3f2 Merge pull request 'Intial en locale addition' (#2) from i18n into dev
Reviewed-on: Hessenuk/Javelina#2
2024-02-04 02:13:29 +02:00
Weblate
00f907c09c Translated using Weblate (German)
Currently translated at 100.0% (0 of 0 strings)

Added translation using Weblate (German)

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Profitroll <vozhd.kk@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://weblate.end-play.xyz/projects/hessenuk/javelina/de/
Translation: Hessenuk/Javelina
2024-02-04 02:12:27 +02:00
Weblate
45573c48ae Translated using Weblate (English)
Currently translated at 73.0% (38 of 52 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Translate-URL: https://weblate.end-play.xyz/projects/hessenuk/javelina/en/
Translation: Hessenuk/Javelina
2024-02-04 02:12:07 +02:00
57a5b5b4b3 Fixed uk and added en 2024-02-04 01:11:47 +01:00
36 changed files with 1155 additions and 128 deletions

View File

@@ -14,3 +14,9 @@
<img alt="Discord" src="https://img.shields.io/discord/981251696208531466">
</a>
</p>
## Starting the bot
```shell
uvicorn main:app
```

View File

@@ -1,3 +1,7 @@
from fastapi import FastAPI
app = FastAPI()
# TODO Add an integration for the contact information
app = FastAPI(
title="Javelina",
version="0.0.1",
)

40
api/extensions/guild.py Normal file
View File

@@ -0,0 +1,40 @@
import logging
from logging import Logger
from fastapi import HTTPException, status
from fastapi.responses import JSONResponse
from api.app import app
from classes import PycordGuild
from classes.errors import WalletNotFoundError, GuildNotFoundError
from classes.wallet import Wallet
logger: Logger = logging.getLogger(__name__)
@app.get("/v1/guilds/{guild_id}", response_class=JSONResponse)
async def get_guild_wallet(guild_id: int):
try:
guild: PycordGuild = await PycordGuild.from_id(guild_id, allow_creation=False)
except GuildNotFoundError as exc:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Guild not found"
) from exc
except NotImplementedError as exc:
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="Not implemented"
) from exc
return guild.to_dict(json_compatible=True)
@app.get("/v1/guilds/{guild_id}/wallets/{user_id}", response_class=JSONResponse)
async def get_guild_wallet(guild_id: int, user_id: int):
try:
wallet: Wallet = await Wallet.from_id(user_id, guild_id, allow_creation=False)
except WalletNotFoundError as exc:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Wallet not found"
) from exc
return wallet.to_dict(json_compatible=True)

23
api/extensions/user.py Normal file
View File

@@ -0,0 +1,23 @@
import logging
from logging import Logger
from fastapi import HTTPException, status
from fastapi.responses import JSONResponse
from api.app import app
from classes import PycordUser
from classes.errors import UserNotFoundError
logger: Logger = logging.getLogger(__name__)
@app.get("/v1/users/{user_id}", response_class=JSONResponse)
async def get_user(user_id: int):
try:
user: PycordUser = await PycordUser.from_id(user_id, allow_creation=False)
except UserNotFoundError as exc:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
) from exc
return user.to_dict(json_compatible=True)

View File

@@ -1,7 +1,13 @@
import logging
from logging import Logger
from pathlib import Path
from api.app import app
from fastapi.responses import FileResponse
from api.app import app
logger: Logger = logging.getLogger(__name__)
@app.get("/favicon.ico", response_class=FileResponse, include_in_schema=False)
async def favicon():

5
classes/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
from .pycord_guild import PycordGuild
from .pycord_guild_colors import PycordGuildColors
from .pycord_user import PycordUser
# from .wallet import Wallet

15
classes/custom_channel.py Normal file
View File

@@ -0,0 +1,15 @@
from dataclasses import dataclass
from datetime import datetime
from bson import ObjectId
@dataclass
class CustomChannel:
_id: ObjectId
channel_id: int
owner_id: int
allow_comments: bool
allow_reactions: bool
created: datetime
deleted: datetime | None

14
classes/custom_role.py Normal file
View File

@@ -0,0 +1,14 @@
from dataclasses import dataclass
from datetime import datetime
from bson import ObjectId
@dataclass
class CustomRole:
_id: ObjectId
role_id: int
role_color: int
owner_id: int
created: datetime
deleted: datetime | None

View File

@@ -0,0 +1 @@
from .message_events import MessageEvents

View File

@@ -0,0 +1,5 @@
from enum import Enum
class MessageEvents(Enum):
WEATHER_FORECAST = 0

View File

@@ -0,0 +1,8 @@
from .pycord_guild import GuildNotFoundError
from .pycord_user import UserNotFoundError
from .wallet import (
WalletBalanceLimitExceeded,
WalletInsufficientFunds,
WalletNotFoundError,
WalletOverdraftLimitExceeded,
)

View File

@@ -0,0 +1,7 @@
class GuildNotFoundError(Exception):
"""PycordGuild could not find guild with such an ID in the database"""
def __init__(self, guild_id: int) -> None:
self.guild_id = guild_id
super().__init__(f"Guild with id {self.guild_id} was not found")

View File

@@ -0,0 +1,7 @@
class UserNotFoundError(Exception):
"""PycordUser could not find user with such an ID in the database"""
def __init__(self, user_id: int) -> None:
self.user_id = user_id
super().__init__(f"User with id {self.user_id} was not found")

62
classes/errors/wallet.py Normal file
View File

@@ -0,0 +1,62 @@
class WalletNotFoundError(Exception):
"""Wallet could not find user with such an ID from a guild in the database"""
def __init__(self, owner_id: int, guild_id: int) -> None:
self.owner_id = owner_id
self.guild_id = guild_id
super().__init__(
f"Wallet of a user with id {self.owner_id} was not found for the guild with id {self.guild_id}"
)
class WalletInsufficientFunds(Exception):
"""Wallet's balance is not sufficient to perform the operation"""
def __init__(
self,
wallet: "Wallet",
amount: float,
) -> None:
self.wallet = wallet
self.amount = amount
super().__init__(
f"Wallet of a user with id {self.wallet.owner_id} for the guild with id {self.wallet.guild_id} does not have sufficient funds to perform the operation (balance: {self.wallet.balance}, requested: {self.amount})"
)
class WalletOverdraftLimitExceeded(Exception):
"""Wallet's overdraft limit is not sufficient to perform the operation"""
def __init__(
self,
wallet: "Wallet",
amount: float,
overdraft_limit: float,
) -> None:
self.wallet = wallet
self.amount = amount
self.overdraft_limit = overdraft_limit
super().__init__(
f"Wallet of a user with id {self.wallet.owner_id} for the guild with id {self.wallet.guild_id} does not have sufficient funds to perform the operation (balance: {self.wallet.balance}, requested: {self.amount}, overdraft limit: {self.overdraft_limit})"
)
class WalletBalanceLimitExceeded(Exception):
"""Wallet's balance limit is not high enough to perform the operation"""
def __init__(
self,
wallet: "Wallet",
amount: float,
balance_limit: float,
) -> None:
self.wallet = wallet
self.amount = amount
self.balance_limit = balance_limit
super().__init__(
f"Wallet of a user with id {self.wallet.owner_id} for the guild with id {self.wallet.guild_id} would have too much funds after the operation (balance: {self.wallet.balance}, deposited: {self.amount}, balance limit: {self.balance_limit})"
)

81
classes/pycord_bot.py Normal file
View File

@@ -0,0 +1,81 @@
import logging
from logging import Logger
from typing import Any
from aiohttp import ClientSession
from discord import User
from libbot.cache.manager import create_cache_client
from libbot.pycord.classes import PycordBot as LibPycordBot
from classes import PycordUser
logger: Logger = logging.getLogger(__name__)
# from modules.tracking.dhl import update_tracks_dhl
class PycordBot(LibPycordBot):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self._set_cache_engine()
self.client_session = ClientSession()
if self.scheduler is None:
return
# This replacement exists because of the different
# i18n formats than provided by libbot
self._ = self._modified_string_getter
# Scheduler job for DHL parcel tracking
# self.scheduler.add_job(
# update_tracks_dhl,
# trigger="cron",
# hour=self.config["modules"]["tracking"]["fetch_hours"],
# args=[self, self.client_session],
# )
def _modified_string_getter(self, key: str, *args: str, locale: str | None = None) -> Any:
"""This method exists because of the different i18n formats than provided by libbot.
It splits "-" and takes the first part of the provided locale to make complex language codes
compatible with an easy libbot approach to i18n.
"""
return self.bot_locale._(
key, *args, locale=None if locale is None else locale.split("-")[0]
)
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"])
async def find_user(self, user: int | User) -> PycordUser:
"""Find User by its ID or User object.
Args:
user (int | User): ID or User object to extract ID from
Returns:
PycordUser: User object
Raises:
UserNotFoundException: User was not found and creation was not allowed
"""
return (
await PycordUser.from_id(user, cache=self.cache)
if isinstance(user, int)
else await PycordUser.from_id(user.id, cache=self.cache)
)
async def start(self, *args: Any, **kwargs: Any) -> None:
await super().start(*args, **kwargs)
async def close(self, **kwargs) -> None:
await self.client_session.close()
if self.scheduler is not None:
self.scheduler.shutdown()
await super().close(**kwargs)

47
classes/pycord_guild.py Normal file
View File

@@ -0,0 +1,47 @@
from dataclasses import dataclass
from typing import Dict, Any, Optional
from bson import ObjectId
from libbot.cache.classes import Cache
@dataclass
class PycordGuild:
_id: ObjectId
id: int
def __init__(self) -> None:
raise NotImplementedError()
@classmethod
async def from_id(
cls, guild_id: int, allow_creation: bool = True, cache: Optional[Cache] = None
) -> "PycordGuild":
"""Find guild in database and create new record if guild does not exist.
Args:
guild_id (int): User's Discord ID
allow_creation (:obj:`bool`, optional): Create new guild record if none found in the database
cache (:obj:`Cache`, optional): Cache engine to get the cache from
Returns:
PycordGuild: User object
Raises:
GuildNotFoundError: User was not found and creation was not allowed
"""
raise NotImplementedError()
def to_dict(self, json_compatible: bool = False) -> Dict[str, Any]:
"""Convert PycordGuild object to a JSON representation.
Args:
json_compatible (bool): Whether the JSON-incompatible objects like ObjectId need to be converted
Returns:
Dict[str, Any]: JSON representation of PycordGuild
"""
return {
"_id": self._id if not json_compatible else str(self._id),
"id": self.id,
}

172
classes/pycord_user.py Normal file
View File

@@ -0,0 +1,172 @@
import logging
from dataclasses import dataclass
from logging import Logger
from typing import Any, Dict, Optional
from bson import ObjectId
from libbot.cache.classes import Cache
from pymongo.results import InsertOneResult
from classes.errors.pycord_user import UserNotFoundError
from classes.wallet import Wallet
from modules.database import col_users
logger: Logger = logging.getLogger(__name__)
@dataclass
class PycordUser:
"""Dataclass of DB entry of a user"""
__slots__ = ("_id", "id")
_id: ObjectId
id: int
@classmethod
async def from_id(
cls, user_id: int, allow_creation: bool = True, cache: Optional[Cache] = None
) -> "PycordUser":
"""Find user in database and create new record if user does not exist.
Args:
user_id (int): User's Discord ID
allow_creation (:obj:`bool`, optional): Create new user record if none found in the database
cache (:obj:`Cache`, optional): Cache engine to get the cache from
Returns:
PycordUser: User object
Raises:
UserNotFoundError: User was not found and creation was not allowed
"""
if cache is not None:
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 = await col_users.find_one({"id": user_id})
if db_entry is None:
if not allow_creation:
raise UserNotFoundError(user_id)
db_entry = PycordUser.get_defaults(user_id)
insert_result: InsertOneResult = await col_users.insert_one(db_entry)
db_entry["_id"] = insert_result.inserted_id
if cache is not None:
cache.set_json(f"user_{user_id}", db_entry)
return cls(**db_entry)
def to_dict(self, json_compatible: bool = False) -> Dict[str, Any]:
"""Convert PycordUser object to a JSON representation.
Args:
json_compatible (bool): Whether the JSON-incompatible objects like ObjectId need to be converted
Returns:
Dict[str, Any]: JSON representation of PycordUser
"""
return {
"_id": self._id if not json_compatible else str(self._id),
"id": self.id,
}
async def _set(self, key: str, value: Any, cache: Optional[Cache] = None) -> None:
"""Set attribute data and save it into the database.
Args:
key (str): Attribute to change
value (Any): Value to set
cache (:obj:`Cache`, optional): Cache engine to write the update into
"""
if not hasattr(self, key):
raise AttributeError()
setattr(self, key, value)
await col_users.update_one({"_id": self._id}, {"$set": {key: value}}, upsert=True)
self._update_cache(cache)
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:
"""Remove attribute data and save it into the database.
Args:
key (str): Attribute to remove
cache (:obj:`Cache`, optional): Cache engine to write the update into
"""
if not hasattr(self, key):
raise AttributeError()
default_value: Any = PycordUser.get_default_value(key)
setattr(self, key, default_value)
await col_users.update_one(
{"_id": self._id}, {"$set": {key: default_value}}, upsert=True
)
self._update_cache(cache)
logger.info("Removed attribute '%s' of user %s", key, self.id)
def _get_cache_key(self) -> str:
return f"user_{self.id}"
def _update_cache(self, cache: Optional[Cache] = None) -> None:
if cache is None:
return
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:
if cache is None:
return
cache.delete(self._get_cache_key())
@staticmethod
def get_defaults(user_id: Optional[int] = None) -> Dict[str, Any]:
return {
"id": user_id,
}
@staticmethod
def get_default_value(key: str) -> Any:
if key not in PycordUser.get_defaults():
raise KeyError(f"There's no default value for key '{key}' in PycordUser")
return PycordUser.get_defaults()[key]
async def purge(self, cache: Optional[Cache] = None) -> None:
"""Completely remove user data from database. Currently only removes the user record from users collection.
Args:
cache (:obj:`Cache`, optional): Cache engine to write the update into
"""
await col_users.delete_one({"_id": self._id})
self._delete_cache(cache)
async def get_wallet(self, guild_id: int) -> Wallet:
"""Get wallet of the user.
Args:
guild_id (int): Guild ID of the wallet
Returns:
Wallet: Wallet object of the user
"""
return await Wallet.from_id(self.id, guild_id)

View File

@@ -1,45 +0,0 @@
from typing import Any, Union
from aiohttp import ClientSession
from discord import User
from libbot.pycord.classes import PycordBot as LibPycordBot
from classes.pycorduser import PycordUser
from modules.tracking.dhl import update_tracks_dhl
class PycordBot(LibPycordBot):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.client_session = ClientSession()
# Scheduler job for DHL parcel tracking
self.scheduler.add_job(
update_tracks_dhl,
trigger="cron",
hour=self.config["modules"]["tracking"]["fetch_hours"],
args=[self, self.client_session],
)
async def find_user(self, user: Union[int, User]) -> PycordUser:
"""Find User by it's ID or User object.
### Args:
* user (`Union[int, User]`): ID or User object to extract ID from.
### Returns:
* `PycordUser`: User in database representation.
"""
return (
await PycordUser.find(user)
if isinstance(user, int)
else await PycordUser.find(user.id)
)
async def close(self, *args: Any, **kwargs: Any) -> None:
await self.client_session.close()
self.scheduler.shutdown()
await super().close(*args, **kwargs)

View File

@@ -1,42 +0,0 @@
import logging
from dataclasses import dataclass
from bson import ObjectId
from modules.database import col_users
logger = logging.getLogger(__name__)
@dataclass
class PycordUser:
"""Dataclass of DB entry of a user"""
__slots__ = ("_id", "id")
_id: ObjectId
id: int
@classmethod
async def find(cls, id: int):
"""Find user in database and create new record if user does not exist.
### Args:
* id (`int`): User's Discord ID
### Raises:
* `RuntimeError`: Raised when user entry after insertion could not be found.
### Returns:
* `PycordUser`: User with its database data.
"""
db_entry = await col_users.find_one({"id": id})
if db_entry is None:
inserted = await col_users.insert_one({"id": id})
db_entry = await col_users.find_one({"_id": inserted.inserted_id})
if db_entry is None:
raise RuntimeError("Could not find inserted user entry.")
return cls(**db_entry)

164
classes/wallet.py Normal file
View File

@@ -0,0 +1,164 @@
import logging
from dataclasses import dataclass
from datetime import datetime, timezone
from logging import Logger
from typing import Any, Dict, Optional
from bson import ObjectId
from pymongo.results import InsertOneResult
from classes.errors.wallet import (
WalletBalanceLimitExceeded,
WalletNotFoundError,
WalletOverdraftLimitExceeded,
WalletInsufficientFunds,
)
from modules.database import col_wallets
logger: Logger = logging.getLogger(__name__)
@dataclass
class Wallet:
_id: ObjectId
owner_id: int
guild_id: int
balance: float
is_frozen: bool
created: datetime
# TODO Write a docstring
@classmethod
async def from_id(
cls, owner_id: int, guild_id: int, allow_creation: bool = True
) -> "Wallet":
db_entry = await col_wallets.find_one({"owner_id": owner_id, "guild_id": guild_id})
if db_entry is None:
if not allow_creation:
raise WalletNotFoundError(owner_id, guild_id)
db_entry = Wallet.get_defaults(owner_id, guild_id)
insert_result: InsertOneResult = await col_wallets.insert_one(db_entry)
db_entry["_id"] = insert_result.inserted_id
return cls(**db_entry)
def to_dict(self, json_compatible: bool = False) -> Dict[str, Any]:
"""Convert Wallet object to a JSON representation.
Args:
json_compatible (bool): Whether the JSON-incompatible objects like ObjectId need to be converted
Returns:
Dict[str, Any]: JSON representation of Wallet
"""
return {
"_id": self._id if not json_compatible else str(self._id),
"owner_id": self.owner_id,
"guild_id": self.guild_id,
"balance": self.balance,
"is_frozen": self.is_frozen,
"created": self.created,
}
async def _set(self, key: str, value: Any) -> None:
if not hasattr(self, key):
raise AttributeError()
setattr(self, key, value)
await col_wallets.update_one({"_id": self._id}, {"$set": {key: value}}, upsert=True)
logger.info("Set attribute '%s' of the wallet %s to '%s'", key, str(self._id), value)
@staticmethod
def get_defaults(
owner_id: Optional[int] = None, guild_id: Optional[int] = None
) -> Dict[str, Any]:
return {
"owner_id": owner_id,
"guild_id": guild_id,
"balance": 0.0,
"is_frozen": False,
"created": datetime.now(tz=timezone.utc),
}
@staticmethod
def get_default_value(key: str) -> Any:
if key not in Wallet.get_defaults():
raise KeyError(f"There's no default value for key '{key}' in Wallet")
return Wallet.get_defaults()[key]
# TODO Write a docstring
async def freeze(self) -> None:
await self._set("is_frozen", True)
# TODO Write a docstring
async def unfreeze(self) -> None:
await self._set("is_frozen", False)
# TODO Write a docstring
async def deposit(self, amount: float, balance_limit: Optional[float] = None) -> float:
new_balance: float = round(self.balance + amount, 2)
if balance_limit is not None and new_balance > balance_limit:
raise WalletBalanceLimitExceeded(self, amount, balance_limit)
await self._set("balance", new_balance)
return new_balance
# TODO Write a docstring
async def withdraw(
self,
amount: float,
allow_overdraft: bool = False,
overdraft_limit: Optional[float] = None,
) -> float:
if amount > self.balance:
if not allow_overdraft or overdraft_limit is None:
raise WalletInsufficientFunds(self, amount)
if allow_overdraft and amount > overdraft_limit:
raise WalletOverdraftLimitExceeded(self, amount, overdraft_limit)
new_balance: float = round(self.balance - amount, 2)
await self._set("balance", new_balance)
return new_balance
async def transfer(
self,
wallet_owner_id: int,
wallet_guild_id: int,
amount: float,
balance_limit: Optional[float] = None,
allow_overdraft: bool = False,
overdraft_limit: Optional[float] = None,
) -> None:
# TODO Replace with a concrete exception
if amount < 0:
raise ValueError()
wallet: Wallet = await self.from_id(
wallet_owner_id, wallet_guild_id, allow_creation=False
)
if balance_limit is not None and amount + wallet.balance > balance_limit:
raise WalletBalanceLimitExceeded(wallet, amount, balance_limit)
if amount > self.balance:
if not allow_overdraft or overdraft_limit is None:
raise WalletInsufficientFunds(self, amount)
if allow_overdraft and amount > overdraft_limit:
raise WalletOverdraftLimitExceeded(self, amount, overdraft_limit)
# TODO Make a sanity check to revert the transaction if anything goes wrong
await self.withdraw(amount, allow_overdraft, overdraft_limit)
await wallet.deposit(amount, balance_limit)

27
cli.py Normal file
View File

@@ -0,0 +1,27 @@
import logging
from argparse import ArgumentParser
from logging import Logger
from modules.migrator import migrate_database
logger: Logger = logging.getLogger(__name__)
parser = ArgumentParser(
prog="Javelina",
description="Discord bot for community management.",
)
parser.add_argument("--migrate", action="store_true")
parser.add_argument("--only-api", action="store_true")
args = parser.parse_args()
def main():
if args.migrate:
logger.info("Performing migrations...")
migrate_database()
if __name__ == "__main__":
main()

73
cogs/wallet.py Normal file
View File

@@ -0,0 +1,73 @@
import logging
from logging import Logger
from discord import ApplicationContext, SlashCommandGroup, option, User
from discord.ext import commands
from classes.errors import WalletInsufficientFunds
from classes.pycord_bot import PycordBot
from classes.wallet import Wallet
logger: Logger = logging.getLogger(__name__)
class WalletCog(commands.Cog):
def __init__(self, client: PycordBot):
self.client: PycordBot = client
command_group: SlashCommandGroup = SlashCommandGroup("wallet", "Wallet management")
@command_group.command(
name="balance",
description="View wallet's balance",
)
@option("user", description="User whose balance to check (if not your own)", required=False)
async def command_wallet_balance(self, ctx: ApplicationContext, user: User = None) -> None:
wallet: Wallet = await Wallet.from_id(
ctx.user.id if not user else user.id, ctx.guild_id
)
await ctx.respond(
self.client._("balance_own", "messages", "wallet", locale=ctx.locale).format(
balance=wallet.balance
)
if user is None
else self.client._("balance_user", "messages", "wallet", locale=ctx.locale).format(
balance=wallet.balance, user=user.display_name
)
)
@command_group.command(
name="transfer",
description="View wallet's balance",
)
@option("user", description="Recipient")
@option("amount", description="Amount", min_value=0.01)
async def command_wallet_transfer(
self, ctx: ApplicationContext, user: User, amount: float
) -> None:
amount = round(amount, 2)
# Guild will be needed for overdraft options
# guild: PycordGuild = await PycordGuild.from_id(ctx.guild_id)
wallet: Wallet = await Wallet.from_id(ctx.user.id, ctx.guild_id)
try:
await wallet.transfer(user.id, ctx.guild_id, amount)
except WalletInsufficientFunds:
await ctx.respond(
self.client._(
"transfer_insufficient_funds", "messages", "wallet", locale=ctx.locale
).format(amount=round(abs(wallet.balance - amount), 2))
)
return
await ctx.respond(
self.client._("transfer_success", "messages", "wallet", locale=ctx.locale).format(
amount=amount, recipient=user.display_name
)
)
def setup(client: PycordBot) -> None:
client.add_cog(WalletCog(client))

View File

@@ -23,6 +23,15 @@
"port": 27017,
"name": "javelina"
},
"cache": {
"type": null,
"memcached": {
"uri": "127.0.0.1:11211"
},
"redis": {
"uri": "redis://127.0.0.1:6379/0"
}
},
"privacy": {
"api_endpoint": "https://api.javelina.eu/v1"
},

94
locale/de.json Normal file
View File

@@ -0,0 +1,94 @@
{
"actions": {
"bite": {
"name": "Вкусити",
"text": "**{user_name}** робить кусь **{target_name}**"
},
"poke": {
"name": "Тикнути",
"text": "**{user_name}** тикає в **{target_name}**"
},
"hug": {
"name": "Обійняти",
"text": "**{user_name}** обіймає **{target_name}**"
},
"kiss": {
"name": "Поцілувати",
"text": "**{user_name}** цілує **{target_name}**"
},
"lick": {
"name": "Лизнути",
"text": "**{user_name}** лиже **{target_name}**"
},
"pat": {
"name": "Погладити",
"text": "**{user_name}** гладить **{target_name}**"
},
"wave": {
"name": "Помахати",
"text": "**{user_name}** махає **{target_name}**"
},
"wink": {
"name": "Підморгнути",
"text": "**{user_name}** підморгує **{target_name}**"
}
},
"messages": {
"welcome": {
"morning": [
"{0} Добрий ранок та ласкаво просимо! {1}",
"{0} Доброго ранку та ласкаво просимо! {1}",
"{0} Вітаннячко! Ласкаво просимо! {1}",
"{0} Доброго ранку! Ласкаво просимо! {1}"
],
"midday": [
"{0} Добрий день! Ласкаво просимо! {1}",
"{0} Добридень! Ласкаво просимо! {1}",
"{0} День добрий! Ласкаво просимо! {1}",
"{0} Мої вітання! Ласкаво просимо! {1}",
"{0} Здоровенькі були! Ласкаво просимо! {1}",
"{0} Раді вітати вас! Ласкаво просимо! {1}",
"{0} Доброго здоров’ячка! Ласкаво просимо! {1}"
],
"evening": [
"{0} Добрий вечір! Ласкаво просимо! {1}",
"{0} Доброго вечора! Ласкаво просимо! {1}",
"{0} Добривечір! Ласкаво просимо! {1}",
"{0} Доброго вечора та ласкаво просимо! {1}",
"{0} Добрий вечір та ласкаво просимо! {1}"
],
"night": [
"{0} Доброї ночі! Ласкаво просимо! {1}",
"{0} Здоровенькі були! Ласкаво просимо! {1}"
],
"unknown": [
"{0} Вітаннячко! Ласкаво просимо! {1}"
]
}
},
"tracking": {
"dhl": {
"statuses": {
"delivered": "Доставлено",
"transit": "Транзит",
"pre-transit": "Пре-транзит",
"failure": "Невдача"
},
"messages": {
"DHL PAKET (parcel)": "DHL PAKET (посилка)",
"The shipment was prepared for onward transport.": "Вантаж підготовлено до подальшого транспортування.",
"Warenpost (Merchandise Shipment)": "Варенпост (відвантаження товарів)",
"The shipment has been processed in the parcel center": "Відправлення пройшло обробку в посилковому центрі",
"Unfortunately, the shipment could not be delivered today due to a strike action.": "На жаль, сьогодні вантаж не вдалося доставити через страйк.",
"Die Sendung wurde von DHL abgeholt.": "Відправлення забрала компанія DHL.",
"The shipment has been successfully delivered": "Відправлення успішно доставлено",
"The shipment could not be delivered, and the recipient has been notified": "Відправлення не вдалося доставити, одержувача було повідомлено про це",
"The shipment has been loaded onto the delivery vehicle": "Вантаж завантажено на транспортний засіб доставки",
"The shipment arrived in the region of recipient and will be transported to the delivery base in the next step.": "Відправлення прибуло в регіон одержувача і буде доставлено на базу доставки на наступному кроці",
"The shipment has been processed in the parcel center of origin": "Відправлення оброблено в центрі відправлення посилок",
"The shipment has been posted by the sender at the retail outlet": "Відправлення відправлено відправником у торговій точці",
"The instruction data for this shipment have been provided by the sender to DHL electronically": "Дані інструкцій для цього відправлення були надані DHL відправником в електронному вигляді"
}
}
}
}

100
locale/en.json Normal file
View File

@@ -0,0 +1,100 @@
{
"messages": {
"wallet": {
"balance_own": "Your balance is `{balance}`.",
"balance_user": "**{user}**'s balance is `{balance}`.",
"transfer_success": "You have transferred `{amount}` to **{recipient}**.",
"transfer_insufficient_funds": "Insufficient funds. `{amount}` more is needed for this transaction."
},
"welcome": {
"morning": [
"{0} Good morning and welcome! {1}",
"{0} Доброго ранку та ласкаво просимо! {1}",
"{0} Вітаннячко! Ласкаво просимо! {1}",
"{0} Доброго ранку! Ласкаво просимо! {1}"
],
"midday": [
"{0} Good day and welcome! {1}",
"{0} Добридень! Ласкаво просимо! {1}",
"{0} День добрий! Ласкаво просимо! {1}",
"{0} Мої вітання! Ласкаво просимо! {1}",
"{0} Здоровенькі були! Ласкаво просимо! {1}",
"{0} Раді вітати вас! Ласкаво просимо! {1}",
"{0} Доброго здоров’ячка! Ласкаво просимо! {1}"
],
"evening": [
"{0} Good evening and welcome! {1}",
"{0} Доброго вечора! Ласкаво просимо! {1}",
"{0} Добривечір! Ласкаво просимо! {1}",
"{0} Доброго вечора та ласкаво просимо! {1}",
"{0} Добрий вечір та ласкаво просимо! {1}"
],
"night": [
"{0} Good night and welcome! {1}",
"{0} Здоровенькі були! Ласкаво просимо! {1}"
],
"unknown": [
"{0} Hello and welcome! {1}"
]
}
},
"actions": {
"bite": {
"name": "Bite",
"text": "**{user_name}** bites **{target_name}**"
},
"hug": {
"name": "Hug",
"text": "**{user_name}** hugs **{target_name}**"
},
"kiss": {
"name": "Kiss",
"text": "**{user_name}** kisses **{target_name}**"
},
"lick": {
"name": "Lick",
"text": "**{user_name}** licks **{target_name}**"
},
"pat": {
"name": "Pat",
"text": "**{user_name}** pats **{target_name}**"
},
"poke": {
"name": "Poke",
"text": "**{user_name}** pokes **{target_name}**"
},
"wave": {
"name": "Wave",
"text": "**{user_name}** waves **{target_name}**"
},
"wink": {
"name": "Wink",
"text": "**{user_name}** winks **{target_name}**"
}
},
"tracking": {
"dhl": {
"statuses": {
"delivered": "Delivered",
"transit": "Transit",
"pre-transit": "Pre-transit",
"failure": "Failure"
},
"messages": {
"DHL PAKET (parcel)": "DHL PAKET (parcel)",
"The shipment was prepared for onward transport.": "The shipment was prepared for onward transport.",
"Warenpost (Merchandise Shipment)": "Warenpost (Merchandise Shipment)",
"The shipment has been processed in the parcel center": "The shipment has been processed in the parcel center",
"Unfortunately, the shipment could not be delivered today due to a strike action.": "Unfortunately, the shipment could not be delivered today due to a strike action.",
"Die Sendung wurde von DHL abgeholt.": "The shipment has been picked up by DHL.",
"The shipment has been successfully delivered": "The shipment has been successfully delivered",
"The shipment could not be delivered, and the recipient has been notified": "The shipment could not be delivered, and the recipient has been notified",
"The shipment has been loaded onto the delivery vehicle": "The shipment has been loaded onto the delivery vehicle",
"The shipment arrived in the region of recipient and will be transported to the delivery base in the next step.": "The shipment arrived in the region of recipient and will be transported to the delivery base in the next step.",
"The shipment has been processed in the parcel center of origin": "The shipment has been processed in the parcel center of origin",
"The shipment has been posted by the sender at the retail outlet": "The shipment has been posted by the sender at the retail outlet",
"The instruction data for this shipment have been provided by the sender to DHL electronically": "The instruction data for this shipment have been provided by the sender to DHL electronically"
}
}
}
}

View File

@@ -1,5 +1,11 @@
{
"messages": {
"wallet": {
"balance_own": "Ваш баланс складає `{balance}`.",
"balance_user": "Баланс **{user}** складає `{balance}`.",
"transfer_success": "Ви перевели `{amount}` на рахунок **{recipient}**.",
"transfer_insufficient_funds": "Недостатньо коштів. Потрібно ще `{amount}` для цієї транзакції."
},
"welcome": {
"morning": [
"{0} Добрий ранок та ласкаво просимо! {1}",
@@ -38,7 +44,7 @@
"text": "**{user_name}** робить кусь **{target_name}**"
},
"hug": {
"name": "",
"name": "Обійняти",
"text": "**{user_name}** обіймає **{target_name}**"
},
"kiss": {

26
main.py
View File

@@ -1,23 +1,31 @@
import asyncio
import contextlib
import logging
from logging import Logger
from os import getpid
from libbot import sync
from libbot.utils import config_get
from classes.pycordbot import PycordBot
# Import required for uvicorn
from api.app import app # noqa
from classes.pycord_bot import PycordBot
from modules.extensions_loader import dynamic_import_from_src
from modules.scheduler import scheduler
# Import required for uvicorn
from api.app import app
logging.basicConfig(
level=logging.DEBUG if sync.config_get("debug") else logging.INFO,
level=logging.DEBUG if config_get("debug") else logging.INFO,
format="%(name)s.%(funcName)s | %(levelname)s | %(message)s",
datefmt="[%X]",
)
logger = logging.getLogger(__name__)
logger: Logger = logging.getLogger(__name__)
# Try to import the module that improves performance
# and ignore errors when module is not installed
with contextlib.suppress(ImportError):
import uvloop
uvloop.install()
async def main():
@@ -26,10 +34,10 @@ async def main():
bot.load_extension("cogs")
# Import API modules
dynamic_import_from_src("api.extensions", star_import=True)
dynamic_import_from_src("api/extensions", star_import=True)
try:
await bot.start(sync.config_get("bot_token", "bot"))
await bot.start(config_get("bot_token", "bot"))
except KeyboardInterrupt:
logger.warning("Forcefully shutting down with PID %s...", getpid())
await bot.close()

View File

@@ -1,4 +1,3 @@
from modules.migrator import migrate_database
migrate_database()

View File

@@ -3,7 +3,7 @@
from typing import Any, Mapping
from async_pymongo import AsyncClient, AsyncCollection, AsyncDatabase
from libbot.sync import config_get
from libbot.utils import config_get
db_config: Mapping[str, Any] = config_get("database")
@@ -20,12 +20,19 @@ else:
db_config["host"], db_config["port"], db_config["name"]
)
# Async declarations
db_client = AsyncClient(con_string)
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_checkouts: AsyncCollection = db.get_collection("checkouts")
col_trackings: AsyncCollection = db.get_collection("trackings")
col_authorized: AsyncCollection = db.get_collection("authorized")
col_transactions: AsyncCollection = db.get_collection("transactions")
col_wallets: AsyncCollection = db.get_collection("wallets")
# col_messages: AsyncCollection = db.get_collection("messages")
# col_warnings: AsyncCollection = db.get_collection("warnings")
# col_checkouts: AsyncCollection = db.get_collection("checkouts")
# col_trackings: AsyncCollection = db.get_collection("trackings")
# col_authorized: AsyncCollection = db.get_collection("authorized")
# col_transactions: AsyncCollection = db.get_collection("transactions")
# Update indexes
db.dispatch.get_collection("users").create_index("id", unique=True)
db.dispatch.get_collection("wallets").create_index(["owner_id", "guild_id"], unique=False)

View File

@@ -1,28 +1,26 @@
from importlib.util import module_from_spec, spec_from_file_location
import logging
from importlib.util import module_from_spec, spec_from_file_location
from os import getcwd, walk
from pathlib import Path
from types import ModuleType
from typing import List, Union
from typing import List
logger = logging.getLogger(__name__)
# Import functions
# Took from https://stackoverflow.com/a/57892961
def get_py_files(src: Union[str, Path]) -> List[str]:
def get_py_files(src: str | Path) -> List[str]:
cwd = getcwd() # Current Working directory
py_files = []
for root, dirs, files in walk(src):
py_files.extend(
Path(f"{cwd}/{root}/{file}") for file in files if file.endswith(".py")
)
py_files.extend(Path(f"{cwd}/{root}/{file}") for file in files if file.endswith(".py"))
return py_files
def dynamic_import(module_name: str, py_path: str) -> Union[ModuleType, None]:
def dynamic_import(module_name: str, py_path: str) -> ModuleType | None:
try:
module_spec = spec_from_file_location(module_name, py_path)
@@ -38,7 +36,7 @@ def dynamic_import(module_name: str, py_path: str) -> Union[ModuleType, None]:
"Could not load extension %s due to spec loader being None.",
module_name,
)
return
return None
module_spec.loader.exec_module(module)
@@ -48,13 +46,13 @@ def dynamic_import(module_name: str, py_path: str) -> Union[ModuleType, None]:
"Could not load extension %s due to invalid syntax. Check logs/errors.log for details.",
module_name,
)
return
return None
except Exception as exc:
logger.warning("Could not load extension %s due to %s", module_name, exc)
return
return None
def dynamic_import_from_src(src: Union[str, Path], star_import=False) -> None:
def dynamic_import_from_src(src: str | Path, star_import=False) -> None:
my_py_files = get_py_files(src)
for py_file in my_py_files:
@@ -64,7 +62,7 @@ def dynamic_import_from_src(src: Union[str, Path], star_import=False) -> None:
imported_module = dynamic_import(module_name, py_file)
if imported_module != None:
if imported_module is not None:
if star_import:
for obj in dir(imported_module):
globals()[obj] = imported_module.__dict__[obj]

View File

@@ -1,6 +1,6 @@
from typing import Any, Mapping
from libbot.sync import config_get
from libbot.utils import config_get
from mongodb_migrations.cli import MigrationManager
from mongodb_migrations.config import Configuration

View File

@@ -0,0 +1,6 @@
def hex_to_int(hex_color: str) -> int:
return int(hex_color.lstrip("#"), 16)
def int_to_hex(integer_color: int) -> str:
return "#" + format(integer_color, "06x")

View File

@@ -0,0 +1,9 @@
from typing import Any, Dict
def parse_weather(api_response: Dict[str, Any]) -> str:
return ""
def parse_weather_current(api_response: Dict[str, Any]) -> str:
return ""

106
modules/weather/reporter.py Normal file
View File

@@ -0,0 +1,106 @@
from datetime import datetime
import logging
from typing import Any, Dict, List
from discord import Embed
from pymongo import DESCENDING
from pytz import timezone
from classes.enums import MessageEvents
from classes.pycordbot import PycordBot
from ujson import loads
from modules.utils import hex_to_int
from database import col_messages
from modules.weather.parser import parse_weather
# Example guild key
# "forecast": {
# "channel": 0,
# "time": "10:00:00",
# "delete_previous": true
# "locations": [{"name": "Sample Location", "location": [10.000, 20.000]}],
# }
logger = logging.getLogger(__name__)
async def report_weather(
bot: PycordBot,
guild: PycordGuild, # TODO
channel_id: int,
delete_previous: bool,
locations: List[Dict[str, Any]],
) -> None: # sourcery skip: aware-datetime-for-utc
channel = bot.get_channel(channel_id)
if channel is None:
logger.error(
"Cannot generate weather report for %s's channel %s because channel was not found.",
guild.id,
channel_id,
)
return
# Find and delete previous forecast, if needed
if delete_previous:
async for event in col_messages.find(
{
"event": MessageEvents.WEATHER_FORECAST,
"guild": guild.id,
"channel": channel_id,
},
limit=1,
).sort("date", direction=DESCENDING):
try:
old_message = bot.get_message(event["message"])
if old_message is not None:
await old_message.delete(
reason="Cleanup of the old weather report (look in guild config for details)"
)
except Exception as exc:
logger.warning(
"Could not delete the previous weather report in %s' channel %s due to %s",
guild.id,
channel_id,
exc,
)
embeds: List[Embed] = []
# Iterate through the locations and request their forecasts.
# Results must be parsed and added as embeds to the embeds lits.
for location in locations:
location_timezone_offset = ":".join(
str(
timezone(bot.config["bot"]["timezone"]).utcoffset(datetime.utcnow())
).split(":")[:2]
)
api_response = await (
await bot.client_session.get(
f"https://api.openweathermap.org/data/2.5/onecall?lat={location['location'][0]}&lon={location['location'][1]}&exclude=minutely&units=metric&lang=uk&appid={bot.config['modules']['weather']['forecasts']['api_key']}&tz={location_timezone_offset}"
)
).json(loads=loads)
parsed_weather = parse_weather(api_response)
embeds.append(
Embed(
title=location["name"],
description=parsed_weather,
color=hex_to_int(guild.colors.default),
)
)
# Add a trailing embed with OWM information
embeds.append(
Embed(
title=bot._("weather_report_title", "embeds"),
description=bot._("weather_report_description", "embeds"),
color=hex_to_int(guild.colors.default),
)
)
await channel.send( # type: ignore
content=bot._("weather_report_content", "messages"), embeds=embeds
)

6
pyproject.toml Normal file
View File

@@ -0,0 +1,6 @@
[tool.black]
line-length = 96
target-version = ["py311"]
[tool.isort]
profile = "black"

View File

@@ -1,12 +1,11 @@
aiohttp>=3.6.0
apscheduler~=3.10.4
apscheduler~=3.11.0
async_pymongo==0.1.11
colorthief==0.2.1
deepl==1.16.1
fastapi[all]~=0.109.1
mongodb-migrations==1.3.0
deepl==1.22.0
fastapi[all]~=0.115.0
libbot[speed,pycord,cache]==4.1.0
mongodb-migrations==1.3.1
pynacl~=1.5.0
pyrmv==0.3.5
pytz~=2024.1
--extra-index-url https://git.end-play.xyz/api/packages/profitroll/pypi/simple
async_pymongo==0.1.4
libbot[speed,pycord]==3.0.0
pyrmv==0.4.0
pytz~=2025.1