Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#6 Pydantic Settings as env manager #7

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.local.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
IBB_KEY=local
IBB_EXPIRATION=604800

TELEGRAM_API_ID=1
TELEGRAM_API_HASH=local
TELEGRAM_CHATS=[{"ID": 1234, "IGNORE_USERS": ["test"], "WEBHOOKS": ["https://discord.com/api/webhooks/9012/wxyz"]}]
167 changes: 161 additions & 6 deletions .gitignore
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Es importante ignorar los archivos

*.session
*.session-journal

porque tienen información sensible sobre la sesión iniciada y podría commitearse por error.
Creo que también debería ignorarse cache.json

Original file line number Diff line number Diff line change
@@ -1,7 +1,162 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock

# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock

# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/

# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
.vscode
config.json
cache.json
*.session
*.session-journal
.idea
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
51 changes: 17 additions & 34 deletions main.py
Original file line number Diff line number Diff line change
@@ -7,21 +7,8 @@
from telethon.extensions import markdown

from src.logger import logger

with open("config.json") as f:
CONFIG = json.load(f)
try:
API_ID = int(CONFIG["api_id"])
API_HASH = str(CONFIG["api_hash"])
CHATS: dict = CONFIG["chats"]
except KeyError as e:
logger.error(f"Missing required field '{e.args[0]}' in config.json")
exit(1)

IBB_KEY = str(CONFIG.get("ibb_key", ""))
IBB_EXPIRATION = int(CONFIG.get("ibb_expiration", 7)) * 24 * 60 * 60
MAX_SIZE = int(CONFIG.get("max_size", 10)) * 1024 * 1024
CHATS_IDS = list(map(int, keys)) if "*" not in (keys := CHATS.keys()) else None
from src.settings.telegram import TelegramSettings
from src.settings.ibb import IBBSettings


class MarkdownNoEmbeds:
@@ -50,44 +37,40 @@ def unparse(text, entities):
return markdown.unparse("".join(new_text), entities)


client = TelegramClient("anon", API_ID, API_HASH)
client = TelegramClient("anon", TelegramSettings.API_ID, TelegramSettings.API_HASH)
client.parse_mode = MarkdownNoEmbeds() # ¿Hacerlo opcional? ¿Por cada Chat?
session = None


@client.on(events.NewMessage(chats=CHATS_IDS))
@client.on(events.NewMessage(chats=TelegramSettings.chats))
async def new_message(event: events.NewMessage.Event):
chat = await event.get_chat()
sender = await event.get_sender()
try:
config = CHATS.get(str(chat.id)) or CHATS["*"]
targets = config["webhooks"]
except KeyError as e:
logger.error(f"Missing required field '{e.args[0]}' in config.json")
return
ignore_usernames = config.get("ignore_users", [])
if sender.username in ignore_usernames:
config = TelegramSettings.get_chat(id=int(chat.id))

if sender.username in config.ignore_users:
logger.info(f"User @{sender.username} ignored")
return

author = f" @{sender.username}" if sender.username else ""
d_username = utils.get_display_name(chat) + author
data = {
"username": d_username,
"content": event.text,
}
if IBB_KEY:
if IBBSettings.KEY:
data["avatar_url"] = await get_profile_photo_url(chat)
if event.message.file:
filename = event.message.file.name or f"file.{event.message.file.ext}"
if event.message.file.size <= MAX_SIZE:
if event.message.file.size <= TelegramSettings.IMAGE_MAX_SIZE:
blob = await event.message.download_media(bytes)
data[filename] = blob
else:
logger.warning(
f"File {filename} exceeds maximum size of {MAX_SIZE / 1024 / 1024} MB"
f"File {filename} exceeds maximum size of {TelegramSettings.IMAGE_MAX_SIZE / 1024 / 1024} MB"
)
for target in targets:
async with session.post(target, data=data) as r:
for webhook in config.webhooks:
async with session.post(webhook, data=data) as r:
if r.status in (200, 204):
logger.info(f"Forwarded message from {d_username}")
else:
@@ -118,18 +101,18 @@ async def get_profile_photo_url(entity):
filename = f"{entity.id}-{entity.photo.photo_id}.jpg"
cache = load_cache()
code = cache.get(filename, "0")
url = f"https://i.ibb.co/{code}/{filename}"
url = IBBSettings.URL.unicode_string() + f"{code}/{filename}"
async with session.get(url) as r1:
if r1.status == 200:
return url
logger.info(f"Cache miss: {filename}")
data = {
"key": IBB_KEY,
"key": IBBSettings.KEY,
"name": filename,
"expiration": str(IBB_EXPIRATION),
"expiration": str(IBBSettings.EXPIRATION),
"image": await client.download_profile_photo(entity, bytes),
}
async with session.post("https://api.imgbb.com/1/upload", data=data) as r2:
async with session.post(IBBSettings.UPLOAD_ENDPOINT.unicode_string(), data=data) as r2:
if r2.status == 200:
info = (await r2.json())["data"]
url = info["url"]
Binary file modified requirements.txt
Binary file not shown.
18 changes: 0 additions & 18 deletions sample.config.json

This file was deleted.

Empty file added src/settings/__init__.py
Empty file.
9 changes: 9 additions & 0 deletions src/settings/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from dotenv import find_dotenv
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=find_dotenv(".env.local"),
extra="ignore"
)
30 changes: 30 additions & 0 deletions src/settings/ibb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from pydantic import Field, HttpUrl
from src.settings.base import Settings


class _IBBSettings(Settings):
KEY: str = Field(
"",
description="ImgBB API key",
examples=[
"b025164c8669ccc59567604a21b0637c"
]
)
EXPIRATION: int = Field(
default=7 * 24 * 60 * 60,
description="Enable this if you want to force uploads to be auto deleted after certain time (in seconds 60-15552000)",
examples=[
60,
15552000
]
)
URL: HttpUrl = Field(
default="https://i.ibb.co/",
)
UPLOAD_ENDPOINT: HttpUrl = Field(
default="https://api.imgbb.com/1/upload",
description="ImgBB API URL",
)


IBBSettings = _IBBSettings(_env_prefix="IBB_")
Loading