commit 40a07392274e0f15f9cdce8e6d22ac4cedb6be3a Author: Ivan Habunek Date: Wed Apr 12 16:42:04 2017 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ed9d2c --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.egg-info/ +*.pyc +.cache/ +build/ +dist/ +tmp/ +.pypirc +/.env \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1440700 --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +default : clean dist + +dist : + @echo "\nMaking source" + @echo "-------------" + @python setup.py sdist + + @echo "\nMaking wheel" + @echo "-------------" + @python setup.py bdist_wheel --universal + + @echo "\nDone." + +clean : + rm -rf build dist *.egg-info MANIFEST + +publish : + twine upload dist/* diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..81da31d --- /dev/null +++ b/README.rst @@ -0,0 +1,28 @@ +==== +Toot +==== + +Post to Mastodon social networks from the command line. + + +Installation +------------ + +Install using pip: + +.. code-block:: + + pip install toot + + +Usage +----- + +Currently implements only posting a new status: + + +.. code-block:: + + toot post "Hello world!" + +On first use, will ask you to choose a Mastodon instance and log in. diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..3c6e79c --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal=1 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..0499ae4 --- /dev/null +++ b/setup.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python + +from setuptools import setup + +with open("README.rst") as readme: + long_description = readme.read() + +setup( + name='toot', + version='0.1.0', + description='Interact with Mastodon social networks from the command line.', + long_description=long_description, + author='Ivan Habunek', + author_email='ivan@habunek.com', + url='https://github.com/ihabunek/toot/', + keywords='mastodon toot', + license='MIT', + classifiers=[ + 'Development Status :: 4 - Beta', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + ], + packages=['toot'], + install_requires=[ + 'future' + ], + entry_points={ + 'console_scripts': [ + 'toot=toot.console:main', + ], + } +) diff --git a/toot.py b/toot.py new file mode 100644 index 0000000..26e8f43 --- /dev/null +++ b/toot.py @@ -0,0 +1,28 @@ +from mastodon import Mastodon + +# app = Mastodon.create_app('toot', to_file='app_creds.txt') +# print app + +# mastodon = Mastodon(client_id='app_creds.txt') +# mastodon.log_in('ivan@habunek.com', 'K2oEeDHdMEvCbAnEJjeB18sv', to_file='user_creds.txt') + + +# # Create actual instance +# mastodon = Mastodon( +# client_id='app_creds.txt', +# access_token='user_creds.txt' +# ) + +# mastodon.toot('Testing') + + +# import ConfigParser + +# config = ConfigParser.ConfigParser() +# config.read('auth.ini') + +# print config.get('Auth', 'foo2') + + + + diff --git a/toot/__init__.py b/toot/__init__.py new file mode 100644 index 0000000..84ac429 --- /dev/null +++ b/toot/__init__.py @@ -0,0 +1,58 @@ +import requests + +from collections import namedtuple + +App = namedtuple('App', ['base_url', 'client_id', 'client_secret']) +User = namedtuple('User', ['username', 'access_token']) + +APP_NAME = 'toot' +DEFAULT_INSTANCE = 'mastodon.social' + + +def create_app(base_url): + url = base_url + 'api/v1/apps' + + response = requests.post(url, { + 'client_name': 'toot', + 'redirect_uris': 'urn:ietf:wg:oauth:2.0:oob', + 'scopes': 'read write', + 'website': 'https://github.com/ihabunek/toot', + }) + + response.raise_for_status() + + data = response.json() + client_id = data.get('client_id') + client_secret = data.get('client_secret') + + return App(base_url, client_id, client_secret) + + +def login(app, username, password): + url = app.base_url + 'oauth/token' + + response = requests.post(url, { + 'grant_type': 'password', + 'client_id': app.client_id, + 'client_secret': app.client_secret, + 'username': username, + 'password': password, + 'scope': 'read write', + }) + + response.raise_for_status() + + data = response.json() + access_token = data.get('access_token') + + return User(username, access_token) + + +def post_status(app, user, status): + url = app.base_url + '/api/v1/statuses' + headers = {"Authorization": "Bearer " + user.access_token} + + response = requests.post(url, {'status': status}, headers=headers) + response.raise_for_status() + + return response.json() diff --git a/toot/config.py b/toot/config.py new file mode 100644 index 0000000..9fffb01 --- /dev/null +++ b/toot/config.py @@ -0,0 +1,57 @@ +import os + +from . import User, App + +CONFIG_DIR = os.environ['HOME'] + '/.config/toot/' +CONFIG_APP_FILE = CONFIG_DIR + 'app.cfg' +CONFIG_USER_FILE = CONFIG_DIR + 'user.cfg' + + +def collapse(tuple): + return [v for k, v in tuple.__dict__.items()] + + +def _load(file, tuple_class): + if not os.path.exists(file): + return None + + with open(file, 'r') as f: + lines = f.read().split() + try: + return tuple_class(*lines) + except TypeError: + return None + + +def _save(file, named_tuple): + directory = os.path.dirname(file) + if not os.path.exists(directory): + os.makedirs(directory) + + with open(file, 'w') as f: + values = [v for k, v in named_tuple.__dict__.items()] + return f.write("\n".join(values)) + + +def load_app(): + return _load(CONFIG_APP_FILE, App) + + +def load_user(): + return _load(CONFIG_USER_FILE, User) + + +def save_app(app): + return _save(CONFIG_APP_FILE, app) + + +def save_user(user): + return _save(CONFIG_USER_FILE, user) + + +def delete_app(app): + return os.unlink(CONFIG_APP_FILE) + + +def delete_user(user): + return os.unlink(CONFIG_USER_FILE) diff --git a/toot/console.py b/toot/console.py new file mode 100644 index 0000000..5ab97e5 --- /dev/null +++ b/toot/console.py @@ -0,0 +1,90 @@ +import os +import sys + +from builtins import input +from getpass import getpass + +from .config import save_user, load_user, load_app, save_app, CONFIG_APP_FILE, CONFIG_USER_FILE +from . import create_app, login, post_status, DEFAULT_INSTANCE + + +def green(text): + return "\033[92m{}\033[0m".format(text) + + +def red(text): + return "\033[91m{}\033[0m".format(text) + + +def create_app_interactive(): + instance = input("Choose an instance [{}]: ".format(DEFAULT_INSTANCE)) + if not instance: + instance = DEFAULT_INSTANCE + + base_url = 'https://{}'.format(instance) + + print("Creating app with {}".format(base_url)) + app = create_app(base_url) + + print("App tokens saved to: {}".format(green(CONFIG_APP_FILE))) + save_app(app) + + +def login_interactive(app): + print("\nLog in to " + green(app.base_url)) + email = input('Email: ') + password = getpass('Password: ') + + print("Authenticating...") + user = login(app, email, password) + + save_user(user) + print("User token saved to " + green(CONFIG_USER_FILE)) + + return user + + +def print_usage(): + print("toot - interact with Mastodon from the command line") + print("") + print("Usage:") + print(" toot post \"All your base are belong to us\"") + print("") + print("https://github.com/ihabunek/toot") + + +def cmd_post_status(app, user): + if len(sys.argv) < 3: + print red("No status text given") + return + + response = post_status(app, user, sys.argv[2]) + + print "Toot posted: " + green(response.get('url')) + + +def cmd_auth(app, user): + if app and user: + print("You are logged in") + print("Mastodon instance: " + green(app.base_url)) + print("Username: " + green(user.username)) + else: + print("You are not logged in") + + +def main(): + command = sys.argv[1] if len(sys.argv) > 1 else None + + if os.getenv('TOOT_DEBUG'): + import logging + logging.basicConfig(level=logging.DEBUG) + + app = load_app() or create_app_interactive() + user = load_user() or login_interactive(app) + + if command == 'post': + cmd_post_status(app, user) + elif command == 'auth': + cmd_auth(app, user) + else: + print_usage()