Introduction
Introduction Statistics Contact Development Disclaimer Help
Added follow and unfollow commands - toot - Unnamed repository; edit this file …
Log
Files
Refs
LICENSE
---
commit a493da5c84cafd717500f06708c5b0450501d932
parent 64d46955e2b827c406f503cabc1be8b6169e72dd
Author: Ivan Habunek <[email protected]>
Date: Sun, 16 Apr 2017 17:15:05 +0200
Added follow and unfollow commands
Diffstat:
README.rst | 2 ++
tests/test_console.py | 113 ++++++++++++++++++++++++++++---
tests/utils.py | 3 ++-
toot/api.py | 40 +++++++++++++++++++++++++++++--
toot/console.py | 77 ++++++++++++++++++++++++++-----
5 files changed, 211 insertions(+), 24 deletions(-)
---
diff --git a/README.rst b/README.rst
@@ -39,6 +39,8 @@ Running ``toot <command> -h`` shows the documentation for the…
``toot post`` Post a status to your timeline.
``toot search`` Search for accounts or hashtags.
``toot timeline`` Display recent items in your public timeline.
+ ``toot follow`` Follow an account.
+ ``toot unfollow`` Unfollow an account.
=================== =========================================================…
Authentication
diff --git a/tests/test_console.py b/tests/test_console.py
@@ -2,8 +2,7 @@
import pytest
import requests
-from toot import User, App
-from toot.console import print_usage, cmd_post_status, cmd_timeline, cmd_uploa…
+from toot import console, User, App
from tests.utils import MockResponse
@@ -12,7 +11,7 @@ user = User('[email protected]', 'xxx')
def test_print_usagecap(capsys):
- print_usage()
+ console.print_usage()
out, err = capsys.readouterr()
assert "toot - interact with Mastodon from the command line" in out
@@ -36,7 +35,7 @@ def test_post_status_defaults(monkeypatch, capsys):
monkeypatch.setattr(requests.Request, 'prepare', mock_prepare)
monkeypatch.setattr(requests.Session, 'send', mock_send)
- cmd_post_status(app, user, ['Hello world'])
+ console.cmd_post_status(app, user, ['Hello world'])
out, err = capsys.readouterr()
assert "Toot posted" in out
@@ -63,7 +62,7 @@ def test_post_status_with_options(monkeypatch, capsys):
args = ['"Hello world"', '--visibility', 'unlisted']
- cmd_post_status(app, user, args)
+ console.cmd_post_status(app, user, args)
out, err = capsys.readouterr()
assert "Toot posted" in out
@@ -73,7 +72,7 @@ def test_post_status_invalid_visibility(monkeypatch, capsys):
args = ['Hello world', '--visibility', 'foo']
with pytest.raises(SystemExit):
- cmd_post_status(app, user, args)
+ console.cmd_post_status(app, user, args)
out, err = capsys.readouterr()
assert "invalid visibility value: 'foo'" in err
@@ -83,7 +82,7 @@ def test_post_status_invalid_media(monkeypatch, capsys):
args = ['Hello world', '--media', 'does_not_exist.jpg']
with pytest.raises(SystemExit):
- cmd_post_status(app, user, args)
+ console.cmd_post_status(app, user, args)
out, err = capsys.readouterr()
assert "can't open 'does_not_exist.jpg'" in err
@@ -107,7 +106,7 @@ def test_timeline(monkeypatch, capsys):
monkeypatch.setattr(requests, 'get', mock_get)
- cmd_timeline(app, user, [])
+ console.cmd_timeline(app, user, [])
out, err = capsys.readouterr()
assert "The computer can't tell you the emotional story." in out
@@ -133,7 +132,7 @@ def test_upload(monkeypatch, capsys):
monkeypatch.setattr(requests.Request, 'prepare', mock_prepare)
monkeypatch.setattr(requests.Session, 'send', mock_send)
- cmd_upload(app, user, [__file__])
+ console.cmd_upload(app, user, [__file__])
out, err = capsys.readouterr()
assert "Uploading media" in out
@@ -163,10 +162,104 @@ def test_search(monkeypatch, capsys):
monkeypatch.setattr(requests, 'get', mock_get)
- cmd_search(app, user, ['freddy'])
+ console.cmd_search(app, user, ['freddy'])
out, err = capsys.readouterr()
assert "Hashtags:\n\033[32m#foo\033[0m, \033[32m#bar\033[0m, \033[32m#baz\…
assert "Accounts:" in out
assert "\033[32m@thequeen\033[0m Freddy Mercury" in out
assert "\033[32m@[email protected]\033[0m Mercury Freddy" in out
+
+
+def test_follow(monkeypatch, capsys):
+ def mock_get(url, params, headers):
+ assert url == 'https://habunek.com/api/v1/search'
+ assert params == {'q': 'blixa', 'resolve': False}
+ assert headers == {'Authorization': 'Bearer xxx'}
+
+ return MockResponse({
+ 'accounts': [
+ {'id': 123, 'acct': '[email protected]'},
+ {'id': 321, 'acct': 'blixa'},
+ ]
+ })
+
+ def mock_prepare(request):
+ assert request.url == 'https://habunek.com/api/v1/accounts/321/follow'
+
+ def mock_send(*args, **kwargs):
+ return MockResponse()
+
+ monkeypatch.setattr(requests.Request, 'prepare', mock_prepare)
+ monkeypatch.setattr(requests.Session, 'send', mock_send)
+ monkeypatch.setattr(requests, 'get', mock_get)
+
+ console.cmd_follow(app, user, ['blixa'])
+
+ out, err = capsys.readouterr()
+ assert "You are now following blixa" in out
+
+
+def test_follow_not_found(monkeypatch, capsys):
+ def mock_get(url, params, headers):
+ assert url == 'https://habunek.com/api/v1/search'
+ assert params == {'q': 'blixa', 'resolve': False}
+ assert headers == {'Authorization': 'Bearer xxx'}
+
+ return MockResponse({
+ 'accounts': []
+ })
+
+ monkeypatch.setattr(requests, 'get', mock_get)
+
+ console.cmd_follow(app, user, ['blixa'])
+
+ out, err = capsys.readouterr()
+ assert "Account not found" in err
+
+
+def test_unfollow(monkeypatch, capsys):
+ def mock_get(url, params, headers):
+ assert url == 'https://habunek.com/api/v1/search'
+ assert params == {'q': 'blixa', 'resolve': False}
+ assert headers == {'Authorization': 'Bearer xxx'}
+
+ return MockResponse({
+ 'accounts': [
+ {'id': 123, 'acct': '[email protected]'},
+ {'id': 321, 'acct': 'blixa'},
+ ]
+ })
+
+ def mock_prepare(request):
+ assert request.url == 'https://habunek.com/api/v1/accounts/321/unfollo…
+
+ def mock_send(*args, **kwargs):
+ return MockResponse()
+
+ monkeypatch.setattr(requests.Request, 'prepare', mock_prepare)
+ monkeypatch.setattr(requests.Session, 'send', mock_send)
+ monkeypatch.setattr(requests, 'get', mock_get)
+
+ console.cmd_unfollow(app, user, ['blixa'])
+
+ out, err = capsys.readouterr()
+ assert "You are no longer following blixa" in out
+
+
+def test_unfollow_not_found(monkeypatch, capsys):
+ def mock_get(url, params, headers):
+ assert url == 'https://habunek.com/api/v1/search'
+ assert params == {'q': 'blixa', 'resolve': False}
+ assert headers == {'Authorization': 'Bearer xxx'}
+
+ return MockResponse({
+ 'accounts': []
+ })
+
+ monkeypatch.setattr(requests, 'get', mock_get)
+
+ console.cmd_unfollow(app, user, ['blixa'])
+
+ out, err = capsys.readouterr()
+ assert "Account not found" in err
diff --git a/tests/utils.py b/tests/utils.py
@@ -1,6 +1,7 @@
class MockResponse:
- def __init__(self, response_data={}):
+ def __init__(self, response_data={}, ok=True):
+ self.ok = ok
self.response_data = response_data
def raise_for_status(self):
diff --git a/toot/api.py b/toot/api.py
@@ -4,6 +4,7 @@ import logging
import requests
from requests import Request, Session
+from future.moves.urllib.parse import quote_plus
from toot import App, User, CLIENT_NAME, CLIENT_WEBSITE
@@ -12,6 +13,14 @@ SCOPES = 'read write follow'
logger = logging.getLogger('toot')
+class ApiError(Exception):
+ pass
+
+
+class NotFoundError(ApiError):
+ pass
+
+
def _log_request(request, prepared_request):
logger.debug(">>> \033[32m{} {}\033[0m".format(request.method, request.url…
logger.debug(">>> DATA: \033[33m{}\033[0m".format(request.data))
@@ -20,8 +29,12 @@ def _log_request(request, prepared_request):
def _log_response(response):
- logger.debug("<<< \033[32m{}\033[0m".format(response))
- logger.debug("<<< \033[33m{}\033[0m".format(response.json()))
+ if response.ok:
+ logger.debug("<<< \033[32m{}\033[0m".format(response))
+ logger.debug("<<< \033[33m{}\033[0m".format(response.json()))
+ else:
+ logger.debug("<<< \033[31m{}\033[0m".format(response))
+ logger.debug("<<< \033[31m{}\033[0m".format(response.content))
def _get(app, user, url, params=None):
@@ -48,6 +61,17 @@ def _post(app, user, url, data=None, files=None):
_log_response(response)
+ if not response.ok:
+ try:
+ error = response.json()['error']
+ except:
+ error = "Unknown error"
+
+ if response.status_code == 404:
+ raise NotFoundError(error)
+
+ raise ApiError(error)
+
response.raise_for_status()
return response.json()
@@ -115,3 +139,15 @@ def search(app, user, query, resolve):
'q': query,
'resolve': resolve,
})
+
+
+def follow(app, user, account):
+ url = '/api/v1/accounts/%d/follow' % account
+
+ return _post(app, user, url)
+
+
+def unfollow(app, user, account):
+ url = '/api/v1/accounts/%d/unfollow' % account
+
+ return _post(app, user, url)
diff --git a/toot/console.py b/toot/console.py
@@ -15,8 +15,8 @@ from itertools import chain
from argparse import ArgumentParser, FileType
from textwrap import TextWrapper
-from toot import DEFAULT_INSTANCE
-from toot.api import create_app, login, post_status, timeline_home, upload_med…
+from toot import api, DEFAULT_INSTANCE
+from toot.api import ApiError
from toot.config import save_user, load_user, load_app, save_app, CONFIG_APP_F…
@@ -49,7 +49,7 @@ def create_app_interactive():
print("Registering application with %s" % green(base_url))
try:
- app = create_app(base_url)
+ app = api.create_app(base_url)
except:
raise ConsoleError("Failed authenticating application. Did you enter a…
@@ -66,7 +66,7 @@ def login_interactive(app):
print("Authenticating...")
try:
- user = login(app, email, password)
+ user = api.login(app, email, password)
except:
raise ConsoleError("Login failed")
@@ -86,6 +86,8 @@ def print_usage():
print(" toot post <msg> - toot a new post to your timeline")
print(" toot search - search for accounts or hashtags")
print(" toot timeline - shows your public timeline")
+ print(" toot follow - follow an account")
+ print(" toot unfollow - unfollow an account")
print("")
print("To get help for each command run:")
print(" toot <command> --help")
@@ -140,7 +142,7 @@ def cmd_timeline(app, user, args):
args = parser.parse_args(args)
- items = timeline_home(app, user)
+ items = api.timeline_home(app, user)
parsed_items = [parse_timeline(t) for t in items]
print("─" * 31 + "┬" + "─" * 88)
@@ -174,7 +176,7 @@ def cmd_post_status(app, user, args):
else:
media_ids = None
- response = post_status(app, user, args.text, media_ids=media_ids, visibili…
+ response = api.post_status(app, user, args.text, media_ids=media_ids, visi…
print("Toot posted: " + green(response.get('url')))
@@ -194,11 +196,11 @@ def cmd_auth(app, user, args):
print("You are not logged in")
-def cmd_login():
+def cmd_login(args):
parser = ArgumentParser(prog="toot login",
description="Log into a Mastodon instance",
epilog="https://github.com/ihabunek/toot")
- parser.parse_args()
+ parser.parse_args(args)
app = create_app_interactive()
user = login_interactive(app)
@@ -264,7 +266,7 @@ def cmd_search(app, user, args):
args = parser.parse_args(args)
- response = search(app, user, args.query, args.resolve)
+ response = api.search(app, user, args.query, args.resolve)
_print_accounts(response['accounts'])
_print_hashtags(response['hashtags'])
@@ -272,7 +274,52 @@ def cmd_search(app, user, args):
def do_upload(app, user, file):
print("Uploading media: {}".format(green(file.name)))
- return upload_media(app, user, file)
+ return api.upload_media(app, user, file)
+
+
+def _find_account(app, user, account_name):
+ """For a given account name, returns the Account object or None if not fou…
+ response = api.search(app, user, account_name, False)
+
+ for account in response['accounts']:
+ if account['acct'] == account_name:
+ return account
+
+
+def cmd_follow(app, user, args):
+ parser = ArgumentParser(prog="toot follow",
+ description="Follow an account",
+ epilog="https://github.com/ihabunek/toot")
+ parser.add_argument("account", help="Account name, e.g. 'Gargron' or 'poly…
+ args = parser.parse_args(args)
+
+ account = _find_account(app, user, args.account)
+
+ if not account:
+ print_error("Account not found")
+ return
+
+ api.follow(app, user, account['id'])
+
+ print(green(u"✓ You are now following %s" % args.account))
+
+
+def cmd_unfollow(app, user, args):
+ parser = ArgumentParser(prog="toot unfollow",
+ description="Unfollow an account",
+ epilog="https://github.com/ihabunek/toot")
+ parser.add_argument("account", help="Account name, e.g. 'Gargron' or 'poly…
+ args = parser.parse_args(args)
+
+ account = _find_account(app, user, args.account)
+
+ if not account:
+ print_error("Account not found")
+ return
+
+ api.unfollow(app, user, account['id'])
+
+ print(green(u"✓ You are no longer following %s" % args.account))
def run_command(command, args):
@@ -281,7 +328,7 @@ def run_command(command, args):
# Commands which can run when not logged in
if command == 'login':
- return cmd_login()
+ return cmd_login(args)
if command == 'auth':
return cmd_auth(app, user, args)
@@ -307,6 +354,12 @@ def run_command(command, args):
if command == 'search':
return cmd_search(app, user, args)
+ if command == 'follow':
+ return cmd_follow(app, user, args)
+
+ if command == 'unfollow':
+ return cmd_unfollow(app, user, args)
+
print_error("Unknown command '{}'\n".format(command))
print_usage()
@@ -325,3 +378,5 @@ def main():
run_command(command, args)
except ConsoleError as e:
print_error(str(e))
+ except ApiError as e:
+ print_error(str(e))
You are viewing proxied material from vernunftzentrum.de. The copyright of proxied material belongs to its original authors. Any comments or complaints in relation to proxied material should be directed to the original authors of the content concerned. Please see the disclaimer for more details.