[SCRIPT] Sign in to lbrynet without LBRY Desktop! Finally!

Depending on what email solution you use, you might not even have to open a web browser! :hugs:

This is extremely useful for platforms where LBRY Desktop is difficult or impossible to build (for example FreeBSD) and thus difficult or impossible to sign in from.

The question of how to signin or login to lbry-sdk / lbrynet has been asked around without any clear answer. :face_exhaling: This seems to be it. :face_holding_back_tears:

Usage instructions and details on comments.

Basically, make sure lbrynet is running, save the code as signin-lbrynet.py and run python3 signin-lbrynet.py.

Not an expert on this, so suggestions/improvements will be welcome.

I canโ€™t edit posts on this forum (at least not perpetually), so future updates might be here.

#!/usr/bin/env python3

'''

--------------------------------------------------------------------------------

## Signin Script for lbrynet ##

[Third party contribution, not affiliated with official LBRY project]

Lets you signin to lbrynet without a GUI client like LBRY Desktop. Useful
for platforms where Electron clients are hard or impossible to build from
source and third party desktop clients (such as FastLBRY) is needed to be
used.

--------------------------------------------------------------------------------


Warning:
--------

Has not been tested thoroughly and for every kind of account setup. e.g.
Password-protected account or account with password-protected wallet has not
been tested. There might be bugs.

Review code yourself. Test before serious use. Use at your own risk.


Notes:
------

  - Keep lbrynet daemon running before executing this script and make sure
    it has finished initializing.
  - Tested in FreeBSD, Linux should work, Window$ will probably not work.
  - If requests is missing, install python requests package from your OS
    package manager or run:

      $ pip install requests

      or if it fails:

      $ python3 -m pip install requests

  - The auth token is saved in:

      "$HOME/.config/lbry-signin-script-auth-token.txt"

      or

      "<script_path>/.config/lbry-signin-script-auth-token.txt"


Usage:
------

Make sure lbrynet is running. If not, run it with "lbrynet start" & wait until
it finished initializing.

Optionally check the "Config" section below. Usually no change is needed.

Then, run:

    $ python3 signin-lbrynet.py

Follow the on screen instructions to continue.


Reference:
----------

Official documentation on lbrynet login flow:
<https://github.com/lbryio/lbry-desktop/wiki/auth-flows>


Credits:
--------

Idea from this script:
<https://notabug.org/jyamihud/FastLBRY-terminal/issues/17#issuecomment-28153>


--------------------------------------------------------------------------------

License: CC0 1.0 Universal
License text: <https://creativecommons.org/publicdomain/zero/1.0/legalcode>
License summary: <https://creativecommons.org/publicdomain/zero/1.0/>

--------------------------------------------------------------------------------

'''



## Imports ##

import sys

try:
	import requests
except:
	print('Error: requests module could not be imported.')
	print('Please install python requests from package manager or run')
	print('  $ python3 -m pip install requests')
	sys.exit(108)

from urllib.parse import urlparse, parse_qs

import json

import os

from getpass import getpass



## Config ##

# lbrynet host. Default: "http://localhost:5279"
lbrynet_host="http://localhost:5279"

# Online API to use. Default: "https://api.odysee.com"
lbry_api_host="https://api.odysee.com"



## Global things ##

# Prints json data for display
def print_json(json_data):
	print(json.dumps(json_data, indent=2))

# Store the path of this script
script_path=os.path.abspath(os.path.dirname(__file__))

# Store the file path to save the auth token in for later sessions of the script
auth_token_save_file=(
		os.environ.get("HOME", script_path)
		+"/.config/lbry-signin-script-auth-token.txt"
		)



## Check if lbrynet has local wallets/channels ##

# Get auth token from file
def get_saved_auth_token():
	if not os.path.isfile(auth_token_save_file):
		return
	try:
		save_file = open(auth_token_save_file, "r")
		try:
			auth_token = save_file.read()
		finally:
			save_file.close()
			return auth_token
	except IOError:
		print(f"Could not open file {auth_token_save_file} for read")
		sys.exit(70)

# Save auth token to file for later use
def set_saved_auth_token(auth_token):
	try:
		if not os.path.isdir(os.path.dirname(auth_token_save_file)):
			os.mkdir(os.path.dirname(auth_token_save_file), 0o700)
		save_file = open(auth_token_save_file, "w")
		try:
			save_file.write(auth_token)
		finally:
			save_file.close()
	except IOError:
		print(f"Could not open file {auth_token_save_file} for write")
		sys.exit(71)


# Get local channel count from lbrynet
def get_channel_count():
	try:
		channel_count = requests.post(
				f"{lbrynet_host}/?m=channel_list",
				json={"method": "channel_list"}
				).json().get("result", {}).get("total_items", 0)
	except requests.exceptions.ConnectionError:
		print("Error: lbrynet is not probably running or not running"+
			f" on {lbrynet_host}")
		sys.exit(10)
	except:
		channel_count = 0
	return channel_count



# Get local wallet count from lbrynet
def get_wallet_count():
	try:
		wallet_count = requests.post(
			f"{lbrynet_host}/?m=wallet_list", 
			json={"method": "wallet_list"}
			).json().get("result", {}).get("total_items", 0)
	except requests.exceptions.ConnectionError:
		print("Error: lbrynet is not probably running or not running"+
			f" on {lbrynet_host}")
		sys.exit(10)
	except:
		wallet_count = 0
	return wallet_count


# To check if previously saved auth token exists from a previous session
saved_auth_token = get_saved_auth_token()


# Ensure we have a token
if saved_auth_token:
	auth_token = saved_auth_token
else:
	# Get a new auth token
	new_user_return = requests.post(f"{lbry_api_host}/user/new").json()
	auth_token = new_user_return.get("data").get("auth_token")
	set_saved_auth_token(auth_token)

# Let the user know the auth token
print(f"\nUsing auth token (do not share it publicly):\n{auth_token}")

# Get info from auth token
user_me_response = requests.post(
	f"{lbry_api_host}/user/me",
	data={"auth_token": auth_token}
	).json()
user_me_data = {}
if user_me_response.get("success", False) == True:
	user_me_data = user_me_response.get("data")
	# Store for later use
	has_verified_email = user_me_data.get("has_verified_email")

	# Print auth token related info
	print(
"""
>> Data related to auth token <<

> Created at: {created_at}
> Updated at: {updated_at}
> Invite by ID: {invited_by_id}
> Invited at: {invited_at}
> Is Odysee user: {is_odysee_user}
> Country: {country}
> Primary email: {primary_email}
> Password set: {password_set}
> Has verified email: {has_verified_email}
> Is identity verified: {is_identity_verified}
> Is reward approved: {is_reward_approved}
> Device types: {device_types}
"""
		.format(
		created_at=user_me_data.get("created_at"),
		updated_at=user_me_data.get("updated_at"),
		invited_by_id=str(user_me_data.get("invited_by_id")),
		invited_at=str(user_me_data.get("invited_at")),
		is_odysee_user=str(user_me_data.get("is_odysee_user")),
		country=str(user_me_data.get("country")),
		primary_email=str(user_me_data.get("primary_email")),
		password_set=str(user_me_data.get("password_set")),
		has_verified_email=str(user_me_data.get("has_verified_email")),
		is_identity_verified=str(
			user_me_data.get("is_identity_verified")
			),
		is_reward_approved=str(user_me_data.get("is_reward_approved")),
		device_types=str(user_me_data.get("device_types"))
		)
	)

else:
	print("Failed 'user/me' request for auth token.\n"+
		"It seems signin process needs to take place...")


wallet_count = get_wallet_count()
channel_count = get_channel_count()

if wallet_count > 0 or channel_count > 0:
	try:
		input(
"""
You seem to have {wallet_c} local wallets and {channel_c} local channels.

If you are already signed in, continuing with this script might be unnecessary.
For example, if you can see your channels or your wallet balance is accurate on
a desktop client, maybe you are already signed in and this is not necessary.

If you don't want to sign in, press Ctrl+C to cancel, otherwise press enter...
"""
			.format(
				wallet_c=str(wallet_count), 
				channel_c=str(channel_count)
			)
		)
	except KeyboardInterrupt:
		print("Inturrupted. Terminating script...")
		sys.exit(37)



## Take input necessary for signin ##

if user_me_data.get("primary_email"):
	user_email = user_me_data.get("primary_email")
else:
	user_email = input("Enter LBRY/Odysee email: ")

if user_me_data.get("password_set"):
	user_password = getpass(
		"Enter LBRY/Odysee password (leave blank if you didn't set "
		+"any): "
		)
else:
	user_password = ""

# Get wallet password
wallet_password = getpass(
	"Enter wallet password (separate from your account password)"
	+" (leave blank if you didn't set any): "
	)



## Check if user exists at all ##

user_exists_return = requests.post(
			f"{lbry_api_host}/user/exists",
			params={"auth_token": auth_token, "email": user_email}
			).json()

if user_exists_return.get("success") == True:
	# User exists
	has_password = user_exists_return.get("data", {}).get("has_password")
else:
	# User does not exist
	print(
		"\nUser exist check failed. User with the email probably"
		+" doesn't exist."
		)
	sys.exit(67)



## Email verification ##

# If no password is set for account and not verified, start verification
if has_verified_email == False and has_password == False:
	print("\nSince the account doesn't have any password set, email"
		+" verification needs to be done.\n")
	input(f"\nTo continue sending email to {user_email} press "
		+"enter, to abort press Ctrl+C...")
	resend_token_return = requests.post(
		f"{lbry_api_host}/user_email/resend_token",
		params={
			"auth_token": auth_token,
			"email": user_email,
			"only_if_expired": "true"
			}
		).json()

	if resend_token_return.get("success") == True:
		print(
		"'user_email/resend_token' request was successful"
		)
	else:
		print("'user_email/resend_token' request was not"
			+" successful.\nDetails:")
		print_json(resend_token_return)

	print(
"""
Check inbox for {email_address} and don't click the URL.
You will need to paste the URL here instead.
You will get further instructions below...
"""
		.format(
		email_address=user_email
		)
	)
	input("Press enter when you got the email...")

	# Ask for URL sent in email
	print(
f"""
An email is probably sent to your {user_email} email address.
Check the Sign in URL that is sent. The URL might be valid only for 15 minutes.
"""
	)
	# Assume we didn't get a valid email URL (so that while loop runs)
	got_valid_email_url=False

	# Keep asking for an URL until a valid one is given by user
	while got_valid_email_url == False:
		email_url = input("Enter the Sign in URL that was sent: ")

		# Parse query parameters out of the given URL
		url_queries = parse_qs(urlparse(email_url).query)

		try:
			# Not using .get() because we want to catch KeyError
			# exception (below) in case given URL has missing
			# query parameters
			auth_token_email = url_queries["auth_token"][0]
			user_email = url_queries["email"][0]
			needs_recaptcha = url_queries["needs_recaptcha"][0]
			verification_token = (
				url_queries["verification_token"][0]
				)

			print(
f"""
Data detected from the URL:

> auth_token: {auth_token_email}
> email: {user_email}
> needs_recaptcha: {needs_recaptcha}
> verification_token: {verification_token}
"""
			)
		except KeyError:
			print(
"""
Error: some information could not be found in the URL provided.
Please make sure it has auth_token, email, needs_recaptcha, verification_token
query parameters and retry. A typical URL looks like this:

{example_url}
"""
			.format(
				example_url="https://odysee.com/$/verify?auth_token=<authentication token>&email=<email id>%40<email domain>&needs_recaptcha=false&verification_token=<verification token>"
			)
			)
		else:
			got_valid_email_url=True



	verify_return = requests.post(
		f"{lbry_api_host}/user_email/confirm",
		params={
			"auth_token": auth_token_email,
			"email": user_email, 
			"verification_token": verification_token, 
			"recaptcha": needs_recaptcha
			}
		).json()

	
	if verify_return.get("success") == True:
		print("'user_email/confirm' request was successful")
	else:
		print("'user_email/confirm' request was not successful.")
		print("Details:")
		print_json(verify_return)



## Finally request a sign in ##

signin_return = requests.post(
	f"{lbry_api_host}/user/signin",
	params={
		"auth_token": auth_token, 
		"email": user_email,
		"password": user_password
		}
	).json()

if signin_return.get("success") == True:
	print("'user/signin' request was successful")
	set_saved_auth_token(auth_token)
else:
	print("'user/signin' request was not successful.\nDetails:")
	print_json(signin_return)
	print("\nNOTE: Sometimes it shows failure but it gets you signed in.")
	print("So, if you are signed in at the end, ignore this.\n")



## Deal with sync ##

# Get sync hash from lbrynet
sync_hash = requests.post(
	f"{lbrynet_host}/?m=sync_hash", 
	json={"jsonrpc": "2.0", "method": "sync_hash", "params": {}, "id": 1}
	).json().get("result")

if sync_hash is None:
	print("Unable to retrieve sync_hash. Cannot continue.")
	sys.exit(36)
else:
	print(f"sync_hash is: {sync_hash}")

sync_get_output = requests.post(
	f"{lbry_api_host}/sync/get", 
	data={"auth_token": auth_token, "hash": sync_hash}
	).json()

if "data" in sync_get_output:
	print("'sync/get' request returned data. We can continue.")
	sync_apply_output = requests.post(
		f"{lbrynet_host}/?m=sync_apply",
		json={
			"jsonrpc": "2.0",
			"method": "sync_apply",
			"params": {
				"password": wallet_password,
				"blocking": "true", 
				"data": (
					sync_get_output
					.get("data", {}).get("data")
					)
			}
		}
		).json()
else:
	print("'sync/get' request returned no key named 'data' in response."
		+"'sync_apply' not possible. Sync might have failed.")

if sync_apply_output.get("result", {}).get("data"):
	print("\nYou are probably signed in now.")
else:
	print("\nThere is no result data found in the 'sync_apply' response")
	print("\nNot sure if the sync was successful")

print("\nPlease check in your LBRY powered desktop app of your choice.\n")