Merge branch 'main' of https://gitea.corbz.dev/corbz/NewsBot
This commit is contained in:
commit
f2e10e6584
11
.vscode/settings.json
vendored
Normal file
11
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"python.testing.unittestArgs": [
|
||||||
|
"-v",
|
||||||
|
"-s",
|
||||||
|
"./src",
|
||||||
|
"-p",
|
||||||
|
"test*.py"
|
||||||
|
],
|
||||||
|
"python.testing.pytestEnabled": false,
|
||||||
|
"python.testing.unittestEnabled": true
|
||||||
|
}
|
18
README.md
18
README.md
@ -1,17 +1,7 @@
|
|||||||
# NewsBot
|
# PYRSS
|
||||||
|
|
||||||
Bot delivering news articles to discord servers.
|
An RSS driven Discord bot written in Python.
|
||||||
|
|
||||||
Plans
|
Provides user commands for storing RSS feed URLs that can be assigned to any given discord channel.
|
||||||
|
|
||||||
- Multiple news providers
|
Content is shared every 10 minutes as an Embed.
|
||||||
- Choose how much of each provider should be delivered
|
|
||||||
- Check for duplicate articles between providers, and only deliver preferred provider article
|
|
||||||
|
|
||||||
|
|
||||||
## Dev Notes:
|
|
||||||
|
|
||||||
For the sake of development, the following defintions apply:
|
|
||||||
|
|
||||||
- Feed - An RSS feed stored within the database, submitted by a user.
|
|
||||||
- Assigned Feed - A discord channel set to receive content from a Feed.
|
|
@ -17,11 +17,14 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class DiscordBot(commands.Bot):
|
class DiscordBot(commands.Bot):
|
||||||
|
|
||||||
def __init__(self, BASE_DIR: Path):
|
def __init__(self, BASE_DIR: Path, developing: bool):
|
||||||
activity = Game("Indev")
|
activity = Game("Indev") if developing else None
|
||||||
super().__init__(command_prefix="-", intents=Intents.all(), activity=activity)
|
super().__init__(command_prefix="-", intents=Intents.all(), activity=activity)
|
||||||
self.functions = Functions(self)
|
self.functions = Functions(self)
|
||||||
self.BASE_DIR = BASE_DIR
|
self.BASE_DIR = BASE_DIR
|
||||||
|
self.developing = developing
|
||||||
|
|
||||||
|
log.info("developing=%s", developing)
|
||||||
|
|
||||||
async def sync_app_commands(self):
|
async def sync_app_commands(self):
|
||||||
"""
|
"""
|
||||||
|
@ -48,13 +48,20 @@ class TaskCog(commands.Cog):
|
|||||||
|
|
||||||
@commands.Cog.listener()
|
@commands.Cog.listener()
|
||||||
async def on_ready(self):
|
async def on_ready(self):
|
||||||
"""Instructions to call when the cog is ready."""
|
"""Instructions to execute when the cog is ready."""
|
||||||
|
|
||||||
self.rss_task.start()
|
if not self.bot.developing:
|
||||||
|
self.rss_task.start()
|
||||||
|
|
||||||
log.info("%s cog is ready", self.__class__.__name__)
|
log.info("%s cog is ready", self.__class__.__name__)
|
||||||
|
|
||||||
@tasks.loop(time=times)
|
@commands.Cog.listener(name="cog_unload")
|
||||||
|
async def on_unload(self):
|
||||||
|
"""Instructions to execute before the cog is unloaded."""
|
||||||
|
|
||||||
|
self.rss_task.cancel()
|
||||||
|
|
||||||
|
@tasks.loop(minutes=10)
|
||||||
async def rss_task(self):
|
async def rss_task(self):
|
||||||
"""Automated task responsible for processing rss feeds."""
|
"""Automated task responsible for processing rss feeds."""
|
||||||
|
|
||||||
@ -86,7 +93,9 @@ class TaskCog(commands.Cog):
|
|||||||
|
|
||||||
channel = self.bot.get_channel(feed.discord_channel_id)
|
channel = self.bot.get_channel(feed.discord_channel_id)
|
||||||
|
|
||||||
unparsed_content = await get_unparsed_feed(feed.rss_source.rss_url)
|
# TODO: integrate the `validate_feed` code into here, also do on list command and show errors.
|
||||||
|
|
||||||
|
unparsed_content = await self.bot.functions.get_unparsed_feed(feed.rss_source.rss_url)
|
||||||
parsed_feed = parse(unparsed_content)
|
parsed_feed = parse(unparsed_content)
|
||||||
source = Source.from_parsed(parsed_feed)
|
source = Source.from_parsed(parsed_feed)
|
||||||
articles = source.get_latest_articles(5)
|
articles = source.get_latest_articles(5)
|
||||||
|
@ -84,7 +84,7 @@ class LogSetup:
|
|||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=log_level,
|
level=log_level,
|
||||||
handlers=(queue_handler,),
|
handlers=(queue_handler,),
|
||||||
format='[%(asctime)s] [%(levelname)-8s] [%(name)-18s]: %(message)s'
|
format='[%(asctime)s] [%(levelname)s] [%(name)s]: %(message)s'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create a new log file
|
# Create a new log file
|
||||||
|
@ -31,15 +31,17 @@ async def main():
|
|||||||
if not token:
|
if not token:
|
||||||
raise ValueError("Token is empty")
|
raise ValueError("Token is empty")
|
||||||
|
|
||||||
|
developing = bool(getenv("DEVELOPING"))
|
||||||
|
|
||||||
# Setup logging settings and mute spammy loggers
|
# Setup logging settings and mute spammy loggers
|
||||||
logsetup = LogSetup(BASE_DIR)
|
logsetup = LogSetup(BASE_DIR)
|
||||||
logsetup.setup_logs(logging.DEBUG)
|
logsetup.setup_logs(logging.DEBUG if developing else logging.INFO)
|
||||||
logsetup.update_log_levels(
|
logsetup.update_log_levels(
|
||||||
('discord', 'PIL', 'urllib3', 'aiosqlite', 'charset_normalizer'),
|
('discord', 'PIL', 'urllib3', 'aiosqlite', 'charset_normalizer'),
|
||||||
level=logging.WARNING
|
level=logging.WARNING
|
||||||
)
|
)
|
||||||
|
|
||||||
async with DiscordBot(BASE_DIR) as bot:
|
async with DiscordBot(BASE_DIR, developing=developing) as bot:
|
||||||
await bot.load_extensions()
|
await bot.load_extensions()
|
||||||
await bot.start(token, reconnect=True)
|
await bot.start(token, reconnect=True)
|
||||||
|
|
||||||
|
53
src/tests.py
53
src/tests.py
@ -1,15 +1,54 @@
|
|||||||
|
|
||||||
|
import unittest
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.engine.cursor import CursorResult
|
||||||
|
from sqlalchemy.engine.result import ChunkedIteratorResult
|
||||||
|
|
||||||
|
from db import DatabaseManager, FeedChannelModel, AuditModel
|
||||||
|
|
||||||
|
|
||||||
|
class TestDatabaseConnections(unittest.IsolatedAsyncioTestCase):
|
||||||
|
"""The purpose of this test, is to ensure that the database connections function properly."""
|
||||||
|
|
||||||
def test_article_embed():
|
async def test_select__feed_channel_model(self):
|
||||||
assert True, ""
|
"""This test runs a select query on the `FeedChannelModel`"""
|
||||||
|
|
||||||
|
async with DatabaseManager() as database:
|
||||||
|
query = select(FeedChannelModel).limit(1000)
|
||||||
|
result = await database.session.execute(query)
|
||||||
|
|
||||||
|
self.assertIsInstance(
|
||||||
|
result,
|
||||||
|
ChunkedIteratorResult,
|
||||||
|
f"Result should be `ChunkedIteratorResult`, not {type(result)!r}"
|
||||||
|
)
|
||||||
|
|
||||||
def main():
|
async def test_select__rss_source_model(self):
|
||||||
|
"""This test runs a select query on the `RssSourceModel`"""
|
||||||
# test article embed
|
|
||||||
test_article_embed()
|
async with DatabaseManager() as database:
|
||||||
|
query = select(RssSourceModel).limit(1000)
|
||||||
|
result = await database.session.execute(query)
|
||||||
|
|
||||||
|
self.assertIsInstance(
|
||||||
|
result,
|
||||||
|
ChunkedIteratorResult,
|
||||||
|
f"Result should be `ChunkedIteratorResult`, not {type(result)!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def test_select__audit_model(self):
|
||||||
|
"""This test runs a select query on the `AuditModel`"""
|
||||||
|
|
||||||
|
async with DatabaseManager() as database:
|
||||||
|
query = select(AuditModel).limit(1000)
|
||||||
|
result = await database.session.execute(query)
|
||||||
|
|
||||||
|
self.assertIsInstance(
|
||||||
|
result,
|
||||||
|
ChunkedIteratorResult,
|
||||||
|
f"Result should be `ChunkedIteratorResult`, not {type(result)!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
unittest.main()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user