mirror of
https://github.com/nova-r/fediplug.git
synced 2025-02-02 06:07:27 +01:00
commit
1f1829551f
7 changed files with 149 additions and 83 deletions
2
Pipfile
2
Pipfile
|
@ -12,6 +12,8 @@ python-dotenv = "*"
|
|||
click = "*"
|
||||
"e1839a8" = {path = ".", editable = true}
|
||||
appdirs = "*"
|
||||
keyring = "*"
|
||||
"keyrings.alt" = "*"
|
||||
|
||||
[dev-packages]
|
||||
pytest = "*"
|
||||
|
|
32
Pipfile.lock
generated
32
Pipfile.lock
generated
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "add9770cb1b3cd200c03d312f2a977e024c75df453ca2f33094265959e1e7376"
|
||||
"sha256": "823dc15b2e595d044b28df0bb1d28da5da1f2235baa200cf8d36c3eeb988a86d"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
|
@ -133,6 +133,14 @@
|
|||
"editable": true,
|
||||
"path": "."
|
||||
},
|
||||
"entrypoints": {
|
||||
"hashes": [
|
||||
"sha256:10ad569bb245e7e2ba425285b9fa3e8178a0dc92fc53b1e1c553805e15a8825b",
|
||||
"sha256:d2d587dde06f99545fb13a383d2cd336a8ff1f359c5839ce3a64c917d10c029f"
|
||||
],
|
||||
"markers": "python_version >= '2.7'",
|
||||
"version": "==0.2.3"
|
||||
},
|
||||
"http-ece": {
|
||||
"hashes": [
|
||||
"sha256:2f31a0640c31a0c2934ab1e37005dd9a559ae854a16304f9b839e062074106cc"
|
||||
|
@ -146,6 +154,22 @@
|
|||
],
|
||||
"version": "==2.7"
|
||||
},
|
||||
"keyring": {
|
||||
"hashes": [
|
||||
"sha256:6364bb8c233f28538df4928576f4e051229e0451651073ab20b315488da16a58",
|
||||
"sha256:6e01954fd3e404820e1fade262ee661974051551ed08c899ffc5e88bb9df288e"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==13.2.1"
|
||||
},
|
||||
"keyrings.alt": {
|
||||
"hashes": [
|
||||
"sha256:6a00fa799baf1385cf9620bd01bcc815aa56e6970342a567bcfea0c4d21abe5f",
|
||||
"sha256:b59c86b67b9027a86e841a49efc41025bcc3b1b0308629617b66b7011e52db5a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.1"
|
||||
},
|
||||
"lxml": {
|
||||
"hashes": [
|
||||
"sha256:0941f4313208c07734410414d8308812b044fd3fb98573454e3d3a0d2e201f3d",
|
||||
|
@ -238,11 +262,11 @@
|
|||
},
|
||||
"youtube-dl": {
|
||||
"hashes": [
|
||||
"sha256:3300689ccc6cd22b8229e0c9cbb9f2cfcceaa3cef871a8e83c6a456623bb44e5",
|
||||
"sha256:e4f81d38d2c7b8fa6fa069cd0f0096835dcaab91e74a4ca760625b76e0c614b4"
|
||||
"sha256:08d91f0e4dd2d4608a677aa68896b272dcd243a966e161643242203c291b051e",
|
||||
"sha256:cc380b9f2dee75370760fdc8dbfc7ff4eb0b611fa07ab86b80aafc2be5d08a4d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2018.7.10"
|
||||
"version": "==2018.7.21"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
|
|
|
@ -2,13 +2,15 @@
|
|||
|
||||
A Mastodon client that automatically plays your friends' music as they toot links to it.
|
||||
|
||||
## What's new in 2.0
|
||||
## What's new in 2.1
|
||||
|
||||
If you've been using fediplay before, the all-new version 2.0 will be a little different!
|
||||
If you've been using fediplay before, the all-new version 2.1 will be a little different!
|
||||
|
||||
- You now specify the instance you want to stream from on the command line, instead of setting it in the environment. fediplay has been upgraded with the power of [Click](http://click.pocoo.org/) to give it a more modern command-line interface.
|
||||
|
||||
- We use [appdirs](https://github.com/ActiveState/appdirs) to store your credentials in your operating system's user config directory, and downloaded music files in your operating system's user cache directory. If you already have `.secret` files from an earlier version, we'll move and rename them automatically for you.
|
||||
- We use [appdirs](https://pypi.org/project/appdirs/) to keep downloaded music files in your operating system's user cache directory.
|
||||
|
||||
- We use [keyring](https://pypi.org/project/keyring/) to store your client credentials and access token, securely if your operating system supports it. If you already have `.secret` files from an earlier version, we'll migrate them automatically for you.
|
||||
|
||||
Be sure to follow all the instructions, including re-running `pipenv install` to update the installed dependencies.
|
||||
|
||||
|
|
|
@ -1,69 +1,52 @@
|
|||
'''Entry point for command-line interface.'''
|
||||
|
||||
import os
|
||||
path = os.path
|
||||
import sys
|
||||
|
||||
import appdirs
|
||||
import click
|
||||
from mastodon import Mastodon
|
||||
|
||||
from fediplay.dirs import DIRS
|
||||
import fediplay.mastodon as mastodon
|
||||
import fediplay.keyring as keyring
|
||||
|
||||
path = os.path
|
||||
|
||||
dirs = appdirs.AppDirs('fediplay', 'zigg')
|
||||
|
||||
|
||||
def build_usercred_filename(instance):
|
||||
'''Generate a usercred filename from an instance name.'''
|
||||
|
||||
return path.join(dirs.user_config_dir, instance + '.usercred.secret')
|
||||
|
||||
def build_clientcred_filename(instance):
|
||||
'''Generate a clientcred filename from an instance name.'''
|
||||
|
||||
return path.join(dirs.user_config_dir, instance + '.clientcred.secret')
|
||||
|
||||
def ensure_dirs():
|
||||
'''Make sure the application directories exist.'''
|
||||
|
||||
if not path.exists(dirs.user_config_dir):
|
||||
os.makedirs(dirs.user_config_dir)
|
||||
if not path.exists(DIRS.user_config_dir):
|
||||
os.makedirs(DIRS.user_config_dir)
|
||||
|
||||
if not path.exists(dirs.user_cache_dir):
|
||||
os.makedirs(dirs.user_cache_dir)
|
||||
if not path.exists(DIRS.user_cache_dir):
|
||||
os.makedirs(DIRS.user_cache_dir)
|
||||
|
||||
def ensure_usercred(instance):
|
||||
'''Ensure the usercred file exists.'''
|
||||
def get_access_token(instance):
|
||||
'''Ensure the user credential exists.'''
|
||||
|
||||
usercred = build_usercred_filename(instance)
|
||||
keyring.migrate_access_token(instance)
|
||||
|
||||
if path.exists(usercred):
|
||||
return usercred
|
||||
if not keyring.has_credential(instance, keyring.CREDENTIAL_ACCESS_TOKEN):
|
||||
click.echo('user credential for {} does not exist; try `fediplay login`'.format(instance))
|
||||
sys.exit(1)
|
||||
|
||||
if path.exists('usercred.secret'):
|
||||
click.echo('==> Migrating usercred.secret to ' + usercred)
|
||||
os.rename('usercred.secret', usercred)
|
||||
return usercred
|
||||
return keyring.get_credential(instance, keyring.CREDENTIAL_ACCESS_TOKEN)
|
||||
|
||||
click.echo(usercred + ' does not exist; try `fediplay login`')
|
||||
sys.exit(1)
|
||||
def get_client_credentials(instance):
|
||||
'''Ensure the client credentials exist.'''
|
||||
|
||||
def ensure_clientcred(instance):
|
||||
'''Ensure the clientcred file exists.'''
|
||||
keyring.migrate_client_credentials(instance)
|
||||
|
||||
clientcred = build_clientcred_filename(instance)
|
||||
if not (keyring.has_credential(instance, keyring.CREDENTIAL_CLIENT_ID) and
|
||||
keyring.has_credential(instance, keyring.CREDENTIAL_CLIENT_SECRET)):
|
||||
click.echo('client credentials for {} do not exist; try `fediplay register`'.format(instance))
|
||||
sys.exit(1)
|
||||
|
||||
if path.exists(clientcred):
|
||||
return clientcred
|
||||
|
||||
if path.exists('clientcred.secret'):
|
||||
click.echo('==> Migrating clientcred.secret to ' + clientcred)
|
||||
os.rename('clientcred.secret', clientcred)
|
||||
return clientcred
|
||||
|
||||
click.echo(clientcred + ' does not exist; try `fediplay register`')
|
||||
sys.exit(1)
|
||||
return (
|
||||
keyring.get_credential(instance, keyring.CREDENTIAL_CLIENT_ID),
|
||||
keyring.get_credential(instance, keyring.CREDENTIAL_CLIENT_SECRET)
|
||||
)
|
||||
|
||||
@click.group()
|
||||
def cli():
|
||||
|
@ -76,43 +59,30 @@ def cli():
|
|||
def register(instance):
|
||||
'''Register fediplay on your Mastodon instance.'''
|
||||
|
||||
clientcred = build_clientcred_filename(instance)
|
||||
|
||||
if path.exists(clientcred):
|
||||
click.echo(clientcred + ' already exists')
|
||||
sys.exit(1)
|
||||
|
||||
mastodon.register(instance, clientcred)
|
||||
mastodon.register(instance)
|
||||
|
||||
@cli.command()
|
||||
@click.argument('instance')
|
||||
def login(instance):
|
||||
'''Log in to your Mastodon instance.'''
|
||||
|
||||
clientcred = ensure_clientcred(instance)
|
||||
|
||||
usercred = build_usercred_filename(instance)
|
||||
if path.exists(usercred):
|
||||
click.echo(usercred + ' already exists')
|
||||
sys.exit(1)
|
||||
|
||||
client = mastodon.build_client(instance, clientcred)
|
||||
client_id, client_secret = get_client_credentials(instance)
|
||||
|
||||
click.echo('Open this page in your browser and follow the instructions.')
|
||||
click.echo('Paste the code here.')
|
||||
click.echo('')
|
||||
click.echo(mastodon.get_auth_request_url(client))
|
||||
click.echo(mastodon.get_auth_request_url(instance, client_id, client_secret))
|
||||
click.echo('')
|
||||
|
||||
grant_code = input('Code: ')
|
||||
mastodon.login(client, grant_code, usercred)
|
||||
mastodon.login(instance, client_id, client_secret, grant_code)
|
||||
|
||||
@cli.command()
|
||||
@click.argument('instance')
|
||||
def stream(instance):
|
||||
'''Stream music from your timeline.'''
|
||||
|
||||
clientcred = ensure_clientcred(instance)
|
||||
usercred = ensure_usercred(instance)
|
||||
client_id, client_secret = get_client_credentials(instance)
|
||||
access_token = get_access_token(instance)
|
||||
|
||||
mastodon.stream(instance, clientcred, usercred, cache_dir=dirs.user_cache_dir)
|
||||
mastodon.stream(instance, client_id, client_secret, access_token, cache_dir=DIRS.user_cache_dir)
|
||||
|
|
6
fediplay/dirs.py
Normal file
6
fediplay/dirs.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
'''Application directories.'''
|
||||
|
||||
from appdirs import AppDirs
|
||||
|
||||
|
||||
DIRS = AppDirs('fediplay', 'zigg')
|
60
fediplay/keyring.py
Normal file
60
fediplay/keyring.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
'''Secret storage.'''
|
||||
|
||||
import os
|
||||
path = os.path
|
||||
|
||||
import appdirs
|
||||
import click
|
||||
from keyring import get_password, set_password
|
||||
|
||||
from fediplay.dirs import DIRS
|
||||
|
||||
|
||||
SERVICE_NAME = 'fediplay'
|
||||
CREDENTIAL_CLIENT_ID = 'client_id'
|
||||
CREDENTIAL_CLIENT_SECRET = 'client_secret'
|
||||
CREDENTIAL_ACCESS_TOKEN = 'access_token'
|
||||
|
||||
def build_username(instance, credential_kind):
|
||||
return credential_kind + '@' + instance
|
||||
|
||||
def set_credential(instance, credential_kind, credential):
|
||||
set_password(SERVICE_NAME, build_username(instance, credential_kind), credential)
|
||||
|
||||
def get_credential(instance, credential_kind):
|
||||
return get_password(SERVICE_NAME, build_username(instance, credential_kind))
|
||||
|
||||
def has_credential(instance, credential_kind):
|
||||
return get_credential(instance, credential_kind) is not None
|
||||
|
||||
def migrate_client_credentials(instance):
|
||||
def migrate_and_unlink(filename):
|
||||
if path.exists(filename):
|
||||
click.echo('==> Migrating client credentials to keyring from ' + filename)
|
||||
|
||||
with open(filename, 'r', encoding='utf-8') as infile:
|
||||
client_id = infile.readline().strip()
|
||||
client_secret = infile.readline().strip()
|
||||
|
||||
set_credential(instance, CREDENTIAL_CLIENT_ID, client_id)
|
||||
set_credential(instance, CREDENTIAL_CLIENT_SECRET, client_secret)
|
||||
|
||||
os.unlink(filename)
|
||||
|
||||
migrate_and_unlink('clientcred.secret')
|
||||
migrate_and_unlink(path.join(DIRS.user_config_dir, instance + '.clientcred.secret'))
|
||||
|
||||
def migrate_access_token(instance):
|
||||
def migrate_and_unlink(filename):
|
||||
if path.exists(filename):
|
||||
click.echo('==> Migrating access token to keyring from ' + filename)
|
||||
|
||||
with open(filename, 'r', encoding='utf-8') as infile:
|
||||
access_token = infile.readline().strip()
|
||||
|
||||
set_credential(instance, CREDENTIAL_ACCESS_TOKEN, access_token)
|
||||
|
||||
os.unlink(filename)
|
||||
|
||||
migrate_and_unlink('usercred.secret')
|
||||
migrate_and_unlink(path.join(DIRS.user_config_dir, instance + '.usercred.secret'))
|
|
@ -7,6 +7,7 @@ from lxml.etree import HTML # pylint: disable=no-name-in-module
|
|||
import mastodon
|
||||
from youtube_dl.utils import DownloadError
|
||||
|
||||
import fediplay.keyring as keyring
|
||||
from fediplay.queue import Queue
|
||||
|
||||
Mastodon = mastodon.Mastodon
|
||||
|
@ -35,34 +36,35 @@ class StreamListener(mastodon.StreamListener):
|
|||
except DownloadError:
|
||||
pass
|
||||
|
||||
def register(instance, clientcred):
|
||||
def register(instance):
|
||||
'''Register fediplay to a Mastodon server and save the client credentials.'''
|
||||
|
||||
saved_umask = umask(0o77)
|
||||
Mastodon.create_app('fediplay', scopes=['read'], api_base_url=api_base_url(instance), to_file=clientcred)
|
||||
umask(saved_umask)
|
||||
client_id, client_secret = Mastodon.create_app('fediplay', scopes=['read'], api_base_url=api_base_url(instance))
|
||||
keyring.set_credential(instance, keyring.CREDENTIAL_CLIENT_ID, client_id)
|
||||
keyring.set_credential(instance, keyring.CREDENTIAL_CLIENT_SECRET, client_secret)
|
||||
|
||||
def build_client(instance, clientcred, usercred=None):
|
||||
def build_client(instance, client_id, client_secret, access_token=None):
|
||||
'''Builds a Mastodon client.'''
|
||||
|
||||
return Mastodon(client_id=clientcred, access_token=usercred, api_base_url=api_base_url(instance))
|
||||
return Mastodon(api_base_url=api_base_url(instance),
|
||||
client_id=client_id, client_secret=client_secret, access_token=access_token)
|
||||
|
||||
def get_auth_request_url(client):
|
||||
def get_auth_request_url(instance, client_id, client_secret):
|
||||
'''Gets an authorization request URL from a Mastodon instance.'''
|
||||
|
||||
return client.auth_request_url(scopes=['read'])
|
||||
return build_client(instance, client_id, client_secret).auth_request_url(scopes=['read'])
|
||||
|
||||
def login(client, grant_code, usercred):
|
||||
def login(instance, client_id, client_secret, grant_code):
|
||||
'''Log in to a Mastodon server and save the user credentials.'''
|
||||
|
||||
saved_umask = umask(0o77)
|
||||
client.log_in(code=grant_code, scopes=['read'], to_file=usercred)
|
||||
umask(saved_umask)
|
||||
client = build_client(instance, client_id, client_secret)
|
||||
access_token = client.log_in(code=grant_code, scopes=['read'])
|
||||
keyring.set_credential(instance, keyring.CREDENTIAL_ACCESS_TOKEN, access_token)
|
||||
|
||||
def stream(instance, clientcred, usercred, cache_dir='.'):
|
||||
def stream(instance, client_id, client_secret, access_token, cache_dir='.'):
|
||||
'''Stream statuses and add them to a queue.'''
|
||||
|
||||
client = build_client(instance, clientcred, usercred)
|
||||
client = build_client(instance, client_id, client_secret, access_token)
|
||||
listener = StreamListener(Queue(cache_dir))
|
||||
click.echo('==> Streaming from {}'.format(instance))
|
||||
client.stream_user(listener)
|
||||
|
|
Loading…
Reference in a new issue