Add --json option to account commands

json
Ivan Habunek 2023-11-21 18:16:23 +01:00
rodzic 016ae25569
commit 443f9445b1
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: F5F0623FF5EBCB3D
9 zmienionych plików z 329 dodań i 188 usunięć

Wyświetl plik

@ -13,6 +13,7 @@ export TOOT_TEST_DATABASE_DSN="dbname=mastodon_development"
```
"""
import json
import re
import os
import psycopg2
@ -94,6 +95,16 @@ def friend(app):
return register_account(app)
@pytest.fixture(scope="session")
def user_id(app, user):
return api.find_account(app, user, user.username)["id"]
@pytest.fixture(scope="session")
def friend_id(app, user, friend):
return api.find_account(app, user, friend.username)["id"]
@pytest.fixture
def run(app, user, capsys):
def _run(command, *params, as_user=None):
@ -110,6 +121,14 @@ def run(app, user, capsys):
return _run
@pytest.fixture
def run_json(run):
def _run_json(command, *params):
out = run(command, *params)
return json.loads(out)
return _run_json
@pytest.fixture
def run_anon(capsys):
def _run(command, *params):

Wyświetl plik

@ -1,21 +1,22 @@
import json
from toot.entities import Account, from_dict
from toot import App, User, api
from toot.entities import Account, Relationship, from_dict
def test_whoami(user, run):
def test_whoami(user: User, run):
out = run("whoami")
# TODO: test other fields once updating account is supported
assert f"@{user.username}" in out
def test_whoami_json(user, run):
def test_whoami_json(user: User, run):
out = run("whoami", "--json")
account = from_dict(Account, json.loads(out))
assert account.username == user.username
def test_whois(app, friend, run):
def test_whois(app: App, friend: User, run):
variants = [
friend.username,
f"@{friend.username}",
@ -26,3 +27,183 @@ def test_whois(app, friend, run):
for username in variants:
out = run("whois", username)
assert f"@{friend.username}" in out
def test_following(app: App, user: User, friend: User, friend_id, run):
# Make sure we're not initally following friend
api.unfollow(app, user, friend_id)
out = run("following", user.username)
assert out == ""
out = run("follow", friend.username)
assert out == f"✓ You are now following {friend.username}"
out = run("following", user.username)
assert friend.username in out
out = run("unfollow", friend.username)
assert out == f"✓ You are no longer following {friend.username}"
out = run("following", user.username)
assert out == ""
def test_following_case_insensitive(user: User, friend: User, run):
assert friend.username != friend.username.upper()
out = run("follow", friend.username.upper())
assert out == f"✓ You are now following {friend.username.upper()}"
def test_following_not_found(run):
out = run("follow", "bananaman")
assert out == "Account not found"
out = run("unfollow", "bananaman")
assert out == "Account not found"
def test_following_json(app: App, user: User, friend: User, user_id, friend_id, run_json):
# Make sure we're not initally following friend
api.unfollow(app, user, friend_id)
result = run_json("following", user.username, "--json")
assert result == []
result = run_json("followers", friend.username, "--json")
assert result == []
result = run_json("follow", friend.username, "--json")
relationship = from_dict(Relationship, result)
assert relationship.id == friend_id
assert relationship.following is True
[result] = run_json("following", user.username, "--json")
relationship = from_dict(Relationship, result)
assert relationship.id == friend_id
[result] = run_json("followers", friend.username, "--json")
assert result["id"] == user_id
result = run_json("unfollow", friend.username, "--json")
assert result["id"] == friend_id
assert result["following"] is False
result = run_json("following", user.username, "--json")
assert result == []
result = run_json("followers", friend.username, "--json")
assert result == []
def test_mute(app, user, friend, friend_id, run):
# Make sure we're not initially muting friend
api.unmute(app, user, friend_id)
out = run("muted")
assert out == "No accounts muted"
out = run("mute", friend.username)
assert out == f"✓ You have muted {friend.username}"
out = run("muted")
assert friend.username in out
out = run("unmute", friend.username)
assert out == f"{friend.username} is no longer muted"
out = run("muted")
assert out == "No accounts muted"
def test_mute_case_insensitive(friend: User, run):
out = run("mute", friend.username.upper())
assert out == f"✓ You have muted {friend.username.upper()}"
def test_mute_not_found(run):
out = run("mute", "doesnotexistperson")
assert out == f"Account not found"
out = run("unmute", "doesnotexistperson")
assert out == f"Account not found"
def test_mute_json(app: App, user: User, friend: User, run_json, friend_id):
# Make sure we're not initially muting friend
api.unmute(app, user, friend_id)
result = run_json("muted", "--json")
assert result == []
result = run_json("mute", friend.username, "--json")
relationship = from_dict(Relationship, result)
assert relationship.id == friend_id
assert relationship.muting is True
[result] = run_json("muted", "--json")
account = from_dict(Account, result)
assert account.id == friend_id
result = run_json("unmute", friend.username, "--json")
relationship = from_dict(Relationship, result)
assert relationship.id == friend_id
assert relationship.muting is False
result = run_json("muted", "--json")
assert result == []
def test_block(app, user, friend, friend_id, run):
# Make sure we're not initially blocking friend
api.unblock(app, user, friend_id)
out = run("blocked")
assert out == "No accounts blocked"
out = run("block", friend.username)
assert out == f"✓ You are now blocking {friend.username}"
out = run("blocked")
assert friend.username in out
out = run("unblock", friend.username)
assert out == f"{friend.username} is no longer blocked"
out = run("blocked")
assert out == "No accounts blocked"
def test_block_case_insensitive(friend: User, run):
out = run("block", friend.username.upper())
assert out == f"✓ You are now blocking {friend.username.upper()}"
def test_block_not_found(run):
out = run("block", "doesnotexistperson")
assert out == f"Account not found"
def test_block_json(app: App, user: User, friend: User, run_json, friend_id):
# Make sure we're not initially blocking friend
api.unblock(app, user, friend_id)
result = run_json("blocked", "--json")
assert result == []
result = run_json("block", friend.username, "--json")
relationship = from_dict(Relationship, result)
assert relationship.id == friend_id
assert relationship.blocking is True
[result] = run_json("blocked", "--json")
account = from_dict(Account, result)
assert account.id == friend_id
result = run_json("unblock", friend.username, "--json")
relationship = from_dict(Relationship, result)
assert relationship.id == friend_id
assert relationship.blocking is False
result = run_json("blocked", "--json")
assert result == []

Wyświetl plik

@ -1,8 +1,10 @@
import json
from pprint import pprint
import pytest
import re
from toot import api
from toot.entities import Account, from_dict_list
from toot.exceptions import ConsoleError
from uuid import uuid4
@ -58,6 +60,12 @@ def test_search_account(friend, run):
assert out == f"Accounts:\n* @{friend.username}"
def test_search_account_json(friend, run_json):
out = run_json("search", friend.username, "--json")
[account] = from_dict_list(Account, out["accounts"])
assert account.acct == friend.username
def test_search_hashtag(app, user, run):
api.post_status(app, user, "#hashtag_x")
api.post_status(app, user, "#hashtag_y")
@ -67,6 +75,19 @@ def test_search_hashtag(app, user, run):
assert out == "Hashtags:\n#hashtag_x, #hashtag_y, #hashtag_z"
def test_search_hashtag_json(app, user, run_json):
api.post_status(app, user, "#hashtag_x")
api.post_status(app, user, "#hashtag_y")
api.post_status(app, user, "#hashtag_z")
out = run_json("search", "#hashtag", "--json")
[h1, h2, h3] = sorted(out["hashtags"], key=lambda h: h["name"])
assert h1["name"] == "hashtag_x"
assert h2["name"] == "hashtag_y"
assert h3["name"] == "hashtag_z"
def test_tags(run, base_url):
out = run("tags_followed")
assert out == "You're not following any hashtags."

Wyświetl plik

@ -218,136 +218,6 @@ def test_upload(mock_post, capsys):
assert __file__ in out
@mock.patch('toot.http.get')
def test_search(mock_get, capsys):
mock_get.return_value = MockResponse({
'hashtags': [
{
'history': [],
'name': 'foo',
'url': 'https://mastodon.social/tags/foo'
},
{
'history': [],
'name': 'bar',
'url': 'https://mastodon.social/tags/bar'
},
{
'history': [],
'name': 'baz',
'url': 'https://mastodon.social/tags/baz'
},
],
'accounts': [{
'acct': 'thequeen',
'display_name': 'Freddy Mercury'
}, {
'acct': 'thequeen@other.instance',
'display_name': 'Mercury Freddy'
}],
'statuses': [],
})
console.run_command(app, user, 'search', ['freddy'])
mock_get.assert_called_once_with(app, user, '/api/v2/search', {
'q': 'freddy',
'type': None,
'resolve': False,
})
out, err = capsys.readouterr()
out = uncolorize(out)
assert "Hashtags:\n#foo, #bar, #baz" in out
assert "Accounts:" in out
assert "@thequeen Freddy Mercury" in out
assert "@thequeen@other.instance Mercury Freddy" in out
@mock.patch('toot.http.post')
@mock.patch('toot.http.get')
def test_follow(mock_get, mock_post, capsys):
mock_get.return_value = MockResponse({
"accounts": [
{"id": 123, "acct": "blixa@other.acc"},
{"id": 321, "acct": "blixa"},
]
})
mock_post.return_value = MockResponse()
console.run_command(app, user, 'follow', ['blixa'])
mock_get.assert_called_once_with(app, user, '/api/v2/search', {'q': 'blixa', 'type': 'accounts', 'resolve': True})
mock_post.assert_called_once_with(app, user, '/api/v1/accounts/321/follow')
out, err = capsys.readouterr()
assert "You are now following blixa" in out
@mock.patch('toot.http.post')
@mock.patch('toot.http.get')
def test_follow_case_insensitive(mock_get, mock_post, capsys):
mock_get.return_value = MockResponse({
"accounts": [
{"id": 123, "acct": "blixa@other.acc"},
{"id": 321, "acct": "blixa"},
]
})
mock_post.return_value = MockResponse()
console.run_command(app, user, 'follow', ['bLiXa@oThEr.aCc'])
mock_get.assert_called_once_with(app, user, '/api/v2/search', {'q': 'bLiXa@oThEr.aCc', 'type': 'accounts', 'resolve': True})
mock_post.assert_called_once_with(app, user, '/api/v1/accounts/123/follow')
out, err = capsys.readouterr()
assert "You are now following bLiXa@oThEr.aCc" in out
@mock.patch('toot.http.get')
def test_follow_not_found(mock_get, capsys):
mock_get.return_value = MockResponse({"accounts": []})
with pytest.raises(ConsoleError) as ex:
console.run_command(app, user, 'follow', ['blixa'])
mock_get.assert_called_once_with(app, user, '/api/v2/search', {'q': 'blixa', 'type': 'accounts', 'resolve': True})
assert "Account not found" == str(ex.value)
@mock.patch('toot.http.post')
@mock.patch('toot.http.get')
def test_unfollow(mock_get, mock_post, capsys):
mock_get.return_value = MockResponse({
"accounts": [
{'id': 123, 'acct': 'blixa@other.acc'},
{'id': 321, 'acct': 'blixa'},
]
})
mock_post.return_value = MockResponse()
console.run_command(app, user, 'unfollow', ['blixa'])
mock_get.assert_called_once_with(app, user, '/api/v2/search', {'q': 'blixa', 'type': 'accounts', 'resolve': True})
mock_post.assert_called_once_with(app, user, '/api/v1/accounts/321/unfollow')
out, err = capsys.readouterr()
assert "You are no longer following blixa" in out
@mock.patch('toot.http.get')
def test_unfollow_not_found(mock_get, capsys):
mock_get.return_value = MockResponse({"accounts": []})
with pytest.raises(ConsoleError) as ex:
console.run_command(app, user, 'unfollow', ['blixa'])
mock_get.assert_called_once_with(app, user, '/api/v2/search', {'q': 'blixa', 'type': 'accounts', 'resolve': True})
assert "Account not found" == str(ex.value)
@mock.patch('toot.http.get')
def test_whoami(mock_get, capsys):
mock_get.return_value = MockResponse({

Wyświetl plik

@ -38,9 +38,9 @@ def find_account(app, user, account_name):
raise ConsoleError("Account not found")
def _account_action(app, user, account, action):
def _account_action(app, user, account, action) -> Response:
url = f"/api/v1/accounts/{account}/{action}"
return http.post(app, user, url).json()
return http.post(app, user, url)
def _status_action(app, user, status_id, action, data=None) -> Response:

Wyświetl plik

@ -5,13 +5,14 @@ import platform
from datetime import datetime, timedelta, timezone
from time import sleep, time
from toot import api, config, __version__
from toot.auth import login_interactive, login_browser_interactive, create_app_interactive
from toot.entities import Account, Instance, Notification, Status, from_dict
from toot.exceptions import ApiError, ConsoleError
from toot.output import (print_lists, print_out, print_instance, print_account, print_acct_list,
print_search_results, print_status, print_table, print_timeline, print_notifications, print_tag_list,
print_list_accounts, print_user_list)
print_search_results, print_status, print_table, print_timeline, print_notifications,
print_tag_list, print_list_accounts, print_user_list)
from toot.utils import args_get_instance, delete_tmp_status_file, editor_input, multiline_input, EOF_KEY
from toot.utils.datetime import parse_datetime
@ -418,26 +419,38 @@ def _do_upload(app, user, file, description, thumbnail):
def follow(app, user, args):
account = api.find_account(app, user, args.account)
api.follow(app, user, account['id'])
print_out("<green>✓ You are now following {}</green>".format(args.account))
response = api.follow(app, user, account["id"])
if args.json:
print(response.text)
else:
print_out(f"<green>✓ You are now following {args.account}</green>")
def unfollow(app, user, args):
account = api.find_account(app, user, args.account)
api.unfollow(app, user, account['id'])
print_out("<green>✓ You are no longer following {}</green>".format(args.account))
response = api.unfollow(app, user, account["id"])
if args.json:
print(response.text)
else:
print_out(f"<green>✓ You are no longer following {args.account}</green>")
def following(app, user, args):
account = api.find_account(app, user, args.account)
response = api.following(app, user, account['id'])
print_acct_list(response)
accounts = api.following(app, user, account["id"])
if args.json:
print(json.dumps(accounts))
else:
print_acct_list(accounts)
def followers(app, user, args):
account = api.find_account(app, user, args.account)
response = api.followers(app, user, account['id'])
print_acct_list(response)
accounts = api.followers(app, user, account["id"])
if args.json:
print(json.dumps(accounts))
else:
print_acct_list(accounts)
def tags_follow(app, user, args):
@ -524,36 +537,62 @@ def _get_list_id(app, user, args):
def mute(app, user, args):
account = api.find_account(app, user, args.account)
api.mute(app, user, account['id'])
print_out("<green>✓ You have muted {}</green>".format(args.account))
response = api.mute(app, user, account['id'])
if args.json:
print(response.text)
else:
print_out("<green>✓ You have muted {}</green>".format(args.account))
def unmute(app, user, args):
account = api.find_account(app, user, args.account)
api.unmute(app, user, account['id'])
print_out("<green>✓ {} is no longer muted</green>".format(args.account))
response = api.unmute(app, user, account['id'])
if args.json:
print(response.text)
else:
print_out("<green>✓ {} is no longer muted</green>".format(args.account))
def muted(app, user, args):
response = api.muted(app, user)
print_acct_list(response)
if args.json:
print(json.dumps(response))
else:
if len(response) > 0:
print("Muted accounts:")
print_acct_list(response)
else:
print("No accounts muted")
def block(app, user, args):
account = api.find_account(app, user, args.account)
api.block(app, user, account['id'])
print_out("<green>✓ You are now blocking {}</green>".format(args.account))
response = api.block(app, user, account['id'])
if args.json:
print(response.text)
else:
print_out("<green>✓ You are now blocking {}</green>".format(args.account))
def unblock(app, user, args):
account = api.find_account(app, user, args.account)
api.unblock(app, user, account['id'])
print_out("<green>✓ {} is no longer blocked</green>".format(args.account))
response = api.unblock(app, user, account['id'])
if args.json:
print(response.text)
else:
print_out("<green>✓ {} is no longer blocked</green>".format(args.account))
def blocked(app, user, args):
response = api.blocked(app, user)
print_acct_list(response)
if args.json:
print(json.dumps(response))
else:
if len(response) > 0:
print("Blocked accounts:")
print_acct_list(response)
else:
print("No accounts blocked")
def whoami(app, user, args):

Wyświetl plik

@ -671,77 +671,61 @@ ACCOUNTS_COMMANDS = [
Command(
name="follow",
description="Follow an account",
arguments=[
account_arg,
],
arguments=[account_arg, json_arg],
require_auth=True,
),
Command(
name="unfollow",
description="Unfollow an account",
arguments=[
account_arg,
],
arguments=[account_arg, json_arg],
require_auth=True,
),
Command(
name="following",
description="List accounts followed by the given account",
arguments=[
account_arg,
],
arguments=[account_arg, json_arg],
require_auth=True,
),
Command(
name="followers",
description="List accounts following the given account",
arguments=[
account_arg,
],
arguments=[account_arg, json_arg],
require_auth=True,
),
Command(
name="mute",
description="Mute an account",
arguments=[
account_arg,
],
arguments=[account_arg, json_arg],
require_auth=True,
),
Command(
name="unmute",
description="Unmute an account",
arguments=[
account_arg,
],
arguments=[account_arg, json_arg],
require_auth=True,
),
Command(
name="muted",
description="List muted accounts",
arguments=[],
arguments=[json_arg],
require_auth=True,
),
Command(
name="block",
description="Block an account",
arguments=[
account_arg,
],
arguments=[account_arg, json_arg],
require_auth=True,
),
Command(
name="unblock",
description="Unblock an account",
arguments=[
account_arg,
],
arguments=[account_arg, json_arg],
require_auth=True,
),
Command(
name="blocked",
description="List blocked accounts",
arguments=[],
arguments=[json_arg],
require_auth=True,
),
]

Wyświetl plik

@ -384,6 +384,29 @@ class Instance:
rules: List[Rule]
@dataclass
class Relationship:
"""
Represents the relationship between accounts, such as following / blocking /
muting / etc.
https://docs.joinmastodon.org/entities/Relationship/
"""
id: str
following: bool
showing_reblogs: bool
notifying: bool
languages: List[str]
followed_by: bool
blocking: bool
blocked_by: bool
muting: bool
muting_notifications: bool
requested: bool
domain_blocking: bool
endorsed: bool
note: str
# Generic data class instance
T = TypeVar("T")
@ -422,6 +445,10 @@ def from_dict(cls: Type[T], data: Dict) -> T:
return cls(**dict(_fields()))
def from_dict_list(cls: Type[T], data: List[Dict]) -> List[T]:
return [from_dict(cls, x) for x in data]
def _get_default_value(field):
if field.default is not dataclasses.MISSING:
return field.default

Wyświetl plik

@ -330,17 +330,17 @@ def take_action(button: Button, self: Account):
action = button.get_label()
if action == "Confirm Follow":
self.relationship = api.follow(self.app, self.user, self.account["id"])
self.relationship = api.follow(self.app, self.user, self.account["id"]).json()
elif action == "Confirm Unfollow":
self.relationship = api.unfollow(self.app, self.user, self.account["id"])
self.relationship = api.unfollow(self.app, self.user, self.account["id"]).json()
elif action == "Confirm Mute":
self.relationship = api.mute(self.app, self.user, self.account["id"])
self.relationship = api.mute(self.app, self.user, self.account["id"]).json()
elif action == "Confirm Unmute":
self.relationship = api.unmute(self.app, self.user, self.account["id"])
self.relationship = api.unmute(self.app, self.user, self.account["id"]).json()
elif action == "Confirm Block":
self.relationship = api.block(self.app, self.user, self.account["id"])
self.relationship = api.block(self.app, self.user, self.account["id"]).json()
elif action == "Confirm Unblock":
self.relationship = api.unblock(self.app, self.user, self.account["id"])
self.relationship = api.unblock(self.app, self.user, self.account["id"]).json()
self.last_action = None
self.setup_listbox()