fork para inversionitas con bot simple

This commit is contained in:
2025-03-12 12:03:05 +01:00
parent d0a1ee08f7
commit 65a043aee8
9 changed files with 195 additions and 292 deletions

3
.gitignore vendored
View File

@ -1,3 +1,4 @@
entorno entorno
__pypchache__ __pypchache__
*.pyc *.pyc
venv

View File

@ -9,8 +9,8 @@ load_dotenv()
# Configuración de MySQL desde variables de entorno # Configuración de MySQL desde variables de entorno
MYSQL_HOST = os.getenv("MYSQL_HOST", "mysql") MYSQL_HOST = os.getenv("MYSQL_HOST", "mysql")
MYSQL_USER = os.getenv("MYSQL_USER", "nick") MYSQL_USER = os.getenv("MYSQL_USER", "investionitas")
MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", "nick") MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", "inversionitas")
MYSQL_DATABASE = os.getenv("MYSQL_DATABASE", "news") MYSQL_DATABASE = os.getenv("MYSQL_DATABASE", "news")
MYSQL_PORT = os.getenv("MYSQL_PORT", "3306") MYSQL_PORT = os.getenv("MYSQL_PORT", "3306")

View File

@ -12,74 +12,28 @@ logging.basicConfig(
) )
# Obtener variables de entorno # Obtener variables de entorno
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://host.docker.internal:11434/api/generate") # OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://host.docker.internal:11434/api/generate")
OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "llama3") # OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "llama3")
def is_security_related(prompt): # def is_economy_related(prompt):
data = { # data = {
"model": OLLAMA_MODEL, # "model": OLLAMA_MODEL,
"prompt": f"Does the following topic relate to national defense, armed forces, police, espionage, or intelligence? Answer only with 'true' or 'false'. Topic: {prompt}", # "prompt": f"Does the following topic relate to economy, investing or financial issues? Answer only with 'true' or 'false'. Topic: {prompt}",
} # }
try: # try:
response = requests.post(OLLAMA_URL, json=data) # response = requests.post(OLLAMA_URL, json=data)
response.raise_for_status() # Lanza una excepción si la solicitud falla # response.raise_for_status() # Lanza una excepción si la solicitud falla
for line in response.text.strip().split("\n"): # for line in response.text.strip().split("\n"):
json_data = json.loads(line) # json_data = json.loads(line)
if "response" in json_data and json_data["response"].strip(): # if "response" in json_data and json_data["response"].strip():
result = json_data["response"].strip().lower() == "true" # result = json_data["response"].strip().lower() == "true"
return result # return result
except requests.RequestException as e: # except requests.RequestException as e:
logging.error(f"Request error: {e}") # logging.error(f"Request error: {e}")
except json.JSONDecodeError as e: # except json.JSONDecodeError as e:
logging.error(f"JSON Decode Error: {e}") # logging.error(f"JSON Decode Error: {e}")
return False # return False
def is_critico(prompt):
data = {
"model": OLLAMA_MODEL,
"prompt": f"Does the following text criticizes the armed forces, security forces as Guardia Civil or Police, intelligence agencies such as CNI? Answer only with 'true' or 'false'. Topic: {prompt}",
}
try:
response = requests.post(OLLAMA_URL, json=data)
response.raise_for_status()
for line in response.text.strip().split("\n"):
json_data = json.loads(line)
if "response" in json_data and json_data["response"].strip():
result = json_data["response"].strip().lower() == "true"
return result
except requests.RequestException as e:
logging.error(f"Request error: {e}")
except json.JSONDecodeError as e:
logging.error(f"JSON Decode Error: {e}")
return False
def is_favorable(prompt):
data = {
"model": OLLAMA_MODEL,
"prompt": f"Does the following text favor the armed forces, security forces as Guardia Civil or Police, intelligence agencies such as CNI? Answer only with 'true' or 'false'. Topic: {prompt}",
}
try:
response = requests.post(OLLAMA_URL, json=data)
response.raise_for_status()
for line in response.text.strip().split("\n"):
json_data = json.loads(line)
if "response" in json_data and json_data["response"].strip():
result = json_data["response"].strip().lower() == "true"
return result
except requests.RequestException as e:
logging.error(f"Request error: {e}")
except json.JSONDecodeError as e:
logging.error(f"JSON Decode Error: {e}")
return False

View File

@ -2,6 +2,8 @@ from fastapi import FastAPI
from database import Base, engine from database import Base, engine
from routes import router from routes import router
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
from contextlib import asynccontextmanager
from telegrambot import enviar_mensaje_sync
#from webscrapper import ejecutar_scrapper #from webscrapper import ejecutar_scrapper
# Crear las tablas en MySQL si no existen # Crear las tablas en MySQL si no existen
@ -11,14 +13,17 @@ Base.metadata.create_all(bind=engine)
app = FastAPI() app = FastAPI()
scheduler = BackgroundScheduler() scheduler = BackgroundScheduler()
def tarea_programada():
enviar_mensaje_sync("¡Buenos días! Aquí tienes tu mensaje diario.")
scheduler.add_job(tarea_programada, "cron", hour=8, minute=0)
@asynccontextmanager
async def lifespan(app: FastAPI):
scheduler.start() # Iniciar el scheduler cuando la app arranca
yield
scheduler.shutdown() # Apagar el scheduler al cerrar la app
# Incluir rutas # Incluir rutas
app.include_router(router) app.include_router(router)
# @app.on_event("startup")
# def startup_event():
# scheduler.add_job(ejecutar_scrapper, "interval", hours=24)
# scheduler.start()
# @app.on_event("shutdown")
# def shutdown_event():
# scheduler.shutdown()

View File

@ -12,6 +12,4 @@ class NewsItem(Base):
fuente = Column(String(255), nullable=True) fuente = Column(String(255), nullable=True)
fecha = Column(DateTime, default=datetime.utcnow) fecha = Column(DateTime, default=datetime.utcnow)
link = Column(String(500), unique=True, nullable=False) link = Column(String(500), unique=True, nullable=False)
critico = Column(Boolean, default=False)
favorable = Column(Boolean, default=False)
keyword = Column(String(255), unique=False, nullable=True) keyword = Column(String(255), unique=False, nullable=True)

View File

@ -1,4 +1,4 @@
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy.sql import func from sqlalchemy.sql import func
from database import get_db from database import get_db
@ -25,157 +25,8 @@ class NewsItemCreate(BaseModel):
fuente: str | None = None fuente: str | None = None
fecha: datetime | None = None fecha: datetime | None = None
link: str link: str
critico:bool
favorable:bool
keyword: str | None = None keyword: str | None = None
@router.post("/news/")
def create_news_item(item: NewsItemCreate, db: Session = Depends(get_db)):
# Verificar si el título ya existe
existing_item = db.query(NewsItem).filter(NewsItem.titulo == item.titulo).first()
if existing_item:
logging.info("Título ya en la base de datos")
# Crear nuevo objeto
new_item = NewsItem(
titulo=item.titulo,
contenido=item.contenido,
autor=item.autor,
fuente=item.fuente,
fecha=item.fecha or datetime.utcnow(),
link=item.link,
critico=item.critico,
favorable=item.favorable,
keyword=item.keyword
)
db.add(new_item)
db.commit()
db.refresh(new_item)
return {"message": "Noticia creada con éxito", "id": new_item.id, "titulo": new_item.titulo}
@router.get("/news/count/by-source/date-range")
def count_news_by_source_in_range(
fecha_inicio: datetime, fecha_fin: datetime, db: Session = Depends(get_db)
):
results = (
db.query(NewsItem.fuente, func.count(NewsItem.id))
.filter(NewsItem.fecha >= fecha_inicio, NewsItem.fecha <= fecha_fin)
.group_by(NewsItem.fuente)
.all()
)
return {"count_by_source_in_range": [{"fuente": fuente, "count": count} for fuente, count in results]}
@router.get("/news/count/by-author/date-range")
def count_news_by_author_in_range(
fecha_inicio: datetime, fecha_fin: datetime, db: Session = Depends(get_db)
):
results = (
db.query(NewsItem.autor, func.count(NewsItem.id))
.filter(NewsItem.fecha >= fecha_inicio, NewsItem.fecha <= fecha_fin)
.group_by(NewsItem.autor)
.all()
)
return {"count_by_author_in_range": [{"autor": autor, "count": count} for autor, count in results]}
@router.get("/news/count/favorable/by-author/date-range")
def count_favorable_news_by_author_in_range(
fecha_inicio: datetime, fecha_fin: datetime, db: Session = Depends(get_db)
):
results = (
db.query(NewsItem.autor, func.count(NewsItem.id))
.filter(
NewsItem.favorable == True,
NewsItem.fecha >= fecha_inicio,
NewsItem.fecha <= fecha_fin,
)
.group_by(NewsItem.autor)
.all()
)
return {"favorable_count_by_author_in_range": [{"autor": autor, "count": count} for autor, count in results]}
@router.get("/news/count/unfavorable/by-author/date-range")
def count_unfavorable_news_by_author_in_range(
fecha_inicio: datetime, fecha_fin: datetime, db: Session = Depends(get_db)
):
results = (
db.query(NewsItem.autor, func.count(NewsItem.id))
.filter(
NewsItem.critico == True,
NewsItem.fecha >= fecha_inicio,
NewsItem.fecha <= fecha_fin,
)
.group_by(NewsItem.autor)
.all()
)
return {"unfavorable_count_by_author_in_range": [{"autor": autor, "count": count} for autor, count in results]}
@router.get("/news/count/favorable/by-source/date-range")
def count_favorable_news_by_source_in_range(
fecha_inicio: datetime, fecha_fin: datetime, db: Session = Depends(get_db)
):
results = (
db.query(NewsItem.fuente, func.count(NewsItem.id))
.filter(
NewsItem.favorable == True,
NewsItem.fecha >= fecha_inicio,
NewsItem.fecha <= fecha_fin,
)
.group_by(NewsItem.fuente)
.all()
)
return {"favorable_count_by_source_in_range": [{"fuente": fuente, "count": count} for fuente, count in results]}
@router.get("/news/count/unfavorable/by-source/date-range")
def count_unfavorable_news_by_source_in_range(
fecha_inicio: datetime, fecha_fin: datetime, db: Session = Depends(get_db)
):
results = (
db.query(NewsItem.fuente, func.count(NewsItem.id))
.filter(
NewsItem.critico == True,
NewsItem.fecha >= fecha_inicio,
NewsItem.fecha <= fecha_fin,
)
.group_by(NewsItem.fuente)
.all()
)
return {"unfavorable_count_by_source_in_range": [{"fuente": fuente, "count": count} for fuente, count in results]}
@router.get("/news/neutral/date-range")
def get_neutral_news_in_range(
fecha_inicio: datetime, fecha_fin: datetime, db: Session = Depends(get_db)
):
results = (
db.query(NewsItem)
.filter(
NewsItem.favorable == False,
NewsItem.critico == False,
NewsItem.fecha >= fecha_inicio,
NewsItem.fecha <= fecha_fin,
)
.all()
)
return results
@router.get("/news/mixed/date-range")
def get_mixed_news_in_range(
fecha_inicio: datetime, fecha_fin: datetime, db: Session = Depends(get_db)
):
results = (
db.query(NewsItem)
.filter(
NewsItem.favorable == True,
NewsItem.critico == True,
NewsItem.fecha >= fecha_inicio,
NewsItem.fecha <= fecha_fin,
)
.all()
)
return results
@router.get("/news/count/by-source") @router.get("/news/count/by-source")
def count_news_by_source(db: Session = Depends(get_db)): def count_news_by_source(db: Session = Depends(get_db)):
results = ( results = (
@ -202,62 +53,142 @@ def count_news_by_author(db: Session = Depends(get_db)):
return {"count_by_author": response} return {"count_by_author": response}
@router.get("/news/count/favorable/by-author") @router.get("/news/{news_id}")
def count_favorable_news_by_author(db: Session = Depends(get_db)): def get_news_by_id(news_id: int, db: Session = Depends(get_db)):
results = ( news_item = db.query(NewsItem).filter(NewsItem.id == news_id).first()
db.query(NewsItem.autor, func.count(NewsItem.id))
.filter(NewsItem.favorable == True) if not news_item:
.group_by(NewsItem.autor) raise HTTPException(status_code=404, detail="Noticia no encontrada")
.all()
) return {
return {"favorable_count_by_author": results} "id": news_item.id,
"titulo": news_item.titulo,
"contenido": news_item.contenido,
"autor": news_item.autor,
"fuente": news_item.fuente,
"fecha": news_item.fecha,
"link": news_item.link,
"critico": news_item.critico,
"favorable": news_item.favorable,
"keyword": news_item.keyword,
}
@router.get("/news/count/unfavorable/by-author") @router.get("/news/count/by-keyword")
def count_unfavorable_news_by_author(db: Session = Depends(get_db)): def count_news_by_keyword(
results = ( fechaInicio: datetime | None = None,
db.query(NewsItem.autor, func.count(NewsItem.id)) fechaFin: datetime | None = None,
.filter(NewsItem.critico == True) db: Session = Depends(get_db)):
.group_by(NewsItem.autor) query = db.query(NewsItem.keyword, func.count(NewsItem.id)).filter(NewsItem.keyword.isnot(None))
.all() if fechaInicio and fechaFin:
) query = query.filter(NewsItem.fecha >= fechaInicio, NewsItem.fecha <= fechaFin)
return {"unfavorable_count_by_author": results} results = query.group_by(NewsItem.keyword).all()
@router.get("/news/count/favorable/by-source") return {keyword: count for keyword, count in results}
def count_favorable_news_by_source(db: Session = Depends(get_db)):
@router.get("/news/keywords/")
def get_all_keywords(db: Session = Depends(get_db)):
results = db.query(NewsItem.keyword).distinct().all()
# Extraer solo las keywords y filtrar valores nulos
keywords = [keyword[0] for keyword in results if keyword[0] is not None]
return {"keywords": keywords}
@router.get("/news/count/by-source/keyword/date-range")
def count_news_by_source_for_keyword_in_range(
keyword: str, fechaInicio: datetime, fechaFin: datetime, db: Session = Depends(get_db)
):
results = ( results = (
db.query(NewsItem.fuente, func.count(NewsItem.id)) db.query(NewsItem.fuente, func.count(NewsItem.id))
.filter(NewsItem.favorable == True) .filter(
NewsItem.keyword == keyword,
NewsItem.fecha >= fechaInicio,
NewsItem.fecha <= fechaFin
)
.group_by(NewsItem.fuente) .group_by(NewsItem.fuente)
.all() .all()
) )
return {"favorable_count_by_source": results} formatted_results = {fuente: count for fuente, count in results}
return {"count_by_source_keyword": formatted_results}
@router.get("/news/count/unfavorable/by-source") @router.get("/news/titles/by-keyword/date-range")
def count_unfavorable_news_by_source(db: Session = Depends(get_db)): def get_titles_by_keyword_in_range(
keyword: str, fecha_inicio: datetime, fecha_fin: datetime, db: Session = Depends(get_db)
):
results = ( results = (
db.query(NewsItem.fuente, func.count(NewsItem.id)) db.query(NewsItem.titulo)
.filter(NewsItem.critico == True) .filter(
.group_by(NewsItem.fuente) NewsItem.keyword == keyword,
NewsItem.fecha >= fecha_inicio,
NewsItem.fecha <= fecha_fin
)
.all() .all()
) )
return {"unfavorable_count_by_source": results}
@router.get("/news/neutral") # Convertir resultados en una sola cadena separada por comas
def get_neutral_news(db: Session = Depends(get_db)): titles = ", ".join([titulo[0] for titulo in results if titulo[0]])
results = (
db.query(NewsItem)
.filter(NewsItem.favorable == False, NewsItem.critico == False)
.all()
)
return results
return {"titles": titles}
@router.get("/news/mixed") # @router.get("/news-summary")
def get_mixed_news(db: Session = Depends(get_db)): # def get_news_summary(
results = ( # keyword: str,
db.query(NewsItem) # fecha_inicio: datetime,
.filter(NewsItem.favorable == True, NewsItem.critico == True) # fecha_fin: datetime,
.all() # db: Session = Depends(get_db)
) # ):
return results # results = get_titles_by_keyword_in_range(keyword,fecha_inicio, fecha_fin, db)
# data = {
# "model":"llama3",
# "prompt": f"A continuación tienes los titulares de muchas noticias, resume lo que encuentres más destacado. Noticias: {results}",
# }
# try:
# response = requests.post(
# "http://host.docker.internal:11434/api/generate", json=data
# )
# response.raise_for_status()
# except requests.RequestException as e:
# logging.error(f"Request error: {e}")
# return response
@router.get("/news/titles/")
def get_news_titles(
fechaInicio: datetime | None = None,
fechaFin: datetime | None = None,
db: Session = Depends(get_db),
):
query = db.query(NewsItem.titulo)
if fechaInicio and fechaFin:
query = query.filter(NewsItem.fecha >= fechaInicio, NewsItem.fecha <= fechaFin)
results = query.all()
return {"titles": [titulo[0] for titulo in results]}
@router.get("/news/")
def get_all_news(
fechaInicio: datetime | None = None,
fechaFin: datetime | None = None,
page:int = Query(1, alias="page", ge=1),
pageSize: int = Query(10, alias="pageSize", ge=1, le=100),
db: Session = Depends(get_db),
):
query = db.query(NewsItem.id, NewsItem.titulo, NewsItem.link, NewsItem.favorable, NewsItem.critico) # Solo seleccionamos id y titulo
if fechaInicio and fechaFin:
query = query.filter(NewsItem.fecha >= fechaInicio, NewsItem.fecha <= fechaFin)
totalCount = query.count()
results = query.offset((page - 1) * pageSize).limit(pageSize).all()
return {
"totalCount": totalCount,
"page": page,
"pageSize": pageSize,
"totalPages": (totalCount + pageSize - 1) // pageSize,
"news": [{"id": news_id, "titulo": titulo, "url":link, "critico":critico, "favorable": favorable} for
news_id, titulo, link, critico, favorable in results] # Devolvemos solo id y titulo
}

15
app/telegrambot.py Normal file
View File

@ -0,0 +1,15 @@
from telegram import Bot
import asyncio
# Configura tu bot con el token de BotFather
TOKEN = "7626026035:AAHEMp_iIN3y8AwywL0R6OTQvNi7EcJZ0iY"
CHAT_ID = "-123456789" # Reemplaza con el ID del grupo
bot = Bot(token=TOKEN)
async def enviar_mensaje(mensaje: str):
await bot.send_message(chat_id=CHAT_ID, text=mensaje)
# Para ejecutar la función de forma síncrona cuando sea necesario
def enviar_mensaje_sync(mensaje: str):
asyncio.run(enviar_mensaje(mensaje))

View File

@ -2,7 +2,7 @@ import requests
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import time import time
from googlenewsdecoder import gnewsdecoder from googlenewsdecoder import gnewsdecoder
from iacorrector import is_security_related, is_critico, is_favorable # Importa la función desde iacorrector.py # from iacorrector import is_economy_related # Importa la función desde iacorrector.py
from datetime import datetime from datetime import datetime
import pytz import pytz
import logging import logging
@ -103,8 +103,6 @@ def search_news(query):
date = article.pubDate.get_text(strip=True) if article.pubDate else "Fecha no disponible" date = article.pubDate.get_text(strip=True) if article.pubDate else "Fecha no disponible"
date_parsed = datetime.strptime(date, '%a, %d %b %Y %H:%M:%S GMT') date_parsed = datetime.strptime(date, '%a, %d %b %Y %H:%M:%S GMT')
date_parsed = date_parsed.replace(tzinfo=pytz.UTC) date_parsed = date_parsed.replace(tzinfo=pytz.UTC)
critico = False
favorable = False
# Obtener la URL final del artículo # Obtener la URL final del artículo
final_url = get_url_from_google_news(link) final_url = get_url_from_google_news(link)
@ -112,24 +110,22 @@ def search_news(query):
# Obtener el autor usando autorsearcher.py # Obtener el autor usando autorsearcher.py
author = get_author_from_script(final_url) author = get_author_from_script(final_url)
content = get_article_content(final_url) content = get_article_content(final_url)
critico = is_critico(content)
favorable = is_favorable(content)
# Verificar si el artículo es válido usando iacorrector # Verificar si el artículo es válido usando iacorrector
if is_security_related(content): # Solo si el artículo es válido # if is_economy_related(content): # Solo si el artículo es válido
news_item = { # news_item = {
"titulo": title, # "titulo": title,
"contenido": content, # "contenido": content,
"autor": author, # "autor": author,
"fuente": source_info, # "fuente": source_info,
"fecha": date_parsed.isoformat(), # "fecha": date_parsed.isoformat(),
"link": final_url, # Guardamos la URL final en lugar de la de Google News, # "link": final_url, # Guardamos la URL final en lugar de la de Google News,
"critico": critico, # "critico": critico,
"favorable":favorable, # "favorable":favorable,
"keyword": query # "keyword": query
} # }
insertar_datos(news_item) # insertar_datos(news_item)
except Exception as e: except Exception as e:
logging.info(f"Error al procesar un artículo para '{query}': {e}") logging.info(f"Error al procesar un artículo para '{query}': {e}")

View File

@ -9,3 +9,6 @@ sqlalchemy
pydantic pydantic
python-dotenv python-dotenv
mysql-connector-python mysql-connector-python
python-telegram-bot
apscheduler