mirror of
https://github.com/nova-r/fediplug.git
synced 2025-03-22 13:29:30 +01:00
commit
1f1829551f
7 changed files with 149 additions and 83 deletions
2
Pipfile
2
Pipfile
|
@ -12,6 +12,8 @@ python-dotenv = "*"
|
||||||
click = "*"
|
click = "*"
|
||||||
"e1839a8" = {path = ".", editable = true}
|
"e1839a8" = {path = ".", editable = true}
|
||||||
appdirs = "*"
|
appdirs = "*"
|
||||||
|
keyring = "*"
|
||||||
|
"keyrings.alt" = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
pytest = "*"
|
pytest = "*"
|
||||||
|
|
32
Pipfile.lock
generated
32
Pipfile.lock
generated
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "add9770cb1b3cd200c03d312f2a977e024c75df453ca2f33094265959e1e7376"
|
"sha256": "823dc15b2e595d044b28df0bb1d28da5da1f2235baa200cf8d36c3eeb988a86d"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
@ -133,6 +133,14 @@
|
||||||
"editable": true,
|
"editable": true,
|
||||||
"path": "."
|
"path": "."
|
||||||
},
|
},
|
||||||
|
"entrypoints": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:10ad569bb245e7e2ba425285b9fa3e8178a0dc92fc53b1e1c553805e15a8825b",
|
||||||
|
"sha256:d2d587dde06f99545fb13a383d2cd336a8ff1f359c5839ce3a64c917d10c029f"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7'",
|
||||||
|
"version": "==0.2.3"
|
||||||
|
},
|
||||||
"http-ece": {
|
"http-ece": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2f31a0640c31a0c2934ab1e37005dd9a559ae854a16304f9b839e062074106cc"
|
"sha256:2f31a0640c31a0c2934ab1e37005dd9a559ae854a16304f9b839e062074106cc"
|
||||||
|
@ -146,6 +154,22 @@
|
||||||
],
|
],
|
||||||
"version": "==2.7"
|
"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": {
|
"lxml": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0941f4313208c07734410414d8308812b044fd3fb98573454e3d3a0d2e201f3d",
|
"sha256:0941f4313208c07734410414d8308812b044fd3fb98573454e3d3a0d2e201f3d",
|
||||||
|
@ -238,11 +262,11 @@
|
||||||
},
|
},
|
||||||
"youtube-dl": {
|
"youtube-dl": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:3300689ccc6cd22b8229e0c9cbb9f2cfcceaa3cef871a8e83c6a456623bb44e5",
|
"sha256:08d91f0e4dd2d4608a677aa68896b272dcd243a966e161643242203c291b051e",
|
||||||
"sha256:e4f81d38d2c7b8fa6fa069cd0f0096835dcaab91e74a4ca760625b76e0c614b4"
|
"sha256:cc380b9f2dee75370760fdc8dbfc7ff4eb0b611fa07ab86b80aafc2be5d08a4d"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==2018.7.10"
|
"version": "==2018.7.21"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"develop": {
|
"develop": {
|
||||||
|
|
|
@ -2,13 +2,15 @@
|
||||||
|
|
||||||
A Mastodon client that automatically plays your friends' music as they toot links to it.
|
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.
|
- 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.
|
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.'''
|
'''Entry point for command-line interface.'''
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
path = os.path
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import appdirs
|
import appdirs
|
||||||
import click
|
import click
|
||||||
from mastodon import Mastodon
|
from mastodon import Mastodon
|
||||||
|
|
||||||
|
from fediplay.dirs import DIRS
|
||||||
import fediplay.mastodon as mastodon
|
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():
|
def ensure_dirs():
|
||||||
'''Make sure the application directories exist.'''
|
'''Make sure the application directories exist.'''
|
||||||
|
|
||||||
if not path.exists(dirs.user_config_dir):
|
if not path.exists(DIRS.user_config_dir):
|
||||||
os.makedirs(dirs.user_config_dir)
|
os.makedirs(DIRS.user_config_dir)
|
||||||
|
|
||||||
if not path.exists(dirs.user_cache_dir):
|
if not path.exists(DIRS.user_cache_dir):
|
||||||
os.makedirs(dirs.user_cache_dir)
|
os.makedirs(DIRS.user_cache_dir)
|
||||||
|
|
||||||
def ensure_usercred(instance):
|
def get_access_token(instance):
|
||||||
'''Ensure the usercred file exists.'''
|
'''Ensure the user credential exists.'''
|
||||||
|
|
||||||
usercred = build_usercred_filename(instance)
|
keyring.migrate_access_token(instance)
|
||||||
|
|
||||||
if path.exists(usercred):
|
if not keyring.has_credential(instance, keyring.CREDENTIAL_ACCESS_TOKEN):
|
||||||
return usercred
|
click.echo('user credential for {} does not exist; try `fediplay login`'.format(instance))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
if path.exists('usercred.secret'):
|
return keyring.get_credential(instance, keyring.CREDENTIAL_ACCESS_TOKEN)
|
||||||
click.echo('==> Migrating usercred.secret to ' + usercred)
|
|
||||||
os.rename('usercred.secret', usercred)
|
|
||||||
return usercred
|
|
||||||
|
|
||||||
click.echo(usercred + ' does not exist; try `fediplay login`')
|
def get_client_credentials(instance):
|
||||||
sys.exit(1)
|
'''Ensure the client credentials exist.'''
|
||||||
|
|
||||||
def ensure_clientcred(instance):
|
keyring.migrate_client_credentials(instance)
|
||||||
'''Ensure the clientcred file exists.'''
|
|
||||||
|
|
||||||
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 (
|
||||||
return clientcred
|
keyring.get_credential(instance, keyring.CREDENTIAL_CLIENT_ID),
|
||||||
|
keyring.get_credential(instance, keyring.CREDENTIAL_CLIENT_SECRET)
|
||||||
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)
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
def cli():
|
def cli():
|
||||||
|
@ -76,43 +59,30 @@ def cli():
|
||||||
def register(instance):
|
def register(instance):
|
||||||
'''Register fediplay on your Mastodon instance.'''
|
'''Register fediplay on your Mastodon instance.'''
|
||||||
|
|
||||||
clientcred = build_clientcred_filename(instance)
|
mastodon.register(instance)
|
||||||
|
|
||||||
if path.exists(clientcred):
|
|
||||||
click.echo(clientcred + ' already exists')
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
mastodon.register(instance, clientcred)
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.argument('instance')
|
@click.argument('instance')
|
||||||
def login(instance):
|
def login(instance):
|
||||||
'''Log in to your Mastodon instance.'''
|
'''Log in to your Mastodon instance.'''
|
||||||
|
|
||||||
clientcred = ensure_clientcred(instance)
|
client_id, client_secret = get_client_credentials(instance)
|
||||||
|
|
||||||
usercred = build_usercred_filename(instance)
|
|
||||||
if path.exists(usercred):
|
|
||||||
click.echo(usercred + ' already exists')
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
client = mastodon.build_client(instance, clientcred)
|
|
||||||
|
|
||||||
click.echo('Open this page in your browser and follow the instructions.')
|
click.echo('Open this page in your browser and follow the instructions.')
|
||||||
click.echo('Paste the code here.')
|
click.echo('Paste the code here.')
|
||||||
click.echo('')
|
click.echo('')
|
||||||
click.echo(mastodon.get_auth_request_url(client))
|
click.echo(mastodon.get_auth_request_url(instance, client_id, client_secret))
|
||||||
click.echo('')
|
click.echo('')
|
||||||
|
|
||||||
grant_code = input('Code: ')
|
grant_code = input('Code: ')
|
||||||
mastodon.login(client, grant_code, usercred)
|
mastodon.login(instance, client_id, client_secret, grant_code)
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.argument('instance')
|
@click.argument('instance')
|
||||||
def stream(instance):
|
def stream(instance):
|
||||||
'''Stream music from your timeline.'''
|
'''Stream music from your timeline.'''
|
||||||
|
|
||||||
clientcred = ensure_clientcred(instance)
|
client_id, client_secret = get_client_credentials(instance)
|
||||||
usercred = ensure_usercred(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
|
import mastodon
|
||||||
from youtube_dl.utils import DownloadError
|
from youtube_dl.utils import DownloadError
|
||||||
|
|
||||||
|
import fediplay.keyring as keyring
|
||||||
from fediplay.queue import Queue
|
from fediplay.queue import Queue
|
||||||
|
|
||||||
Mastodon = mastodon.Mastodon
|
Mastodon = mastodon.Mastodon
|
||||||
|
@ -35,34 +36,35 @@ class StreamListener(mastodon.StreamListener):
|
||||||
except DownloadError:
|
except DownloadError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def register(instance, clientcred):
|
def register(instance):
|
||||||
'''Register fediplay to a Mastodon server and save the client credentials.'''
|
'''Register fediplay to a Mastodon server and save the client credentials.'''
|
||||||
|
|
||||||
saved_umask = umask(0o77)
|
client_id, client_secret = Mastodon.create_app('fediplay', scopes=['read'], api_base_url=api_base_url(instance))
|
||||||
Mastodon.create_app('fediplay', scopes=['read'], api_base_url=api_base_url(instance), to_file=clientcred)
|
keyring.set_credential(instance, keyring.CREDENTIAL_CLIENT_ID, client_id)
|
||||||
umask(saved_umask)
|
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.'''
|
'''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.'''
|
'''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.'''
|
'''Log in to a Mastodon server and save the user credentials.'''
|
||||||
|
|
||||||
saved_umask = umask(0o77)
|
client = build_client(instance, client_id, client_secret)
|
||||||
client.log_in(code=grant_code, scopes=['read'], to_file=usercred)
|
access_token = client.log_in(code=grant_code, scopes=['read'])
|
||||||
umask(saved_umask)
|
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.'''
|
'''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))
|
listener = StreamListener(Queue(cache_dir))
|
||||||
click.echo('==> Streaming from {}'.format(instance))
|
click.echo('==> Streaming from {}'.format(instance))
|
||||||
client.stream_user(listener)
|
client.stream_user(listener)
|
||||||
|
|
Loading…
Reference in a new issue