'Danny Campbell <danny.campbell@gmail.com>']
ABOUT_WEBSITE = 'http://mevemon.garage.maemo.org'
-APP_VERSION = '0.5-1'
+APP_VERSION = '0.5-2'
-# size of a valid api key
-KEY_SIZE = 64
+# size of a valid verification code
+MIN_VER_CODE_SIZE = 20
+MAX_VER_CODE_SIZE = 64
+
+# the access mask we need to perform mevemon's functions (we may want
+# this to be a _minimum_ access, but I'd have to take a closer look at
+# how the masks work - FIXME, danny)
+
+REQUIRED_ACCESS_MASK = 131081
""" wrapper for ini-file-based settings"""
+import os
import configobj
import constants
For example, a typical mEveMon config file (at '~/.mevemon/mevemon.cfg')will look like this:
[accounts]
- [[account.<uid1>]]
- uid = <uid1>
- apikey = <apikey1>
+ [[account.<key_id1>]]
+ key_id = <key_id1>
+ ver_code = <ver_code1>
- [[account.<uid2>]]
- uid = <uid2>
- apikey = <apikey2>
+ [[account.<key_id2>]]
+ key_id = <key_id2>
+ ver_code = <ver_code2>
[general]
# this is just a fake example, we don't store any general
"""
def __init__(self, cfg_file=constants.CONFIG_PATH):
self.cfg_file = cfg_file
- self.config = configobj.ConfigObj(self.cfg_file)
self._convert_gconf_to_cfgfile()
-
+ self._detect_and_backup_old_cfg()
+ self.config = configobj.ConfigObj(self.cfg_file)
+
def get_accounts(self):
- """ Returns a dictionary containing uid:api_key pairs gathered from the config file
+ """ Returns a dictionary containing key_id:ver_code pairs gathered from the config file
"""
account_dict = {}
try:
return account_dict
for account in cfg_accounts:
- account_dict[account['uid']] = account['apikey']
+ account_dict[account['key_id']] = account['ver_code']
return account_dict
- def get_api_key(self, uid):
- """ Returns the api key associated with the given uid.
+ def get_ver_code(self, key_id):
+ """ Returns the verification code associated with the given key_id.
"""
try:
- api_key = self.get_accounts()[uid]
- return api_key
+ ver_code = self.get_accounts()[key_id]
+ return ver_code
except KeyError:
- raise Exception("UID '%s' is not in settings")
+ raise Exception("KEY_ID '%s' is not in settings")
- def add_account(self, uid, api_key):
- """ Adds the provided uid:api_key pair to the config file.
+ def add_account(self, key_id, ver_code):
+ """ Adds the provided key_id:ver_code pair to the config file.
"""
if 'accounts' not in self.config.sections:
self.config['accounts'] = {}
- self.config['accounts']['account.%s' % uid] = {}
- self.config['accounts']['account.%s' % uid]['uid'] = uid
- self.config['accounts']['account.%s' % uid]['apikey'] = api_key
+ self.config['accounts']['account.%s' % key_id] = {}
+ self.config['accounts']['account.%s' % key_id]['key_id'] = key_id
+ self.config['accounts']['account.%s' % key_id]['ver_code'] = ver_code
self.write()
- def remove_account(self, uid):
- """ Removes the provided uid key from the config file
+ def remove_account(self, key_id):
+ """ Removes the provided key_id key from the config file
"""
for key in self.config['accounts']:
- if self.config['accounts'][key]['uid'] == uid:
+ if self.config['accounts'][key]['key_id'] == key_id:
del self.config['accounts'][key]
self.write()
"""
import gconf_settings
gsettings = gconf_settings.Settings()
- for uid, apikey in gsettings.get_accounts().items():
- self.add_account(uid, apikey)
- gsettings.remove_account(uid)
+ for key_id, ver_code in gsettings.get_accounts().items():
+ self.add_account(key_id, ver_code)
+ gsettings.remove_account(key_id)
+
+ def _detect_and_backup_old_cfg(self):
+ """ Searches the config file for the string 'apikey', which
+ would only be present in the old legacy config file. If it's
+ found, it backs up the file and we start over.
+ """
+ try:
+ temp = open(self.cfg_file, "r")
+ except IOError:
+ # if it's not here, forget about it - mission accomplished
+ return
+ config_contents = temp.read()
+ temp.close()
+ if config_contents.find('apikey') > 0:
+ # move the file then create a new one
+ os.rename(self.cfg_file, self.cfg_file + '.old')
+ else:
+ # we've got an updated cfg file
+ pass
+
import file_settings as settings
from constants import LOGPATH, MAXBYTES, LOGCOUNT, CONFIG_DIR, IMG_CACHE_PATH
from constants import APICACHE_PATH
+from constants import REQUIRED_ACCESS_MASK
#ugly hack to check maemo version. any better way?
if hasattr(hildon, "StackableWindow"):
def quit(self, *args):
gtk.main_quit()
- def get_auth(self, uid):
+ def get_auth(self, key_id):
""" Returns an authentication object to be used for eveapi calls
that require authentication.
"""
- api_key = self.settings.get_api_key(uid)
+ ver_code = self.settings.get_ver_code(key_id)
try:
- auth = self.cached_api.auth(userID=uid, apiKey=api_key)
+ auth = self.cached_api.auth(keyID=key_id, vCode=ver_code)
except Exception, e:
self.gui.report_error(str(e))
logging.getLogger('mevemon').exception("Failed to get character name")
return auth
- def get_char_sheet(self, uid, char_id):
+ def get_char_sheet(self, key_id, char_id):
""" Returns an object containing information about the character specified
by the provided character ID.
"""
try:
- sheet = self.get_auth(uid).character(char_id).CharacterSheet()
+ sheet = self.get_auth(key_id).character(char_id).CharacterSheet()
except Exception, e:
self.gui.report_error(str(e))
logging.getLogger('mevemon').exception("Failed to get character name")
return sheet
- def charid2uid(self, char_id):
+ def charid2key_id(self, char_id):
""" Takes a character ID and returns the user ID of the account containing
the character.
"""
acct_dict = self.settings.get_accounts()
- for uid, api_key in acct_dict.items():
- auth = self.cached_api.auth(userID=uid, apiKey=api_key)
+ for key_id, ver_code in acct_dict.items():
+ auth = self.cached_api.auth(keyID=key_id, vCode=ver_code)
try:
api_char_list = auth.account.Characters()
characters = api_char_list.characters
for character in characters:
if character.characterID == char_id:
- return uid
+ return key_id
def char_id2name(self, char_id):
return char_id
- def get_chars_from_acct(self, uid):
+ def get_chars_from_acct(self, key_id):
""" Returns a list of characters associated with the provided user ID.
"""
- auth = self.get_auth(uid)
+ auth = self.get_auth(key_id)
if not auth:
return None
else:
try:
api_char_list = auth.account.Characters()
- char_list = [char.name for char in api_char_list.characters]
+ char_name_list = [char.name for char in api_char_list.characters]
+ char_id_list = [char.characterID for char in api_char_list.characters]
except Exception, e:
self.gui.report_error(str(e))
logging.getLogger('mevemon').exception("Failed to get character list")
return None
- return char_list
+ return char_name_list, char_id_list
def get_characters(self):
- """ Returns a list of (character_name, image_path, uid) tuples from all the
+ """ Returns a list of (character_name, image_path, key_id) tuples from all the
accounts that are registered to mEveMon.
If there is an authentication issue, then instead of adding a valid
ui_char_list = []
err_img = "/usr/share/mevemon/imgs/error.jpg"
err_txt = "Problem fetching info for account (or no accounts added)"
+ bad_key = "Incorrect key access. Your access mask should be %s." % REQUIRED_ACCESS_MASK
placeholder_chars = (err_txt, err_img, None)
if not acct_dict:
return [placeholder_chars]
- for uid in acct_dict.keys():
- char_names = self.get_chars_from_acct(uid)
+ for key_id, ver_code in acct_dict.items():
+
+ char_names, char_ids = self.get_chars_from_acct(key_id)
if not char_names:
- ui_char_list.append((err_txt + "\t(UID: %s)" % uid, err_img, None))
+ ui_char_list.append((err_txt + "\t(KEY_ID: %s)" % key_id, err_img, None))
else:
- # append each char we get to the list we'll return to the
- # UI --danny
- for char_name in char_names:
- ui_char_list.append((char_name, self.get_portrait(char_name, 64) , uid) )
+
+
+ # since there are char names, let's check the key
+ # access and if it's bad we'll generate a key URL for
+ # each character
+ for char_name, char_id in zip(char_names, char_ids):
+ if self.get_access_mask(key_id, ver_code) != REQUIRED_ACCESS_MASK:
+ key_url = self.generate_access_mask_url(char_id)
+ ui_char_list.append((bad_key, err_img, None))
+ else:
+ # append each char we get to the list we'll
+ # return to the UI --danny
+ ui_char_list.append((char_name, self.get_portrait(char_name, 64), key_id))
return ui_char_list
return tree
- def get_skill_in_training(self, uid, char_id):
+ def get_skill_in_training(self, key_id, char_id):
""" Returns an object from eveapi containing information about the
current skill in training
"""
try:
- skill = self.get_auth(uid).character(char_id).SkillInTraining()
+ # should this be accessing the cached object? (confused) --danny
+ skill = self.get_auth(key_id).character(char_id).SkillInTraining()
except Exception, e:
self.gui.report_error(str(e))
logging.getLogger('mevemon').exception("Failed to get skill-in-training:")
assert(connection.request_connection(conic.CONNECT_FLAG_NONE))
- def get_sp(self, uid, char_id):
+ def get_sp(self, key_id, char_id):
""" Adds up the SP for all known skills, then calculates the SP gained
from an in-training skill.
"""
actual_sp = 0
- sheet = self.get_char_sheet(uid, char_id)
+ sheet = self.get_char_sheet(key_id, char_id)
for skill in sheet.skills:
actual_sp += skill.skillpoints
- live_sp = actual_sp + self.get_training_sp(uid, char_id)
+ live_sp = actual_sp + self.get_training_sp(key_id, char_id)
return live_sp
- def get_spps(self, uid, char_id):
+ def get_spps(self, key_id, char_id):
""" Calculate and returns the skill points per hour for the given character.
"""
- skill = self.get_skill_in_training(uid, char_id)
+ skill = self.get_skill_in_training(key_id, char_id)
if not skill.skillInTraining:
return (0, 0)
return (spps, skill.trainingStartTime)
- def get_training_sp(self, uid, char_id):
+ def get_training_sp(self, key_id, char_id):
""" returns the additional SP that the in-training skill has acquired
"""
- spps_tuple = self.get_spps(uid, char_id)
+ spps_tuple = self.get_spps(key_id, char_id)
if not spps_tuple:
return 0
except OSError, e:
logging.getLogger('mevemon').exception("Failed to clear cache")
+ def get_access_mask(self, key_id, ver_code):
+
+ """
+ Returns the access mask that determines what data we have
+ access to on the account.
+
+ """
+
+ return self.cached_api.account.APIKeyInfo(keyID=key_id, vCode=ver_code).key.accessMask
+
+ def generate_access_mask_url(self, char_id):
+
+ """
+ Generates a URL to send the user to the page that will help
+ them create an access key with exactly the access mevemon
+ needs.
+ """
+
+ return "https://supporttest.eveonline.com/api/Key/CreatePredefined/%s/%s/false" % (REQUIRED_ACCESS_MASK, char_id)
+
def excepthook(ex_type, value, tb):
""" a replacement for the default exception handler that logs errors"""
logging.getLogger("mevemon").error('Uncaught exception:',
return value
def edit_account(self, treeview):
- uid = self.get_selected_item(treeview, 0)
+ key_id = self.get_selected_item(treeview, 0)
# pop up the account dialog
self.accounts_model.get_accounts()
def delete_account(self, treeview):
- uid = self.get_selected_item(treeview, 0)
- self.controller.settings.remove_account(uid)
+ key_id = self.get_selected_item(treeview, 0)
+ self.controller.settings.remove_account(key_id)
# refresh model
self.accounts_model.get_accounts()
def add_columns_to_accounts(self, treeview):
#Column 0 for the treeview
renderer = gtk.CellRendererText()
- column = gtk.TreeViewColumn('User ID', renderer,
- text=models.AccountsModel.C_UID)
+ column = gtk.TreeViewColumn('Key ID', renderer,
+ text=models.AccountsModel.C_KID)
column.set_property("expand", True)
treeview.append_column(column)
dialog.set_transient_for(window)
dialog.set_title("New Account")
- uidLabel = gtk.Label("User ID:")
- uidLabel.set_justify(gtk.JUSTIFY_LEFT)
- vbox.add(uidLabel)
+ key_idLabel = gtk.Label("Key ID:")
+ key_idLabel.set_justify(gtk.JUSTIFY_LEFT)
+ vbox.add(key_idLabel)
- uidEntry = gtk.Entry()
- uidEntry.set_property('is_focus', False)
+ key_idEntry = gtk.Entry()
+ key_idEntry.set_property('is_focus', False)
- vbox.add(uidEntry)
+ vbox.add(key_idEntry)
- apiLabel = gtk.Label("API key:")
- apiLabel.set_justify(gtk.JUSTIFY_LEFT)
- vbox.add(apiLabel)
+ verCodeLabel = gtk.Label("Verification code:")
+ verCodeLabel.set_justify(gtk.JUSTIFY_LEFT)
+ vbox.add(verCodeLabel)
- apiEntry = gtk.Entry()
- apiEntry.set_property('is_focus', False)
+ verCodeEntry = gtk.Entry()
+ verCodeEntry.set_property('is_focus', False)
- vbox.add(apiEntry)
+ vbox.add(verCodeEntry)
ok_button = dialog.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK)
cancel_button = dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
while not valid_credentials:
if result == gtk.RESPONSE_OK:
- uid = uidEntry.get_text()
- api_key = apiEntry.get_text()
+ key_id = key_idEntry.get_text()
+ ver_code = verCodeEntry.get_text()
try:
- validation.validate_uid(uid)
- validation.validate_api_key(api_key)
+ validation.validate_key_id(key_id)
+ validation.validate_ver_code(ver_code)
except validation.ValidationError, e:
self.report_error(e.message)
result = dialog.run()
else:
valid_credentials = True
- self.controller.settings.add_account(uid, api_key)
+ self.controller.settings.add_account(key_id, ver_code)
self.accounts_model.get_accounts()
else:
break
# column 0 is the portrait, column 1 is name
char_name = model.get_value(miter, 1)
- uid = model.get_value(miter, 2)
+ key_id = model.get_value(miter, 2)
- if uid:
- CharacterSheetUI(self.controller, char_name, uid)
+ if key_id:
+ CharacterSheetUI(self.controller, char_name, key_id)
else:
pass
#time between live sp updates (in milliseconds)
UPDATE_INTERVAL = 1000
- def __init__(self, controller, char_name, uid):
+ def __init__(self, controller, char_name, key_id):
self.controller = controller
self.char_name = char_name
- self.uid = uid
+ self.key_id = key_id
self.sheet = None
self.char_id = None
self.skills_model = None
self.char_id = self.controller.char_name2id(self.char_name)
- self.sheet = self.controller.get_char_sheet(self.uid, self.char_id)
+ self.sheet = self.controller.get_char_sheet(self.key_id, self.char_id)
self.win.set_title(self.char_name)
gtk.Window.destroy(self.win)
def display_skill_in_training(self, vbox):
- skill = self.controller.get_skill_in_training(self.uid, self.char_id)
+ skill = self.controller.get_skill_in_training(self.key_id, self.char_id)
if skill.skillInTraining:
self.add_label("<small><b>Balance:</b> %s ISK</small>" %
util.comma(self.sheet.balance), box)
- self.live_sp_val = self.controller.get_sp(self.uid, self.char_id)
+ self.live_sp_val = self.controller.get_sp(self.key_id, self.char_id)
self.live_sp = self.add_label("<small><b>Total SP:</b> %s</small>" %
util.comma(int(self.live_sp_val)), box)
- self.spps = self.controller.get_spps(self.uid, self.char_id)[0]
+ self.spps = self.controller.get_spps(self.key_id, self.char_id)[0]
def fill_stats(self, box):
class CharacterSheetUI:
UPDATE_INTERVAL = 1
- def __init__(self, controller, char_name, uid):
+ def __init__(self, controller, char_name, key_id):
self.controller = controller
self.char_name = char_name
- self.uid = uid
+ self.key_id = key_id
self.sheet = None
self.char_id = None
self.skills_model = None
self.char_id = self.controller.char_name2id(self.char_name)
- self.sheet = self.controller.get_char_sheet(self.uid, self.char_id)
+ self.sheet = self.controller.get_char_sheet(self.key_id, self.char_id)
self.win.set_title(self.char_name)
def display_skill_in_training(self, vbox):
- skill = self.controller.get_skill_in_training(self.uid, self.char_id)
+ skill = self.controller.get_skill_in_training(self.key_id, self.char_id)
if skill.skillInTraining:
self.add_label("<small><b>Balance:</b> %s ISK</small>" %
util.comma(self.sheet.balance), box)
- self.live_sp_val = self.controller.get_sp(self.uid, self.char_id)
+ self.live_sp_val = self.controller.get_sp(self.key_id, self.char_id)
self.live_sp = self.add_label("<small><b>Total SP:</b> %s</small>" %
util.comma(int(self.live_sp_val)), box)
- self.spps = self.controller.get_spps(self.uid, self.char_id)[0]
+ self.spps = self.controller.get_spps(self.key_id, self.char_id)[0]
def fill_stats(self, box):
def on_delete_account_clicked(self):
- uid = self._get_selected_item(0)
- self.controller.settings.remove_account(uid)
+ key_id = self._get_selected_item(0)
+ self.controller.settings.remove_account(key_id)
self.accounts.refresh()
def _get_selected_item(self, column):
while not valid_credentials:
if result == gtk.RESPONSE_OK:
- uid = self.uidEntry.get_text()
- api_key = self.apiEntry.get_text()
+ key_id = self.keyIDEntry.get_text()
+ ver_code = self.verCodeEntry.get_text()
try:
- validation.validate_uid(uid)
- validation.validate_api_key(api_key)
+ validation.validate_key_id(key_id)
+ validation.validate_ver_code(ver_code)
except validation.ValidationError, e:
self.report_error(e.message)
result = self.run()
else:
valid_credentials = True
- self.controller.settings.add_account(uid, api_key)
+ self.controller.settings.add_account(key_id, ver_code)
else:
break
self.set_title("New Account")
- uidLabel = gtk.Label("User ID:")
- uidLabel.set_justify(gtk.JUSTIFY_LEFT)
- vbox.add(uidLabel)
+ keyIDLabel = gtk.Label("Key ID:")
+ keyIDLabel.set_justify(gtk.JUSTIFY_LEFT)
+ vbox.add(keyIDLabel)
- self.uidEntry = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
- self.uidEntry.set_placeholder("User ID")
- self.uidEntry.set_property('is_focus', False)
+ self.keyIDEntry = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
+ self.keyIDEntry.set_placeholder("Key ID")
+ self.keyIDEntry.set_property('is_focus', False)
- vbox.add(self.uidEntry)
+ vbox.add(self.keyIDEntry)
- apiLabel = gtk.Label("API key:")
- apiLabel.set_justify(gtk.JUSTIFY_LEFT)
- vbox.add(apiLabel)
+ verCodeLabel = gtk.Label("Verification code:")
+ verCodeLabel.set_justify(gtk.JUSTIFY_LEFT)
+ vbox.add(verCodeLabel)
- self.apiEntry = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
- self.apiEntry.set_placeholder("API Key")
- self.apiEntry.set_property('is_focus', False)
+ self.verCodeEntry = hildon.Entry(gtk.HILDON_SIZE_FINGER_HEIGHT)
+ self.verCodeEntry.set_placeholder("Verification code")
+ self.verCodeEntry.set_property('is_focus', False)
- vbox.add(self.apiEntry)
+ vbox.add(self.verCodeEntry)
ok_button = self.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK)
def add_columns(self):
#Column 0 for the treeview
renderer = gtk.CellRendererText()
- column = gtk.TreeViewColumn('User ID', renderer,
- text=models.AccountsModel.C_UID)
+ column = gtk.TreeViewColumn('Key ID', renderer,
+ text=models.AccountsModel.C_KID)
column.set_property("expand", True)
self.append_column(column)
# column 0 is the portrait, column 1 is name
char_name = model.get_value(miter, 1)
- uid = model.get_value(miter, 2)
+ key_id = model.get_value(miter, 2)
- if uid:
- CharacterSheetUI(self.controller, char_name, uid)
+ if key_id:
+ CharacterSheetUI(self.controller, char_name, key_id)
else:
pass
import util
class AccountsModel(gtk.ListStore):
- C_UID, C_APIKEY, C_CHARS = range(3)
+
+ # userID no longer exists, we want keyID
+ # api key becomes verification code... --danny
+ C_KID, C_VCODE, C_CHARS = range(3)
def __init__(self, controller):
gtk.ListStore.__init__(self, str, str, str)
if not accts_dict:
return None
- for uid, key in accts_dict.items():
+ for key_id, key in accts_dict.items():
liter = self.append()
- chars = self.controller.get_chars_from_acct(uid)
+ chars, ids = self.controller.get_chars_from_acct(key_id)
if chars:
char_str = ', '.join(chars)
char_str = "<small>%s</small>" % char_str
else:
char_str = ""
- self.set(liter, self.C_UID, uid, self.C_APIKEY, key, self.C_CHARS, char_str)
+ self.set(liter, self.C_KID, key_id, self.C_VCODE, key, self.C_CHARS, char_str)
class CharacterListModel(gtk.ListStore):
- C_PORTRAIT, C_NAME, C_UID = range(3)
+
+ C_PORTRAIT, C_NAME, C_KID = range(3)
def __init__(self, controller):
gtk.ListStore.__init__(self, gtk.gdk.Pixbuf, str, str)
char_list = self.controller.get_characters()
- for name, icon, uid in char_list:
+ for name, icon, key_id in char_list:
liter = self.append()
- self.set(liter, self.C_PORTRAIT, self._set_pix(icon), self.C_NAME, name, self.C_UID, uid)
+ self.set(liter, self.C_PORTRAIT, self._set_pix(icon), self.C_NAME, name, self.C_KID, key_id)
def _set_pix(self, filename):
pixbuf = gtk.gdk.pixbuf_new_from_file(filename)
def get_skills(self):
self.clear()
- uid = self.controller.charid2uid(self.charID)
+ key_id = self.controller.charid2key_id(self.charID)
- self.sheet = self.controller.get_char_sheet(uid, self.charID)
+ self.sheet = self.controller.get_char_sheet(key_id, self.charID)
skilltree = self.controller.get_skill_tree()
""" This module contains all our input validation functions """
-from constants import KEY_SIZE
+from constants import MIN_VER_CODE_SIZE, MAX_VER_CODE_SIZE
class ValidationError(StandardError):
""" Exception that is raised if input validation fails
return repr(self.message)
+def validate_key_id(key_id):
-def validate_api_key(api_key):
- """ Validates an EVE api key. throws ValidationError exception if the
- format is invalid.
+ """ Validates an EVE key ID. throws ValidationError exception if
+ the format is invalid.
"""
+
#TODO: anything else we can do to validate the api key?
+
+ # I don't know enough about the keyID yet. seems to be only
+ # numeric. The 2 I made were 705 and 706 respectively. I think
+ # they are just incrementing numbers. But I won't assume that
+ # yet...
- if len(api_key) != KEY_SIZE:
- raise ValidationError("API Key must be %s characters" % KEY_SIZE)
- elif not api_key.isalnum():
- raise ValidationError("API Key must only contain alphanumeric " +\
- "characters")
+ pass
-def validate_uid(uid):
- """ Validates an EVE Online uid, throws ValidationError exception if the
- format is invalid.
- """
- #TODO: anything else we can do to validate the uid?
+def validate_ver_code(ver_code):
+
+ """ Validates an EVE Online verification code, throws
+ ValidationError exception if the format is invalid.
- if not uid.isdigit():
- raise ValidationError("UID must be a number")
- if len(uid) < 1:
- raise ValidationError("Missing UID")
+ """
+
+ # What we DO know about the verification code is that it has to be
+ # at least 20 digits and at most 64. Seems to be alphanumeric
+ # only.
+
+
+ if len(ver_code) < MIN_VER_CODE_SIZE or len(ver_code) > MAX_VER_CODE_SIZE:
+ raise ValidationError("Verification code must be from 20 to 64 "
+ "characters.")
+