Skip to content
Merged
Show file tree
Hide file tree
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
27 changes: 27 additions & 0 deletions redditbot/crawlers/reddit_crawler.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
BASE_URL = 'https://www.reddit.com'

REDDIT_URL = 'https://www.reddit.com/r/{0}/top.json'
USER_URL = 'https://www.reddit.com/user/{0}/about.json'


async def get_subreddits(subreddits):
Expand Down Expand Up @@ -74,3 +75,29 @@ def print_subreddits(threads):

def filter_by_votes(threads, min_votes=1):
return [thread for thread in threads if thread['upvotes'] > min_votes]


async def get_user_info(username):
"""Fetch Reddit user information

:param username: Reddit username to look up
:return: dict with user info or None if not found
"""
headers = {
'User-Agent': 'telegram:redditbot:v1',
}
async with httpx.AsyncClient() as client:
response = await client.get(
USER_URL.format(username),
headers=headers
)
if response.status_code == HTTPStatus.OK:
data = response.json()
user_data = data.get('data', {})
return {
'name': user_data.get('name'),
'link_karma': user_data.get('link_karma', 0),
'comment_karma': user_data.get('comment_karma', 0),
'created_utc': user_data.get('created_utc', 0),
}
return None
33 changes: 33 additions & 0 deletions redditbot/ui/bot.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from datetime import datetime, timezone

from importlib.metadata import version as version_function
from telegram import Update
Expand Down Expand Up @@ -88,6 +89,35 @@ async def get_version(update: Update, context: CallbackContext):
)


async def user_info(update: Update, context: CallbackContext):
args = context.args

if not args:
await update.message.reply_text(
text='Usage: /user <username>'
)
return

username = args[0]
user_data = await rc.get_user_info(username)

if user_data is None:
await update.message.reply_text(
text=f'User "{username}" not found'
)
return

total_karma = user_data['link_karma'] + user_data['comment_karma']
created_date = datetime.fromtimestamp(user_data['created_utc'], timezone.utc)
account_age_days = (datetime.now(timezone.utc) - created_date).days

await update.message.reply_text(
text=f'User: {user_data["name"]}\n'
f'Total Karma: {total_karma}\n'
f'Account Age: {account_age_days} days'
)


def main():
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO)

Expand All @@ -104,6 +134,9 @@ def main():
app.add_handler(
CommandHandler('version', get_version)
)
app.add_handler(
CommandHandler('user', user_info)
)

app.run_polling()

Expand Down
25 changes: 25 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,28 @@ def mock_request_dog_with_low_votes(httpx_mock):
}
},
)


@pytest.fixture
def mock_request_user(httpx_mock):
httpx_mock.add_response(
method='GET',
url='https://www.reddit.com/user/testuser/about.json',
json={
'data': {
'name': 'testuser',
'link_karma': 1000,
'comment_karma': 500,
'created_utc': 1609459200.0,
}
},
)


@pytest.fixture
def mock_request_user_not_found(httpx_mock):
httpx_mock.add_response(
method='GET',
url='https://www.reddit.com/user/nonexistent/about.json',
status_code=404,
)
23 changes: 21 additions & 2 deletions tests/crawlers/test_crawler.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
filter_by_votes,
convert_element_to_thread,
convert_internal_link_to_absolute,
print_subreddits, get_subreddits
print_subreddits,
get_subreddits,
get_user_info
)


Expand Down Expand Up @@ -65,4 +67,21 @@ def test_print_subreddits(print_mock):
async def test_get_subreddits_should_return_threads(mock_request_dog):
threads = await get_subreddits(['dogs'])

assert len(threads) == 1
assert len(threads) == 1


async def test_get_user_info_should_return_user_data(mock_request_user):
user = await get_user_info('testuser')

assert user['name'] == 'testuser'
assert user['link_karma'] == 1000
assert user['comment_karma'] == 500
assert user['created_utc'] == 1609459200.0


async def test_get_user_info_should_return_none_for_not_found(
mock_request_user_not_found
):
user = await get_user_info('nonexistent')

assert user is None
56 changes: 54 additions & 2 deletions tests/ui/test_bot.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from unittest import mock

from redditbot.ui import bot
from redditbot.ui.bot import nada_para_fazer, start
from redditbot.ui.bot import nada_para_fazer, start, user_info


class TestMain:
Expand All @@ -17,7 +17,7 @@ def test_main_should_register_bot_handlers_and_start_polling(

bot.main()

assert app_mock.add_handler.call_count == 4
assert app_mock.add_handler.call_count == 5

app_mock.run_polling.assert_called_once()

Expand Down Expand Up @@ -111,3 +111,55 @@ async def test_nada_para_fazer_should_send_not_found(

assert update.message.reply_text.call_count == 2
update.message.reply_text.assert_has_calls(calls)


class TestUserInfo:

async def test_user_info_should_send_usage_when_no_args(self):
update = mock.MagicMock()
update.message.reply_text = mock.AsyncMock()
context = mock.MagicMock()
context.args = []

await user_info(update, context)

update.message.reply_text.assert_awaited_once_with(
text='Usage: /user <username>'
)

@mock.patch('redditbot.ui.bot.rc.get_user_info')
async def test_user_info_should_send_user_data(self, get_user_info_mock):
get_user_info_mock.return_value = {
'name': 'testuser',
'link_karma': 1000,
'comment_karma': 500,
'created_utc': 1609459200.0,
}
update = mock.MagicMock()
update.message.reply_text = mock.AsyncMock()
context = mock.MagicMock()
context.args = ['testuser']

await user_info(update, context)

get_user_info_mock.assert_awaited_once_with('testuser')
call_args = update.message.reply_text.call_args
assert 'testuser' in call_args.kwargs['text']
assert '1500' in call_args.kwargs['text']

@mock.patch('redditbot.ui.bot.rc.get_user_info')
async def test_user_info_should_send_not_found(
self,
get_user_info_mock
):
get_user_info_mock.return_value = None
update = mock.MagicMock()
update.message.reply_text = mock.AsyncMock()
context = mock.MagicMock()
context.args = ['nonexistent']

await user_info(update, context)

update.message.reply_text.assert_awaited_once_with(
text='User "nonexistent" not found'
)