diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c6932c..b5628ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ Changelog **0.18.0 (TBA)** +* Add support for public, tag and list timelines in `toot timeline` (#52) * Add `--sensitive` and `--spoiler-text` options to `toot post` (#63) **0.17.1 (2018-01-15)** diff --git a/tests/test_console.py b/tests/test_console.py index eabc1bc..198349e 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -109,7 +109,8 @@ def test_timeline(mock_get, monkeypatch, capsys): out, err = capsys.readouterr() assert "The computer can't tell you the emotional story." in out - assert "Frank Zappa @fz" in out + assert "Frank Zappa" in out + assert "@fz" in out @mock.patch('toot.http.post') diff --git a/toot/api.py b/toot/api.py index 7d31367..a5bde26 100644 --- a/toot/api.py +++ b/toot/api.py @@ -2,7 +2,7 @@ import re -from urllib.parse import urlparse, urlencode +from urllib.parse import urlparse, urlencode, quote from toot import http, CLIENT_NAME, CLIENT_WEBSITE from toot.exceptions import AuthenticationError @@ -55,7 +55,7 @@ def get_browser_login_url(app): return "{}/oauth/authorize/?{}".format(app.base_url, urlencode({ "response_type": "code", "redirect_uri": "urn:ietf:wg:oauth:2.0:oob", - "scope": "read write follow", + "scope": SCOPES, "client_id": app.client_id, })) @@ -91,6 +91,22 @@ def timeline_home(app, user): return http.get(app, user, '/api/v1/timelines/home').json() +def timeline_public(app, user, local=False): + params = {'local': 'true' if local else 'false'} + return http.get(app, user, '/api/v1/timelines/public', params).json() + + +def timeline_tag(app, user, hashtag, local=False): + url = '/api/v1/timelines/tag/{}'.format(quote(hashtag)) + params = {'local': 'true' if local else 'false'} + return http.get(app, user, url, params).json() + + +def timeline_list(app, user, list_id): + url = '/api/v1/timelines/list/{}'.format(list_id) + return http.get(app, user, url).json() + + def get_next_path(headers): """Given timeline response headers, returns the path to the next batch""" links = headers.get('Link', '') diff --git a/toot/commands.py b/toot/commands.py index 6a98a0d..67e4fa1 100644 --- a/toot/commands.py +++ b/toot/commands.py @@ -8,7 +8,22 @@ from toot.utils import assert_domain_exists def timeline(app, user, args): - items = api.timeline_home(app, user) + # Make sure tag, list and public are not used simultaneously + if len([arg for arg in [args.tag, args.list, args.public] if arg]) > 1: + raise ConsoleError("Only one of --public, --tag, or --list can be used at one time.") + + if args.local and not (args.public or args.tag): + raise ConsoleError("The --local option is only valid alongside --public or --tag.") + + if args.public: + items = api.timeline_public(app, user, local=args.local) + elif args.tag: + items = api.timeline_tag(app, user, args.tag, local=args.local) + elif args.list: + items = api.timeline_list(app, user, args.list) + else: + items = api.timeline_home(app, user) + print_timeline(items) diff --git a/toot/console.py b/toot/console.py index 8613775..f829985 100644 --- a/toot/console.py +++ b/toot/console.py @@ -131,8 +131,27 @@ READ_COMMANDS = [ ), Command( name="timeline", - description="Show recent items in your public timeline", - arguments=[], + description="Show recent items in a timeline (home by default)", + arguments=[ + (["-p", "--public"], { + "action": "store_true", + "default": False, + "help": "Show public timeline.", + }), + (["-t", "--tag"], { + "type": str, + "help": "Show timeline for given hashtag.", + }), + (["-i", "--list"], { + "type": int, + "help": "Show timeline for given list ID.", + }), + (["-l", "--local"], { + "action": "store_true", + "default": False, + "help": "Show only statuses from local instance (public and tag timelines only).", + }), + ], require_auth=True, ), Command( diff --git a/toot/output.py b/toot/output.py index 536cba2..6d17fab 100644 --- a/toot/output.py +++ b/toot/output.py @@ -9,7 +9,7 @@ from itertools import chain from itertools import zip_longest from textwrap import wrap, TextWrapper -from toot.utils import format_content, get_text +from toot.utils import format_content, get_text, trunc START_CODES = { 'red': '\033[31m', @@ -124,33 +124,34 @@ def print_timeline(items): return chain(*[wrapper.wrap(l) for l in text.split("\n")]) def timeline_rows(item): - name = item['name'] + display_name = item['account']['display_name'] + username = "@" + item['account']['username'] time = item['time'].strftime('%Y-%m-%d %H:%M%Z') - left_column = [name, time] - if 'reblogged' in item: - left_column.append(item['reblogged']) + left_column = [display_name] + if display_name != username: + left_column.append(username) + left_column.append(time) + if item['reblogged']: + left_column.append("Reblogged @{}".format(item['reblogged'])) - text = item['text'] - - right_column = wrap_text(text, 80) + right_column = wrap_text(item['text'], 80) return zip_longest(left_column, right_column, fillvalue="") for left, right in timeline_rows(item): - print_out("{:30} │ {}".format(left, right)) + print_out("{:30} │ {}".format(trunc(left, 30), right)) def _parse_item(item): content = item['reblog']['content'] if item['reblog'] else item['content'] - reblogged = item['reblog']['account']['username'] if item['reblog'] else "" + reblogged = item['reblog']['account']['username'] if item['reblog'] else None - name = item['account']['display_name'] + " @" + item['account']['username'] soup = BeautifulSoup(content, "html.parser") text = soup.get_text().replace(''', "'") time = datetime.strptime(item['created_at'], "%Y-%m-%dT%H:%M:%S.%fZ") return { - "name": name, + "account": item['account'], "text": text, "time": time, "reblogged": reblogged,