#!/usr/bin/env python # -*- coding: utf-8 -*- """ Blockstack-client ~~~~~ copyright: (c) 2014-2015 by Halfmoon Labs, Inc. copyright: (c) 2016 by Blockstack.org This file is part of Blockstack-client. Blockstack-client is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Blockstack-client is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Blockstack-client. If not, see . """ import json import time import copy import blockstack_profiles import httplib import virtualchain import jsonschema import virtualchain from virtualchain.lib.ecdsalib import * import keylib from proxy import * from blockstack_client import storage from blockstack_client import user as user_db from logger import get_logger from constants import USER_ZONEFILE_TTL, CONFIG_PATH, BLOCKSTACK_TEST, BLOCKSTACK_DEBUG from token_file import token_file_parse, token_file_update_profile, token_file_get, token_file_put from zonefile import get_name_zonefile from keys import get_data_privkey_info from schemas import * from config import get_config from constants import BLOCKSTACK_REQUIRED_STORAGE_DRIVERS_WRITE log = get_logger() def get_profile(name, **kw): """ Legacy compatibility method for get_token_file(). Wraps get_token_file, and if a token file is successfully resolved, returns {'status': True, 'profile': profile, 'zonefile': zonefile, 'token_file': token_file, 'raw_zonefile': unparsed zone file 'token_file': actual token file (if present) 'legacy': whether or not the profile was legacy} Returns {'error': ...} on error """ res = token_file_get(name, **kw) if 'error' in res: return res token_file = res['token_file'] zonefile = res['zonefile'] profile = None legacy = False if res.get('legacy_profile') is not None: profile = res['legacy_profile'] legacy = True else: profile = res['profile'] raw_zonefile = res.get('raw_zonefile') name_record = res.get('name_record') token_file = res.get('token_file') ret = { 'status': True, 'profile': profile, 'zonefile': zonefile, 'raw_zonefile': raw_zonefile, 'name_record': name_record, 'token_file': token_file, 'legacy': legacy } return ret def _get_person_profile(name, proxy=None): """ Get the person's profile. Works for raw profiles, and for profiles within token files. Only works if the profile is a Persona. Return {'profile': ..., 'person': ...} on success Return {'error': ...} on error """ res = get_profile(name, proxy=proxy) if 'error' in res: return {'error': 'Failed to load profile: {}'.format(res['error'])} if res['legacy']: return {'error': 'Failed to load profile: legacy format'} token_file = res.pop('token_file') profile = res.pop('profile') person = None try: person = blockstack_profiles.Person(profile) except Exception as e: log.exception(e) return {'error': 'Failed to parse profile data into a Person record'} return {'profile': profile, 'person': person, 'token_file': token_file} def _save_person_profile(name, cur_token_file, profile, signing_private_key, blockchain_id=None, proxy=None, config_path=CONFIG_PATH): """ Save a person's profile, given information fetched with _get_person_profile. Return {'status': True} on success Return {'error': ...} on error """ conf = get_config(config_path) assert conf required_storage_drivers = conf.get('storage_drivers_required_write', BLOCKSTACK_REQUIRED_STORAGE_DRIVERS_WRITE) required_storage_drivers = required_storage_drivers.split() res = token_file_update_profile(cur_token_file, profile, signing_private_key) if 'error' in res: return res new_token_file = res['token_file'] res = token_file_put(name, new_token_file, signing_private_key, proxy=proxy, required_drivers=required_storage_drivers, config_path=config_path) return res def profile_list_accounts(name, proxy=None): """ Get the list of accounts in a name's Person-formatted profile. Return {'accounts': ...} on success Return {'error': ...} on error """ if proxy is None: proxy = get_default_proxy() name_info = _get_person_profile(name, proxy=proxy) if 'error' in name_info: return name_info profile = name_info.pop('profile') person = name_info.pop('person') person_accounts = [] if hasattr(person, 'account'): person_accounts = person.account accounts = [] for acct in person_accounts: try: jsonschema.validate(acct, PROFILE_ACCOUNT_SCHEMA) accounts.append(acct) except jsonschema.ValidationError: continue return {'accounts': accounts} def profile_get_account(blockchain_id, service, identifier, config_path=CONFIG_PATH, proxy=None): """ Get an account. The first hit is returned. Return {'status': True, 'account': ...} on success Return {'error': ...} on error """ account_info = profile_list_accounts(blockchain_id, proxy=proxy ) if 'error' in account_info: return account_info accounts = account_info['accounts'] for account in accounts: if account['service'] == service and account['identifier'] == identifier: return {'status': True, 'account': account} return {'error': 'No such account', 'errno': errno.ENOENT} def profile_find_accounts(cur_profile, service, identifer): """ Given an profile, find accounts that match the service and identifier Returns a list of accounts on success """ accounts = [] for acct in cur_profile.get('account', []): try: jsonschema.validate(acct, PROFILE_ACCOUNT_SCHEMA) if acct['service'] == service and acct['identifier'] == identifier: accounts.append(acct) except jsonschema.ValidationError: continue return accounts def profile_patch_account(cur_profile, service, identifier, content_url, extra_data): """ Patch a given profile to add an account Return the new profile """ profile = copy.deepcopy(cur_profile) # make data new_account = { 'service': service, 'identifier': identifier, } if content_url: new_account['contentUrl'] = content_url if extra_data: new_account.update(extra_data) if not profile.has_key('account'): profile['account'] = [] # overwrite existing, if given replaced = False for i in xrange(0, len(profile['account'])): account = profile['account'][i] try: jsonschema.validate(account, PROFILE_ACCOUNT_SCHEMA) except jsonschema.ValidationError: continue if account['service'] == service and account['identifier'] == identifier: profile['account'][i] = new_account replaced = True break if not replaced: profile['account'].append(new_account) return profile def profile_put_account(blockchain_id, service, identifier, content_url, extra_data, signing_private_key, config_path=CONFIG_PATH, proxy=None): """ Save a new account to a profile. Return {'status': True, 'replaced': True/False} on success Return {'error': ...} on failure """ if proxy is None: proxy = get_default_proxy() person_info = _get_person_profile(blockchain_id, proxy=proxy) if 'error' in person_info: return person_info token_file = person_info['token_file'] if token_file is None: return {'error': 'Name points to raw, legacy profile. Please use the `migrate` command to migrate to the latest profile data format'} profile = person_info.pop('profile') profile = profile_patch_account(profile, service, identifier, content_url, extra_data) # save result = _save_person_profile(blockchain_id, token_file, profile, signing_private_key, blockchain_id=blockchain_id, proxy=proxy, config_path=config_path) if 'error' in result: return result return {'status': True} def profile_delete_account(blockchain_id, service, identifier, signing_private_key, config_path=CONFIG_PATH, proxy=None): """ Delete an account, given the blockchain ID, service, and identifier Return {'status': True} on success Return {'error': ...} on error """ person_info = _get_person_profile(blockchain_id, proxy=proxy) if 'error' in person_info: return person_info profile = person_info['profile'] if not profile.has_key('account'): # nothing to do return {'error': 'No such account'} token_file = person_info['token_file'] if token_file is None: return {'error': 'Name points to raw, legacy profile. Please use the `migrate` command to migrate to the latest profile data format'} found = False for i in xrange(0, len(profile['account'])): account = profile['account'][i] try: jsonschema.validate(account, PROFILE_ACCOUNT_SCHEMA) except jsonschema.ValidationError: continue if account['service'] == service and account['identifier'] == identifier: profile['account'].pop(i) found = True break if not found: return {'error': 'No such account'} result = _save_person_profile(blockchain_id, token_file, profile, signing_private_key, blockchain_id=blockchain_id, proxy=proxy, config_path=config_path) if 'error' in result: return result return {'status': True}