From b95afdfdfac96d02e2c2448fa1951bf63a16a7a0 Mon Sep 17 00:00:00 2001
From: Holger Nahrstaedt <holger@nahrstaedt.de>
Date: Mon, 28 May 2018 23:07:47 +0200
Subject: [PATCH] Include steemconnect v2 to beem

steem
* add steemconnect in init, when set, steemconnect is used for broadasting
steemconnect
* new class can be used to broadcast operation with steemconnect v2
storage
* Token class to store encrypted token
Transactionbuilder
* use steemconnect broadcast with set in steem
Wallet
* add token storage class
* add setToken, clear_local_token, encrypt_token, decrypt_token, addToken, getTokenForAccountName, removeTokenFromPublicName, getPublicNames
Example
* Add login app for testing steemconnect
---
 beem/cli.py                 |   5 +-
 beem/steem.py               |   4 +
 beem/steemconnect.py        | 190 ++++++++++++++++++++++++++++++++++++
 beem/storage.py             | 135 ++++++++++++++++++++++++-
 beem/transactionbuilder.py  |  12 ++-
 beem/version.py             |   2 +-
 beem/wallet.py              | 111 ++++++++++++++++++++-
 beemapi/version.py          |   2 +-
 beembase/version.py         |   2 +-
 beemgraphenebase/version.py |   2 +-
 examples/login_app/app.py   |  36 +++++++
 setup.py                    |   2 +-
 12 files changed, 492 insertions(+), 11 deletions(-)
 create mode 100644 beem/steemconnect.py
 create mode 100644 examples/login_app/app.py

diff --git a/beem/cli.py b/beem/cli.py
index 771605a0..ccb72496 100644
--- a/beem/cli.py
+++ b/beem/cli.py
@@ -65,7 +65,8 @@ availableConfigurationKeys = [
     "default_account",
     "default_vote_weight",
     "nodes",
-    "password_storage"
+    "password_storage",
+    "client_id",
 ]
 
 
@@ -220,6 +221,8 @@ def set(key, value):
                 print("")
         if value == "environment":
             print("The wallet password can be stored in the UNLOCK invironment variable to skip password prompt!")
+    elif key == "client_id":
+        stm.config["client_id"] = value
     else:
         print("wrong key")
 
diff --git a/beem/steem.py b/beem/steem.py
index 8f6fe4e3..b0d86d2f 100644
--- a/beem/steem.py
+++ b/beem/steem.py
@@ -26,6 +26,7 @@ from .exceptions import (
     AccountDoesNotExistsException
 )
 from .wallet import Wallet
+from .steemconnect import SteemConnect
 from .transactionbuilder import TransactionBuilder
 from .utils import formatTime, resolve_authorperm, derive_permlink, remove_from_dict
 from beem.constants import STEEM_VOTE_REGENERATION_SECONDS
@@ -147,6 +148,9 @@ class Steem(object):
         self.unsigned = bool(kwargs.get("unsigned", False))
         self.expiration = int(kwargs.get("expiration", 30))
         self.bundle = bool(kwargs.get("bundle", False))
+        self.steemconnect = kwargs.get("steemconnect", None)
+        if self.steemconnect is not None and not isinstance(self.steemconnect, SteemConnect):
+            raise ValueError("steemconnect musst be SteemConnect object")
         self.blocking = kwargs.get("blocking", False)
 
         # Store config for access through other Classes
diff --git a/beem/steemconnect.py b/beem/steemconnect.py
new file mode 100644
index 00000000..8c0aef31
--- /dev/null
+++ b/beem/steemconnect.py
@@ -0,0 +1,190 @@
+# This Python file uses the following encoding: utf-8
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+from __future__ import unicode_literals
+from builtins import str
+import json
+import urllib.parse
+import requests
+from .storage import configStorage as config
+from beem.instance import shared_steem_instance
+
+
+class SteemConnect(object):
+    """ SteemConnect v2
+
+        :param str scope: comma seperate string with scopes
+            login,offline,vote,comment,delete_comment,comment_options,custom_json,claim_reward_balance
+
+
+        .. code-block:: python
+
+            # Run the login_app in examples and login with a account
+            from beem import Steem
+            from beem.steemconnect import SteemConnect
+            from beem.comment import Comment
+            sc2 = SteemConnect(client_id="beem.app")
+            steem = Steem(steemconnect=sc2)
+            steem.wallet.unlock("supersecret-passphrase")
+            post = Comment("author/permlink", steem_instance=stm)
+            post.upvote(voter="test")  # replace "test" with your account
+
+    """
+
+    def __init__(self, steem_instance=None, *args, **kwargs):
+        self.steem = steem_instance or shared_steem_instance()
+        self.access_token = None
+        self.get_refresh_token = kwargs.get("get_refresh_token", False)
+        self.client_id = kwargs.get("client_id", config["sc2_client_id"])
+        self.scope = kwargs.get("scope", config["sc2_scope"])
+        self.oauth_base_url = kwargs.get("oauth_base_url", config["oauth_base_url"])
+        self.sc2_api_url = kwargs.get("sc2_api_url", config["sc2_api_url"])
+
+    @property
+    def headers(self):
+        return {'Authorization': self.access_token}
+
+    def get_login_url(self, redirect_uri):
+        params = {
+            "client_id": self.client_id,
+            "redirect_uri": redirect_uri,
+            "scope": self.scope,
+        }
+        if self.get_refresh_token:
+            params.update({
+                "response_type": "code",
+            })
+
+        return urllib.parse.urljoin(
+            self.oauth_base_url,
+            "authorize?" + urllib.parse.urlencode(params, safe=","))
+
+    def get_access_token(self, code):
+        post_data = {
+            "grant_type": "authorization_code",
+            "code": code,
+            "client_id": self.client_id,
+            "client_secret": self.steem.wallet.getTokenForAccountName(self.client_id),
+        }
+
+        r = requests.post(
+            urllib.parse.urljoin(self.sc2_api_url, "oauth2/token/"),
+            data=post_data
+        )
+
+        return r.json()
+
+    def me(self, username=None):
+        if username:
+            self.set_username(username)
+        url = urllib.parse.urljoin(self.sc2_api_url, "me/")
+        r = requests.post(url, headers=self.headers)
+        return r.json()
+
+    def set_access_token(self, access_token):
+        """ Is needed for broadcast() and me()
+        """
+        self.access_token = access_token
+
+    def set_username(self, username):
+        """ Set a username for the next broadcast() or me operation()
+            The necessary token is fetched from the wallet
+        """
+        self.access_token = self.steem.wallet.getTokenForAccountName(username)
+
+    def broadcast(self, operations, username=None):
+        """ Broadcast a operations
+
+            Sample operations:
+
+            .. code-block:: js
+
+                [
+                    [
+                        'vote', {
+                                    'voter': 'holger.random',
+                                    'author': 'holger80',
+                                    'permlink': 'does-the-steem-blockchain-comply-with-the-gdpr-and-european-privacy-laws',
+                                    'weight': 10000
+                                }
+                    ]
+                ]
+
+        """
+        url = urllib.parse.urljoin(self.sc2_api_url, "broadcast/")
+        data = {
+            "operations": operations,
+        }
+        if username:
+            self.set_username(username)
+        headers = self.headers.copy()
+        headers.update({
+            "Content-Type": "application/json; charset=utf-8",
+            "Accept": "application/json",
+        })
+
+        r = requests.post(url, headers=headers, data=json.dumps(data))
+        try:
+            return r.json()
+        except ValueError:
+            return r.content
+
+    def refresh_access_token(self, code, scope):
+        post_data = {
+            "grant_type": "refresh_token",
+            "refresh_token": code,
+            "client_id": self.client_id,
+            "client_secret": self.steem.wallet.getTokenForAccountName(self.client_id),
+            "scope": scope,
+        }
+
+        r = requests.post(
+            urllib.parse.urljoin(self.sc2_api_url, "oauth2/token/"),
+            data=post_data,
+        )
+
+        return r.json()
+
+    def revoke_token(self, access_token):
+        post_data = {
+            "access_token": access_token,
+        }
+
+        r = requests.post(
+            urllib.parse.urljoin(self.sc2_api_url, "oauth2/token/revoke"),
+            data=post_data
+        )
+
+        return r.json()
+
+    def update_user_metadata(self, metadata):
+        put_data = {
+            "user_metadata": metadata,
+        }
+        r = requests.put(
+            urllib.parse.urljoin(self.sc2_api_url, "me/"),
+            data=put_data, headers=self.headers)
+
+        return r.json()
+
+    def hot_sign(self, operation, params, redirect_uri=None):
+        """ Creates a link for broadcasting a operation
+
+            :param str operation: operation name (e.g.: vote)
+            :param dict params: operation dict params
+        """
+
+        if not isinstance(operation, str) or not isinstance(params, dict):
+            raise ValueError("Invalid Request.")
+
+        base_url = self.sc2_api_url.replace("/api", "")
+
+        if redirect_uri:
+            params.update({"redirect_uri": redirect_uri})
+
+        params = urllib.parse.urlencode(params)
+        url = urllib.parse.urljoin(base_url, "sign/%s" % operation)
+        url += "?" + params
+
+        return url
diff --git a/beem/storage.py b/beem/storage.py
index 7c33dc9a..4b14df48 100644
--- a/beem/storage.py
+++ b/beem/storage.py
@@ -266,6 +266,129 @@ class Key(DataDir):
             connection.commit()
 
 
+class Token(DataDir):
+    """ This is the token storage that stores the public username and the
+        (possibly encrypted) token in the `token` table in the
+        SQLite3 database.
+    """
+    __tablename__ = 'token'
+
+    def __init__(self):
+        super(Token, self).__init__()
+
+    def exists_table(self):
+        """ Check if the database table exists
+        """
+        query = ("SELECT name FROM sqlite_master "
+                 "WHERE type='table' AND name=?", (self.__tablename__, ))
+        try:
+            connection = sqlite3.connect(self.sqlDataBaseFile)
+            cursor = connection.cursor()
+            cursor.execute(*query)
+            return True if cursor.fetchone() else False
+        except sqlite3.OperationalError:
+            self.sqlDataBaseFile = ":memory:"
+            log.warning("Could not read(database: %s)" % (self.sqlDataBaseFile))
+            return True
+
+    def create_table(self):
+        """ Create the new table in the SQLite database
+        """
+        query = ("CREATE TABLE {0} ("
+                 "id INTEGER PRIMARY KEY AUTOINCREMENT,"
+                 "name STRING(256),"
+                 "token STRING(256))".format(self.__tablename__))
+        connection = sqlite3.connect(self.sqlDataBaseFile)
+        cursor = connection.cursor()
+        cursor.execute(query)
+        connection.commit()
+
+    def getPublicNames(self):
+        """ Returns the public names stored in the database
+        """
+        query = ("SELECT name from {0} ".format(self.__tablename__))
+        connection = sqlite3.connect(self.sqlDataBaseFile)
+        cursor = connection.cursor()
+        try:
+            cursor.execute(query)
+            results = cursor.fetchall()
+            return [x[0] for x in results]
+        except sqlite3.OperationalError:
+            return []
+
+    def getTokenForPublicName(self, name):
+        """ Returns the (possibly encrypted) private token that
+            corresponds to a public name
+
+           :param str pub: Public name
+
+           The encryption scheme is BIP38
+        """
+        query = ("SELECT token from {0} WHERE name=?".format(self.__tablename__), (name,))
+        connection = sqlite3.connect(self.sqlDataBaseFile)
+        cursor = connection.cursor()
+        cursor.execute(*query)
+        token = cursor.fetchone()
+        if token:
+            return token[0]
+        else:
+            return None
+
+    def updateToken(self, name, token):
+        """ Change the token to a name
+
+           :param str name: Public name
+           :param str token: Private token
+        """
+        query = ("UPDATE {0} SET token=? WHERE name=?".format(self.__tablename__), (token, name))
+        connection = sqlite3.connect(self.sqlDataBaseFile)
+        cursor = connection.cursor()
+        cursor.execute(*query)
+        connection.commit()
+
+    def add(self, name, token):
+        """ Add a new public/private token pair (correspondence has to be
+            checked elsewhere!)
+
+           :param str name: Public name
+           :param str token: Private token
+        """
+        if self.getTokenForPublicName(name):
+            raise ValueError("Key already in storage")
+        query = ("INSERT INTO {0} (name, token) VALUES (?, ?)".format(self.__tablename__), (name, token))
+        connection = sqlite3.connect(self.sqlDataBaseFile)
+        cursor = connection.cursor()
+        cursor.execute(*query)
+        connection.commit()
+
+    def delete(self, name):
+        """ Delete the key identified as `name`
+
+           :param str name: Public name
+        """
+        query = ("DELETE FROM {0} WHERE name=?".format(self.__tablename__), (name,))
+        connection = sqlite3.connect(self.sqlDataBaseFile)
+        cursor = connection.cursor()
+        cursor.execute(*query)
+        connection.commit()
+
+    def wipe(self, sure=False):
+        """Purge the entire wallet. No keys will survive this!"""
+        if not sure:
+            log.error(
+                "You need to confirm that you are sure "
+                "and understand the implications of "
+                "wiping your wallet!"
+            )
+            return
+        else:
+            query = ("DELETE FROM {0} ".format(self.__tablename__))
+            connection = sqlite3.connect(self.sqlDataBaseFile)
+            cursor = connection.cursor()
+            cursor.execute(query)
+            connection.commit()
+
+
 class Configuration(DataDir):
     """ This is the configuration storage that stores key/value
         pairs in the `config` table of the SQLite3 database.
@@ -280,7 +403,11 @@ class Configuration(DataDir):
         "password_storage": "environment",
         "rpcpassword": "",
         "rpcuser": "",
-        "order-expiration": 7 * 24 * 60 * 60}
+        "order-expiration": 7 * 24 * 60 * 60,
+        "client_id": "",
+        "sc2_scope": "login",
+        "sc2_api_url": "https://v2.steemconnect.com/api/",
+        "oauth_base_url": "https://v2.steemconnect.com/oauth2/"}
 
     def __init__(self):
         super(Configuration, self).__init__()
@@ -531,6 +658,7 @@ class MasterPassword(object):
 
 # Create keyStorage
 keyStorage = Key()
+tokenStorage = Token()
 configStorage = Configuration()
 
 # Create Tables if database is brand new
@@ -541,3 +669,8 @@ newKeyStorage = False
 if not keyStorage.exists_table():
     newKeyStorage = True
     keyStorage.create_table()
+
+newTokenStorage = False
+if not tokenStorage.exists_table():
+    newTokenStorage = True
+    tokenStorage.create_table()
diff --git a/beem/transactionbuilder.py b/beem/transactionbuilder.py
index 3d8ca973..0104613e 100644
--- a/beem/transactionbuilder.py
+++ b/beem/transactionbuilder.py
@@ -8,6 +8,7 @@ from future.utils import python_2_unicode_compatible
 from beemgraphenebase.py23 import bytes_types, integer_types, string_types, text_type
 from .account import Account
 from .utils import formatTimeFromNow
+from .steemconnect import SteemConnect
 from beembase.objects import Operation
 from beemgraphenebase.account import PrivateKey, PublicKey
 from beembase.signedtransactions import Signed_Transaction
@@ -150,6 +151,9 @@ class TransactionBuilder(dict):
 
         if self.steem.wallet.locked():
             raise WalletLocked()
+        if self.steem.steemconnect is not None:
+            self.steem.steemconnect.set_username(account["name"])
+            return
 
         def fetchkeys(account, perm, level=0):
             if level > 2:
@@ -260,10 +264,10 @@ class TransactionBuilder(dict):
         """
         if not self._is_constructed() or (self._is_constructed() and reconstruct_tx):
             self.constructTx()
-
         if "operations" not in self or not self["operations"]:
             return
-
+        if self.steem.steemconnect is not None:
+            return
         # We need to set the default prefix, otherwise pubkeys are
         # presented wrongly!
         if self.steem.rpc is not None:
@@ -364,7 +368,9 @@ class TransactionBuilder(dict):
             return ret
         # Broadcast
         try:
-            if self.steem.blocking:
+            if self.steem.steemconnect is not None:
+                ret = self.steem.steemconnect.broadcast(self["operations"])
+            elif self.steem.blocking:
                 ret = self.steem.rpc.broadcast_transaction_synchronous(
                     args, api="network_broadcast")
                 ret.update(**ret.get("trx"))
diff --git a/beem/version.py b/beem/version.py
index a689cd88..d21a60e9 100644
--- a/beem/version.py
+++ b/beem/version.py
@@ -1,2 +1,2 @@
 """THIS FILE IS GENERATED FROM beem SETUP.PY."""
-version = '0.19.32'
+version = '0.19.33'
diff --git a/beem/wallet.py b/beem/wallet.py
index 9e399370..f4ac5f8c 100644
--- a/beem/wallet.py
+++ b/beem/wallet.py
@@ -6,10 +6,12 @@ from builtins import str, bytes
 from builtins import object
 import logging
 import os
+import hashlib
 from beemgraphenebase import bip38
 from beemgraphenebase.account import PrivateKey
 from beem.instance import shared_steem_instance
 from .account import Account
+from .aes import AESCipher
 from .exceptions import (
     MissingKeyError,
     InvalidWifError,
@@ -21,6 +23,7 @@ from .exceptions import (
     AccountDoesNotExistsException,
 )
 from beemapi.exceptions import NoAccessApi
+from beemgraphenebase.py23 import py23_bytes
 from .storage import configStorage as config
 try:
     import keyring
@@ -95,16 +98,17 @@ class Wallet(object):
                   import format (wif) (starting with a ``5``).
 
     """
-    keys = []
     masterpassword = None
 
     # Keys from database
     configStorage = None
     MasterPassword = None
     keyStorage = None
+    tokenStorage = None
 
     # Manually provided keys
     keys = {}  # struct with pubkey as key and wif as value
+    token = {}
     keyMap = {}  # type:wif pairs to force certain keys
 
     def __init__(self, steem_instance=None, *args, **kwargs):
@@ -125,6 +129,18 @@ class Wallet(object):
             self.MasterPassword = MasterPassword
             self.keyStorage = keyStorage
 
+        if "token" in kwargs:
+            self.setToken(kwargs["token"])
+        else:
+            """ If no keys are provided manually we load the SQLite
+                keyStorage
+            """
+            from .storage import tokenStorage
+            if MasterPassword is None:
+                from .storage import MasterPassword
+                self.MasterPassword = MasterPassword
+            self.tokenStorage = tokenStorage
+
     @property
     def prefix(self):
         if self.steem.is_connected():
@@ -157,6 +173,18 @@ class Wallet(object):
             pub = self._get_pub_from_wif(wif)
             Wallet.keys[pub] = str(wif)
 
+    def setToken(self, loadtoken):
+        """ This method is strictly only for in memory token that are
+            passed to Wallet/Steem with the ``token`` argument
+        """
+        log.debug(
+            "Force setting of private token. Not using the wallet database!")
+        self.clear_local_token()
+        if isinstance(loadtoken, dict):
+            Wallet.token = loadtoken
+        else:
+            raise ValueError("token must be a dict variable!")
+
     def unlock(self, pwd=None):
         """ Unlock the wallet database
         """
@@ -253,10 +281,12 @@ class Wallet(object):
         else:
             from .storage import (
                 keyStorage,
+                tokenStorage,
                 MasterPassword
             )
             MasterPassword.wipe(sure)
             keyStorage.wipe(sure)
+            tokenStorage.wipe(sure)
             self.clear_local_keys()
 
     def clear_local_keys(self):
@@ -264,6 +294,10 @@ class Wallet(object):
         Wallet.keys = {}
         Wallet.keyMap = {}
 
+    def clear_local_token(self):
+        """Clear all manually provided token"""
+        Wallet.token = {}
+
     def encrypt_wif(self, wif):
         """ Encrypt a wif key
         """
@@ -285,6 +319,35 @@ class Wallet(object):
             raise AssertionError()
         return format(bip38.decrypt(encwif, self.masterpassword), "wif")
 
+    def deriveChecksum(self, s):
+        """ Derive the checksum
+        """
+        checksum = hashlib.sha256(py23_bytes(s, "ascii")).hexdigest()
+        return checksum[:4]
+
+    def encrypt_token(self, token):
+        """ Encrypt a token key
+        """
+        if self.locked():
+            raise AssertionError()
+        aes = AESCipher(self.masterpassword)
+        return "{}${}".format(self.deriveChecksum(token), aes.encrypt(token))
+
+    def decrypt_token(self, enctoken):
+        """ decrypt a wif key
+        """
+        if self.locked():
+            raise AssertionError()
+        aes = AESCipher(self.masterpassword)
+        checksum, encrypted_token = enctoken.split("$")
+        try:
+            decrypted_token = aes.decrypt(encrypted_token)
+        except:
+            raise WrongMasterPasswordException
+        if checksum != self.deriveChecksum(decrypted_token):
+            raise WrongMasterPasswordException
+        return decrypted_token
+
     def _get_pub_from_wif(self, wif):
         """ Get the pubkey as string, from the wif key as string
         """
@@ -298,6 +361,44 @@ class Wallet(object):
             raise InvalidWifError(
                 "Invalid Private Key Format. Please use WIF!")
 
+    def addToken(self, name, token):
+        if self.tokenStorage:
+            if not self.created():
+                raise NoWalletException
+            self.tokenStorage.add(name, self.encrypt_token(token))
+
+    def getTokenForAccountName(self, name):
+        """ Obtain the private token for a given public name
+
+            :param str name: Public name
+        """
+        if(Wallet.token):
+            if name in Wallet.token:
+                return Wallet.token[name]
+            else:
+                raise MissingKeyError("No private token for {} found".format(name))
+        else:
+            # Test if wallet exists
+            if not self.created():
+                raise NoWalletException
+
+            if not self.unlocked():
+                raise WalletLocked
+
+            enctoken = self.tokenStorage.getTokenForPublicName(name)
+            if not enctoken:
+                raise MissingKeyError("No private token for {} found".format(name))
+            return self.decrypt_token(enctoken)
+
+    def removeTokenFromPublicName(self, name):
+        """ Remove a token from the wallet database
+        """
+        if self.tokenStorage:
+            # Test if wallet exists
+            if not self.created():
+                raise NoWalletException
+            self.tokenStorage.delete(name)
+
     def addPrivateKey(self, wif):
         """Add a private key to the wallet database"""
         pub = self._get_pub_from_wif(wif)
@@ -546,3 +647,11 @@ class Wallet(object):
             return self.keyStorage.getPublicKeys()
         else:
             return list(Wallet.keys.keys())
+
+    def getPublicNames(self):
+        """ Return all installed public token
+        """
+        if self.tokenStorage:
+            return self.tokenStorage.getPublicNames()
+        else:
+            return list(Wallet.token.keys())
diff --git a/beemapi/version.py b/beemapi/version.py
index a689cd88..d21a60e9 100644
--- a/beemapi/version.py
+++ b/beemapi/version.py
@@ -1,2 +1,2 @@
 """THIS FILE IS GENERATED FROM beem SETUP.PY."""
-version = '0.19.32'
+version = '0.19.33'
diff --git a/beembase/version.py b/beembase/version.py
index a689cd88..d21a60e9 100644
--- a/beembase/version.py
+++ b/beembase/version.py
@@ -1,2 +1,2 @@
 """THIS FILE IS GENERATED FROM beem SETUP.PY."""
-version = '0.19.32'
+version = '0.19.33'
diff --git a/beemgraphenebase/version.py b/beemgraphenebase/version.py
index a689cd88..d21a60e9 100644
--- a/beemgraphenebase/version.py
+++ b/beemgraphenebase/version.py
@@ -1,2 +1,2 @@
 """THIS FILE IS GENERATED FROM beem SETUP.PY."""
-version = '0.19.32'
+version = '0.19.33'
diff --git a/examples/login_app/app.py b/examples/login_app/app.py
new file mode 100644
index 00000000..0284d7b1
--- /dev/null
+++ b/examples/login_app/app.py
@@ -0,0 +1,36 @@
+from flask import Flask, request
+from beem.steemconnect import SteemConnect
+
+app = Flask(__name__)
+
+
+c = SteemConnect(client_id="beem.app", scope="login,vote,custom_json", get_refresh_token=False)
+# replace test with our wallet password
+c.steem.wallet.unlock("test")
+
+
+@app.route('/')
+def index():
+    login_url = c.get_login_url(
+        "http://localhost:5000/welcome",
+    )
+    return "<a href='%s'>Login with SteemConnect</a>" % login_url
+
+
+@app.route('/welcome')
+def welcome():
+    access_token = request.args.get("access_token", None)
+    name = request.args.get("username", None)
+    if c.get_refresh_token:
+        code = request.args.get("code")
+        refresh_token = c.get_access_token(code)
+        access_token = refresh_token["access_token"]
+        name = refresh_token["username"]
+    elif name is None:
+        c.set_access_token(access_token)
+        name = c.me()["name"]
+
+    if name in c.steem.wallet.getPublicNames():
+        c.steem.wallet.removeTokenFromPublicName(name)
+    c.steem.wallet.addToken(name, access_token)
+    return "Welcome <strong>%s</strong>!" % name
diff --git a/setup.py b/setup.py
index 2d7392cb..9141f9d3 100755
--- a/setup.py
+++ b/setup.py
@@ -16,7 +16,7 @@ except LookupError:
     ascii = codecs.lookup('ascii')
     codecs.register(lambda name, enc=ascii: {True: enc}.get(name == 'mbcs'))
 
-VERSION = '0.19.32'
+VERSION = '0.19.33'
 
 tests_require = ['mock >= 2.0.0', 'pytest', 'pytest-mock', 'parameterized']
 
-- 
GitLab