(Arne Babenhauserheide)
2013-08-16: merge bug report merge bug report
diff --git a/.bugs/bugs b/.bugs/bugs --- a/.bugs/bugs +++ b/.bugs/bugs @@ -1,9 +1,18 @@ minimize dependencies | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:0fc25f7b84f3e1fb89e9134a28eeabbe76bf054f, time:1372231529.45 +Problems using clone to create repositories.~ | owner:Steve Dougherty <steve@asksteved.com>, open:True, id:210c0e45adba5d500c6c5c00d750bfe43824cd3a, time:1375737210.02 Add command to reinsert repo list. | owner:, open:False, id:2dcc27c850209062080906368530b4b9202271d0, time:1373407233.84 pull fails, because config.get_wot_identity requests self.defaults['DEFAULT_TRUSTER'] which is not in defaults. | owner:Arne Babenhauserheide <bab@draketo.de>, open:False, id:31beb672d404944a4655a546b21c95c7baa91002, time:1371735138.39 cloning from a freenet:// uri does not work right now. | owner:Arne Babenhauserheide <bab@draketo.de>, open:False, id:35584408d800bb895d52dcf86cfba12c5b3237dd, time:1373100583.27 +Perform sanity check for multiple repos of the same name. | owner:Steve Dougherty <steve@asksteved.com>, open:True, id:4160157833020b1a33d2e6b05dc12c848c891d03, time:1375733375.04 +Preserve non-Infocalypse repositories in the repository list. | owner:Steve Dougherty <steve@asksteved.com>, open:True, id:4d63383a07faa29083fb0b61846ab6cc3d3151be, time:1375733323.42 set the timezone to UTC on cloning a freenet repo to avoid timezone-based attacks. | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:4dfc4cc28a7fa69f040776a7138da78ee89ec819, time:1355764180.36 mime-type problems | owner:Arne Babenhauserheide <bab@draketo.de>, open:False, id:5916e6e8328e20d8b0276b76b7116dd432730778, time:1353463866.97 | owner:, open:False, id:65d8d544370f80538e325dae0b6c2da449c5bcfe, time:1373407147.03 -simpler-wot-uris: name/repo | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:d4f2df3ca2c441e4be389be846634f5a4a08906e, time:1372232568.9 -add support for hg incoming freenet://… | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:dbff86d4bba0e38adb81a6e27e7180282222f541, time:1375002083.32 +Add --truster support to built-in commands. | owner:Steve Dougherty <steve@asksteved.com>, open:True, id:673a3103a58988552e0ef0b0ab20e8c75c9f6b1f, time:1375736861.8 +add support for hg incoming freenet://… | owner:Arne Babenhauserheide <bab@draketo.de>, open:True, id:dbff86d4bba0e38adb81a6e27e7180282222f541, time:1375002083.32 +Add i18n support for messages. | owner:, open:True, id:7760991aef41c6d38be5315f742f6a6f350a0a76, time:1375010635.52 +Fix implied clone destination when cloning from freenet:. | owner:Steve Dougherty <steve@asksteved.com>, open:True, id:9bd3ef617ba8871d28fbdae2500542e93302c791, time:1375736289.27 +Unit tests involving the node are hard. | owner:Steve Dougherty <steve@asksteved.com>, open:True, id:b01a53e59a2096254ecacdcee7673df5323d786e, time:1375737309.25 +Consider reworking repo list XML for conciseness. | owner:Steve Dougherty <steve@asksteved.com>, open:True, id:b511106032c58ffe7b33d0eb65bac7ec5555575e, time:1375734587.66 +simpler-wot-uris: name/repo | owner:Arne Babenhauserheide <bab@draketo.de>, open:False, id:d4f2df3ca2c441e4be389be846634f5a4a08906e, time:1372232568.9 +Add a warning that comments and unrecognized entries in ~/.infocalypse are removed. | owner:Steve Dougherty <steve@asksteved.com>, open:True, id:e9236fdd23d44bfbf8565bb1a59317289f324fd6, time:1375735601.35 diff --git a/.bugs/details/210c0e45adba5d500c6c5c00d750bfe43824cd3a.txt b/.bugs/details/210c0e45adba5d500c6c5c00d750bfe43824cd3a.txt new file mode 100644 --- /dev/null +++ b/.bugs/details/210c0e45adba5d500c6c5c00d750bfe43824cd3a.txt @@ -0,0 +1,27 @@ +# Lines starting with '#' and sections without content +# are not displayed by a call to 'details' +# +[paths] +# Paths related to this bug. +# suggested format: REPO_PATH:LINENUMBERS + + +[details] +When using clone . freenet:nickname/reponame to create a repository: + - The repository list is not updated. (No request URI in ~/.infocalypse) + - "hg push" doesn't work as the default path cannot be reversed to an insert URI and default-push is not set. Set default-push. + +[expected] +# The expected result + + +[actual] +# What happened instead + + +[reproduce] +# Reproduction steps + + +[comments] +# Comments and updates - leave your name diff --git a/.bugs/details/4160157833020b1a33d2e6b05dc12c848c891d03.txt b/.bugs/details/4160157833020b1a33d2e6b05dc12c848c891d03.txt new file mode 100644 --- /dev/null +++ b/.bugs/details/4160157833020b1a33d2e6b05dc12c848c891d03.txt @@ -0,0 +1,31 @@ +# Lines starting with '#' and sections without content +# are not displayed by a call to 'details' +# +[paths] +# Paths related to this bug. +# suggested format: REPO_PATH:LINENUMBERS + + +[details] +Having multiple directories associated with the same request URI means a pull +request cannot reasonably give a directory to pull to. (Or should it give all +matches?) + +Having multiple repositories with the same name (in the request URI) +makes pull requests not work (because it uses only name), and makes it more +confusing to refer to repositories. + +[expected] +# The expected result + + +[actual] +# What happened instead + + +[reproduce] +# Reproduction steps + + +[comments] +# Comments and updates - leave your name diff --git a/.bugs/details/673a3103a58988552e0ef0b0ab20e8c75c9f6b1f.txt b/.bugs/details/673a3103a58988552e0ef0b0ab20e8c75c9f6b1f.txt new file mode 100644 --- /dev/null +++ b/.bugs/details/673a3103a58988552e0ef0b0ab20e8c75c9f6b1f.txt @@ -0,0 +1,27 @@ +# Lines starting with '#' and sections without content +# are not displayed by a call to 'details' +# +[paths] +# Paths related to this bug. +# suggested format: REPO_PATH:LINENUMBERS + + +[details] +Currently built-in commands can only use the default truster or the one +associated with that repository. This is likely sufficient, so this feature +is low priority. + +[expected] +# The expected result + + +[actual] +# What happened instead + + +[reproduce] +# Reproduction steps + + +[comments] +# Comments and updates - leave your name diff --git a/.bugs/details/7760991aef41c6d38be5315f742f6a6f350a0a76.txt b/.bugs/details/7760991aef41c6d38be5315f742f6a6f350a0a76.txt new file mode 100644 --- /dev/null +++ b/.bugs/details/7760991aef41c6d38be5315f742f6a6f350a0a76.txt @@ -0,0 +1,28 @@ +# Lines starting with '#' and sections without content +# are not displayed by a call to 'details' +# +[paths] +# Paths related to this bug. +# suggested format: REPO_PATH:LINENUMBERS + + +[details] +# Additional details +from mercurial.i18n import _ +raise util.Abort(_("destination " + dest + " already exists.")) +Does including a variable in the string allow translation still? + +[expected] +# The expected result + + +[actual] +# What happened instead + + +[reproduce] +# Reproduction steps + + +[comments] +# Comments and updates - leave your name diff --git a/.bugs/details/9bd3ef617ba8871d28fbdae2500542e93302c791.txt b/.bugs/details/9bd3ef617ba8871d28fbdae2500542e93302c791.txt new file mode 100644 --- /dev/null +++ b/.bugs/details/9bd3ef617ba8871d28fbdae2500542e93302c791.txt @@ -0,0 +1,27 @@ +# Lines starting with '#' and sections without content +# are not displayed by a call to 'details' +# +[paths] +# Paths related to this bug. +# suggested format: REPO_PATH:LINENUMBERS + + +[details] +The implied destination of "hg clone freenet:operhiem1/pyProbe" is "freenet:operhiem1", +which causes the URI parsing to fail because there's no repository name. In this +case the destination should be the directory "pyProbe" instead. + +[expected] +# The expected result + + +[actual] +# What happened instead + + +[reproduce] +# Reproduction steps + + +[comments] +# Comments and updates - leave your name diff --git a/.bugs/details/b01a53e59a2096254ecacdcee7673df5323d786e.txt b/.bugs/details/b01a53e59a2096254ecacdcee7673df5323d786e.txt new file mode 100644 --- /dev/null +++ b/.bugs/details/b01a53e59a2096254ecacdcee7673df5323d786e.txt @@ -0,0 +1,27 @@ +# Lines starting with '#' and sections without content +# are not displayed by a call to 'details' +# +[paths] +# Paths related to this bug. +# suggested format: REPO_PATH:LINENUMBERS + + +[details] +Investigate a mocking framework or something for better frontend tests. +Currently there's just a very limited amount of backend testing, and tests +that involve a node over FCP are not feasible. + +[expected] +# The expected result + + +[actual] +# What happened instead + + +[reproduce] +# Reproduction steps + + +[comments] +# Comments and updates - leave your name diff --git a/.bugs/details/b511106032c58ffe7b33d0eb65bac7ec5555575e.txt b/.bugs/details/b511106032c58ffe7b33d0eb65bac7ec5555575e.txt new file mode 100644 --- /dev/null +++ b/.bugs/details/b511106032c58ffe7b33d0eb65bac7ec5555575e.txt @@ -0,0 +1,27 @@ +# Lines starting with '#' and sections without content +# are not displayed by a call to 'details' +# +[paths] +# Paths related to this bug. +# suggested format: REPO_PATH:LINENUMBERS + + +[details] +For instance, instead of a vcs field in each repository element, have all +infocalypse repositories in a <infocalypse> element. Maybe also have them +be <r> elements to be shorter. + +[expected] +# The expected result + + +[actual] +# What happened instead + + +[reproduce] +# Reproduction steps + + +[comments] +# Comments and updates - leave your name diff --git a/infocalypse/__init__.py b/infocalypse/__init__.py --- a/infocalypse/__init__.py +++ b/infocalypse/__init__.py @@ -568,13 +568,24 @@ extensions.wrapfunction(discovery, 'find # wrap the commands -def freenetpathtouri(ui, path, pull=True): +def freenetpathtouri(ui, path, operation, repo=None): """ Return a usable request or insert URI. Expects a freenet:// or freenet: protocol to be specified. - If the key is not a USK it will be resolved as a WoT identity. In this - case if the resolution fails, print an error message and return None. + If the key is not a USK it will be resolved as a WoT identity. + + + :param repo: Mercurial localrepository, used to resolve the truster set + for the repository. + :param operation: A string name of the operation the URI will be used to + perform. Used to return the appropriate result with + WoT-integrated URI resolution. Valid operations are: + * "pull" - request URI for existing repository. + * "push" - insert URI for existing repository. + * "clone-push" - insert URI for repository that might + not exist. (Skips looking up + published name and edition.) """ # TODO: Is this the only URL encoding that may happen? Why not use a more # semantically meaningful function? @@ -588,12 +599,16 @@ def freenetpathtouri(ui, path, pull=True # nick to be "USK", but this is a corner case. Using --wot will still work. if not path.startswith("USK"): import wot - if pull: - cfg = Config.from_ui(ui) - truster = cfg.defaults['DEFAULT_TRUSTER'] + if operation == "pull": + truster = get_truster(ui, repo) return wot.resolve_pull_uri(ui, path, truster) + elif operation == "push": + return wot.resolve_push_uri(ui, path) + elif operation == "clone-push": + return wot.resolve_push_uri(ui, path, resolve_edition=False) else: - return wot.resolve_push_uri(ui, path) + raise util.Abort("Internal error: invalid operation '{0}' when " + "resolving WoT-integrated URI.".format(operation)) else: return path @@ -615,7 +630,7 @@ def freenetpull(orig, *args, **opts): # only act differently, if the target is an infocalypse repo. if not isfreenetpath(path): return orig(*args, **opts) - uri = freenetpathtouri(ui, path) + uri = freenetpathtouri(ui, path, "pull", repo) opts["uri"] = uri opts["aggressive"] = True # always search for the latest revision. return infocalypse_pull(ui, repo, **opts) @@ -652,7 +667,7 @@ def freenetpush(orig, *args, **opts): # only act differently, if the target is an infocalypse repo. if not isfreenetpath(path): return orig(*args, **opts) - uri = freenetpathtouri(ui, path, pull=False) + uri = parse_repo_path(freenetpathtouri(ui, path, "push", repo)) if uri is None: return # if the uri is the short form (USK@/name/#), generate the key and preprocess the uri. @@ -660,9 +675,9 @@ def freenetpush(orig, *args, **opts): ui.status("creating a new key for the repo. For a new repo with an existing key, use clone.\n") from sitecmds import genkeypair fcphost, fcpport = opts["fcphost"], opts["fcpport"] - if fcphost == '': + if not fcphost: fcphost = '127.0.0.1' - if fcpport == 0: + if not fcpport: fcpport = 9481 # use redundant keys by default, except if explicitely requested otherwise. @@ -703,13 +718,16 @@ def freenetclone(orig, *args, **opts): if dest.endswith(".R1") or dest.endswith(".R0"): dest = dest[:-3] + # TODO: source holds the "repo" argument, but the naming is confusing in + # the context of freenetpathtouri(). # check whether to create, pull or copy pulluri, pushuri = None, None if isfreenetpath(source): - pulluri = freenetpathtouri(ui, source) + pulluri = parse_repo_path(freenetpathtouri(ui, source, "pull")) if isfreenetpath(dest): - pushuri = freenetpathtouri(ui, dest, pull=False) + pushuri = parse_repo_path(freenetpathtouri(ui, dest, "clone-push"), + assume_redundancy=True) # decide which infocalypse command to use. if pulluri and pushuri: @@ -730,9 +748,9 @@ def freenetclone(orig, *args, **opts): ui.status("creating a new key for the repo. To use your default key, call fn-create.\n") from sitecmds import genkeypair fcphost, fcpport = opts["fcphost"], opts["fcpport"] - if fcphost == '': + if not fcphost: fcphost = '127.0.0.1' - if fcpport == 0: + if not fcpport: fcpport = 9481 # use redundant keys by default, except if explicitely requested otherwise. diff --git a/infocalypse/commands.py b/infocalypse/commands.py --- a/infocalypse/commands.py +++ b/infocalypse/commands.py @@ -18,6 +18,8 @@ from validate import is_hex_string, is_f import os +from keys import parse_repo_path + def set_target_version(ui_, repo, opts, params, msg_fmt): """ INTERNAL: Update TARGET_VERSION in params. """ @@ -51,15 +53,19 @@ def infocalypse_create(ui_, repo, **opts insert_uri = '' local_id = None - if opts['uri'] != '' and opts['wot'] != '': + if opts['uri'] and opts['wot']: ui_.warn("Please specify only one of --uri or --wot.\n") return - elif opts['uri'] != '': - insert_uri = opts['uri'] - elif opts['wot'] != '': - # Expecting nick_prefix/repo_name.R<redundancy num>/edition + elif opts['uri']: + insert_uri = parse_repo_path(opts['uri']) + elif opts['wot']: + opts['wot'] = parse_repo_path(opts['wot']) nick_prefix, repo_name, repo_edition = opts['wot'].split('/', 2) + if not repo_name.endswith('.R1') and not repo_name.endswith('.R0'): + ui_.warn("Warning: Creating repository without redundancy. (R0 or" + " R1)\n") + from wot_id import Local_WoT_ID ui_.status("Querying WoT for local identities.\n") @@ -83,8 +89,7 @@ def infocalypse_create(ui_, repo, **opts import fcp node = fcp.FCPNode() vcs_response =\ - node.fcpPluginMessage(async=False, - plugin_name="plugins.WebOfTrust.WebOfTrust", + node.fcpPluginMessage(plugin_name="plugins.WebOfTrust.WebOfTrust", plugin_params=msg_params)[0] if vcs_response['header'] != 'FCPPluginReply' or\ @@ -95,6 +100,7 @@ def infocalypse_create(ui_, repo, **opts else: ui_.warn("Please set the insert key with either --uri or --wot.\n") + return set_target_version(ui_, repo, opts, params, "Only inserting to version(s): %s\n") @@ -117,19 +123,21 @@ def infocalypse_copy(ui_, repo, **opts): """ Copy an Infocalypse repository to a new URI. """ params, stored_cfg = get_config_info(ui_, opts) - insert_uri = opts['inserturi'] - if insert_uri == '': + if not opts['inserturi']: # REDFLAG: fix parameter definition so that it is required? ui_.warn("Please set the insert URI with --inserturi.\n") return + else: + insert_uri = parse_repo_path(opts['inserturi']) - request_uri = opts['requesturi'] - if request_uri == '': + if not opts['requesturi']: request_uri = stored_cfg.get_request_uri(repo.root) if not request_uri: ui_.warn("There is no stored request URI for this repo.\n" "Please set one with the --requesturi option.\n") return + else: + request_uri = parse_repo_path(opts['requesturi']) params['INSERT_URI'] = insert_uri params['REQUEST_URI'] = request_uri @@ -140,13 +148,14 @@ def infocalypse_reinsert(ui_, repo, **op """ Reinsert the current version of an Infocalypse repository. """ params, stored_cfg = get_config_info(ui_, opts) - request_uri = opts['uri'] - if request_uri == '': + if not opts['uri']: request_uri = stored_cfg.get_request_uri(repo.root) if not request_uri: ui_.warn("There is no stored request URI for this repo.\n" "Do a fn-pull from a repository USK and try again.\n") return + else: + request_uri = parse_repo_path(opts['uri']) level = opts['level'] if level < 1 or level > 5: @@ -174,9 +183,11 @@ def infocalypse_pull(ui_, repo, **opts): """ params, stored_cfg = get_config_info(ui_, opts) + request_uri = '' + if opts['hash']: # Use FMS to lookup the uri from the repo hash. - if opts['uri'] != '': + if opts['uri']: ui_.warn("Ignoring --uri because --hash is set!\n") if len(opts['hash']) != 1: raise util.Abort("Only one --hash value is allowed.") @@ -185,13 +196,13 @@ def infocalypse_pull(ui_, repo, **opts): request_uri = get_uri_from_hash(ui_, repo, params, stored_cfg) elif opts['wot']: import wot - truster = get_truster(ui_, repo, opts) + truster = get_truster(ui_, repo, opts['truster']) request_uri = wot.resolve_pull_uri(ui_, opts['wot'], truster) - else: - request_uri = opts['uri'] + elif opts['uri']: + request_uri = parse_repo_path(opts['uri']) - if request_uri == '': + if not request_uri: request_uri = stored_cfg.get_request_uri(repo.root) if not request_uri: ui_.warn("There is no stored request URI for this repo.\n" @@ -211,7 +222,7 @@ def infocalypse_pull_request(ui, repo, * "--wot.\n") wot_id, repo_name = opts['wot'].split('/', 1) - from_identity = get_truster(ui, repo, opts) + from_identity = get_truster(ui, repo, opts['truster']) to_identity = WoT_ID(wot_id, from_identity) wot.send_pull_request(ui, repo, from_identity, to_identity, repo_name) @@ -227,20 +238,22 @@ def infocalypse_check_notifications(ui, def infocalypse_connect(ui, repo, **opts): - import wot - wot.connect(ui, repo) + import plugin_connect + plugin_connect.connect(ui, repo) def infocalypse_push(ui_, repo, **opts): """ Push to an Infocalypse repository in Freenet. """ params, stored_cfg = get_config_info(ui_, opts) - insert_uri = opts['uri'] - if insert_uri == '': + + if not opts['uri']: insert_uri = stored_cfg.get_dir_insert_uri(repo.root) if not insert_uri: ui_.warn("There is no stored insert URI for this repo.\n" "Please set one with the --uri option.\n") return + else: + insert_uri = parse_repo_path(opts['uri']) set_target_version(ui_, repo, opts, params, "Only pushing to version(s): %s\n") @@ -270,7 +283,7 @@ def infocalypse_info(ui_, repo, **opts): opts['fcpport'] = 0 params, stored_cfg = get_config_info(ui_, opts) request_uri = opts['uri'] - if request_uri == '': + if not request_uri: request_uri = stored_cfg.get_request_uri(repo.root) if not request_uri: ui_.warn("There is no stored request URI for this repo.\n" @@ -329,7 +342,7 @@ def infocalypse_fmsread(ui_, repo, **opt opts['fcpport'] = 0 params, stored_cfg = get_config_info(ui_, opts) request_uri = opts['uri'] - if request_uri == '': + if not request_uri: request_uri = stored_cfg.get_request_uri(repo.root) if not request_uri: ui_.status("There is no stored request URI for this repo.\n") @@ -381,7 +394,7 @@ def infocalypse_putsite(ui_, repo, **opt return params, stored_cfg = get_config_info(ui_, opts) - if opts['key'] != '': # order important + if opts['key']: # order important params['SITE_KEY'] = opts['key'] if not (params['SITE_KEY'].startswith('SSK') or params['SITE_KEY'] == 'CHK@'): @@ -431,13 +444,13 @@ def infocalypse_wiki(ui_, repo, **opts): if required > 1: raise util.Abort("Use either --run, --createconfig, or --apply") - if opts['apply'] != '': + if opts['apply']: params, stored_cfg = get_config_info(ui_, opts) params['REQUEST_URI'] = opts['apply'] execute_wiki_apply(ui_, repo, params, stored_cfg) return - if opts['fcphost'] != '' or opts['fcpport'] != 0: + if opts['fcphost'] or opts['fcpport']: raise util.Abort("--fcphost, --fcpport only for --apply") # hmmmm.... useless copy? @@ -496,23 +509,34 @@ def infocalypse_setupfreemail(ui, repo, import wot # TODO: Here --truster doesn't make sense. There is no trust involved. # TODO: Should this be part of the normal fn-setup? - wot.execute_setup_freemail(ui, get_truster(ui, repo, opts)) + wot.execute_setup_freemail(ui, get_truster(ui, repo, opts['truster'])) -def get_truster(ui, repo, opts): +def get_truster(ui, repo=None, truster_identifier=None): """ - Return a local WoT ID - either one that published this repository or the - default. + Return a local WoT ID. + + Search for a local identity from most to least specific: + 1. truster_identifier (if given) + 2. identity that published this respository (if repo is given and an + identity is set) + 3. default truster + :rtype : Local_WoT_ID """ from wot_id import Local_WoT_ID - if opts['truster']: - return Local_WoT_ID(opts['truster']) + if truster_identifier: + return Local_WoT_ID(truster_identifier) else: - cfg = Config().from_ui(ui) + cfg = Config.from_ui(ui) - # Value is identity ID. - identity = cfg.get_wot_identity(cfg.get_request_uri(repo.root)) + # Value is identity ID, so '@' prefix makes it an identifier with an + # empty nickname. + identity = None + if repo: + identity = cfg.get_wot_identity(cfg.get_request_uri(repo.root)) + + # Either repo is not given or there is no associated identity. if not identity: identity = cfg.defaults['DEFAULT_TRUSTER'] @@ -524,7 +548,7 @@ def get_truster(ui, repo, opts): def do_archive_create(ui_, opts, params, stored_cfg): """ fn-archive --create.""" insert_uri = opts['uri'] - if insert_uri == '': + if not insert_uri: raise util.Abort("Please set the insert URI with --uri.") params['INSERT_URI'] = insert_uri @@ -535,7 +559,7 @@ def do_archive_create(ui_, opts, params, def do_archive_push(ui_, opts, params, stored_cfg): """ fn-archive --push.""" insert_uri = opts['uri'] - if insert_uri == '': + if not insert_uri: insert_uri = ( stored_cfg.get_dir_insert_uri(params['ARCHIVE_CACHE_DIR'])) if not insert_uri: @@ -553,7 +577,7 @@ def do_archive_pull(ui_, opts, params, s """ fn-archive --pull.""" request_uri = opts['uri'] - if request_uri == '': + if not request_uri: request_uri = ( stored_cfg.get_request_uri(params['ARCHIVE_CACHE_DIR'])) if not request_uri: diff --git a/infocalypse/config.py b/infocalypse/config.py --- a/infocalypse/config.py +++ b/infocalypse/config.py @@ -128,8 +128,13 @@ class Config: # repo id -> publisher WoT identity self.wot_identities = {} # TODO: Should this be keyed by str(WoT_ID) ? - # WoT public key hash -> Freemail password + # WoT identity ID -> Freemail password self.freemail_passwords = {} + # WoT identity ID -> last known repo list edition. + # TODO: Once WoT allows setting a property without triggering an + # immediate insert, this can move to a WoT property. (Can then query + # remote identities! Easier bootstrapping than from edition 0.) + self.repo_list_editions = {} # fms_id -> (usk_hash, ...) map self.fmsread_trust_map = DEFAULT_TRUST.copy() self.fmsread_groups = DEFAULT_GROUPS @@ -269,6 +274,23 @@ class Config: "Run hg fn-setupfreemail --truster {0}\n" .format(wot_identity)) + def set_repo_list_edition(self, wot_identity, edition): + """ + Set the repository list edition for the given WoT identity. + :type wot_identity: WoT_ID + """ + self.repo_list_editions[wot_identity.identity_id] = edition + + def get_repo_list_edition(self, wot_identity): + """ + Return the repository list edition associated with the given WoT + identity. Return 0 if one is not set. + """ + if wot_identity.identity_id in self.repo_list_editions: + return self.repo_list_editions[wot_identity.identity_id] + else: + return 0 + # Hmmm... really nescessary? def get_dir_insert_uri(self, repo_dir): """ Return the insert USK for repo_dir or None. """ @@ -383,6 +405,11 @@ class Config: cfg.freemail_passwords[wot_id] = parser.get( 'freemail_passwords', wot_id) + if parser.has_section('repo_list_editions'): + for wot_id in parser.options('repo_list_editions'): + cfg.repo_list_editions[wot_id] = int(parser.get( + 'repo_list_editions', wot_id)) + # ignored = fms_id|usk_hash|usk_hash|... if parser.has_section('fmsread_trust_map'): cfg.fmsread_trust_map.clear() # Wipe defaults. @@ -467,6 +494,10 @@ class Config: for wot_id in cfg.freemail_passwords: parser.set('freemail_passwords', wot_id, cfg.freemail_passwords[ wot_id]) + parser.add_section('repo_list_editions') + for wot_id in cfg.repo_list_editions: + parser.set('repo_list_editions', wot_id, cfg.repo_list_editions[ + wot_id]) parser.add_section('fmsread_trust_map') for index, fms_id in enumerate(cfg.fmsread_trust_map): entry = cfg.fmsread_trust_map[fms_id] diff --git a/infocalypse/devnotes.txt b/infocalypse/devnotes.txt --- a/infocalypse/devnotes.txt +++ b/infocalypse/devnotes.txt @@ -9,14 +9,8 @@ WoT identifier - Web of Trust identifier. This is MyNickname@public-key-hash. Often abbreviated "wot_id". Not to be confused with a WoT identity. -WoT identity - +WoT identity ID - name given to the public key hash when returned by WoT. - Can also refer to a dictionary containing the Identity and - other attributes such as a request URI and contexts. - - TODO: Apparently in the WoT code this is actually called - "identity ID". "ID" -> "identification" ~ "identifier", - right? What does that leave to call "nick@public key hash"? ------------------------------------------------------------ Dev log: diff --git a/infocalypse/doc/gsoc-2013-midterm-test.org b/infocalypse/doc/gsoc-2013-midterm-test.org new file mode 100644 --- /dev/null +++ b/infocalypse/doc/gsoc-2013-midterm-test.org @@ -0,0 +1,54 @@ +#+title: Infocalypse GSoC midterm test + +* Intro + +Testing Infocalypse Features from the midterm plans. Firstoff only personal notes, pending an LCWoT rebuild. + +* Setup + +Get infocalypse: + +#+BEGIN_SRC +hg -R ~/infocalypse_wiki_hacking/ up f5800540e7f6 +#+END_SRC + +* Getting a repo via WoT ID + +#+BEGIN_SRC sh +hg clone freenet://ArneBab/tech +#+END_SRC + +: Abbruch: No such identity 'ArneBab' + +hm, did not work. + +#+BEGIN_SRC sh +hg clone freenet://ArneBab@6~ZDYdvAgMoUfG6M5Kwi7SQqyS-gTcyFeaNN1Pf3FvY/tech +#+END_SRC + +: Abbruch: No such identity 'ArneBab@6~ZDYdvAgMoUfG6M5Kwi7SQqyS-gTcyFeaNN1Pf3FvY' + +#+BEGIN_SRC sh +hg fn-pull --wot ArneBab@6~ZDYdvAgMoUfG6M5Kwi7SQqyS-gTcyFeaNN1Pf3FvY/tech.R1/0 +#+END_SRC + +: Abbruch: '@' is ambiguous. +: +: _mgrThread: No incoming message from node +: _mgrThread: Testing for client req +: Unhandled exception in thread started by +: sys.excepthook is missing +: lost sys.stderr +: shutdown: entered + +#+BEGIN_SRC sh +hg fn-pull --aggressive --uri USK@6~ZDYdvAgMoUfG6M5Kwi7SQqyS-gTcyFeaNN1Pf3FvY,OSOT4OEeg4xyYnwcGECZUX6~lnmYrZsz05Km7G7bvOQ,AQACAAE/tech.R1/87 +#+END_SRC + +Works. + +#+BEGIN_SRC sh +hg pull freenet://USK@6~ZDYdvAgMoUfG6M5Kwi7SQqyS-gTcyFeaNN1Pf3FvY,OSOT4OEeg4xyYnwcGECZUX6~lnmYrZsz05Km7G7bvOQ,AQACAAE/tech.R1/87 +#+END_SRC + +Works. diff --git a/infocalypse/keys.py b/infocalypse/keys.py --- a/infocalypse/keys.py +++ b/infocalypse/keys.py @@ -1,4 +1,5 @@ from string import split +from mercurial import util class USK: @@ -17,6 +18,28 @@ class USK: elif self.key.startswith('freenet://'): self.key = self.key[len('freenet://'):] + def get_repo_name(self): + """ + Return name with the redundancy level, if any, removed. + + # TODO: tests. Use in detecting duplicate names. (Also + # determining repo names from URI.) + + >>> USK('USK@.../name/5').get_repo_name() + 'name' + >>> USK('USK@.../name.R1/5').get_repo_name() + 'name' + >>> USK('USK@.../name.R0/5').get_repo_name() + 'name' + >>> USK('USK@.../name.something/5').get_repo_name() + 'name.something' + >>> USK('USK@.../name.R2/5').get_repo_name() + 'name.R2' + """ + if self.name.endswith('.R1') or self.name.endswith('.R0'): + return self.name[:-3] + return self.name + def clone(self): return USK(str(self)) @@ -25,3 +48,50 @@ class USK: def __repr__(self): return "USK('%s')" % str(self) + + +# Method instead of class because the existing code expects keys to be strings. +# TODO: Would assuming edition / redundancy be better suited as arguments to +# the USK __init__()? WoT paths are not USKs though. Once again RepoPath +# might be nice. It would especially avoid repeated string operations to work +# with redundancy level. +def parse_repo_path(path, assume_redundancy=False): + """ + Return the given path to a repo - either USK or WoT path - + assuming if unspecified: + * edition 0 + * optionally, not by default (assume_redundancy) R1 redundancy + + >>> parse_repo_path('USK@.../name') + 'USK@.../name/0' + >>> parse_repo_path('USK@.../name/') + 'USK@.../name/0' + >>> parse_repo_path('USK@.../name', assume_redundancy=True) + 'USK@.../name.R1/0' + >>> parse_repo_path('USK@.../name.R0/5', assume_redundancy=True) + 'USK@.../name.R0/5' + >>> parse_repo_path('not a key') + Traceback (most recent call last): + ... + Abort: Cannot parse 'not a key' as repository path. + """ + parts = path.split('/') + + if len(parts) == 2: + # Assuming USK@..,/name: '/edition' omitted. + parts.append('0') + + if not len(parts) == 3: + raise util.Abort("Cannot parse '{0}' as repository path.".format(path)) + + if not parts[2]: + # Assuming USK@../name/: 'edition' omitted + parts[2] = '0' + + if assume_redundancy: + # Assuming USK@.../repo_name/edition + repo_name = parts[1] + if not repo_name.endswith('.R1') and not repo_name.endswith('.R0'): + parts[1] += '.R1' + + return '/'.join(parts) diff --git a/infocalypse/plugin_connect.py b/infocalypse/plugin_connect.py new file mode 100644 --- /dev/null +++ b/infocalypse/plugin_connect.py @@ -0,0 +1,112 @@ +from signal import signal, SIGINT +from time import sleep +import fcp +import threading +from mercurial import util +import sys + +PLUGIN_NAME = "org.freenetproject.plugin.dvcs_webui.main.Plugin" + +def connect(ui, repo): + node = fcp.FCPNode() + + ui.status("Connecting.\n") + + # TODO: Would it be worthwhile to have a wrapper that includes PLUGIN_NAME? + # TODO: Where to document the spec? devnotes.txt? How to format? + hi_there = node.fcpPluginMessage(plugin_name=PLUGIN_NAME, + plugin_params={'Message': 'Hello', + 'VoidQuery': 'true'})[0] + + if hi_there['header'] == 'Error': + raise util.Abort("The DVCS web UI plugin is not loaded.") + + if hi_there['Replies.Message'] == 'Error': + # TODO: Debugging + print hi_there + raise util.Abort("Another VCS instance is already connected.") + + session_token = hi_there['Replies.SessionToken'] + + ui.status("Connected.\n") + + def disconnect(signum, frame): + ui.status("Disconnecting.\n") + node.fcpPluginMessage(plugin_name=PLUGIN_NAME, + plugin_params= + {'Message': 'Disconnect', + 'SessionToken': session_token}) + sys.exit() + + # Send Disconnect on interrupt instead of waiting on timeout. + signal(SIGINT, disconnect) + + def ping(): + # Loop with delay. + while True: + pong = node.fcpPluginMessage(plugin_name=PLUGIN_NAME, + plugin_params= + {'Message': 'Ping', + 'SessionToken': session_token})[0] + if pong['Replies.Message'] == 'Error': + raise util.Abort(pong['Replies.Description']) + elif pong['Replies.Message'] != 'Pong': + ui.warn("Got unrecognized Ping reply '{0}'.\n".format(pong[ + 'Replies.Message'])) + + # Wait for less than timeout threshold. In testing responses take + # a little over a second. + sleep(3.5) + + # Start self-perpetuating pinging in the background. + t = threading.Timer(0.0, ping) + # Daemon threads do not hold up the process exiting. Allows prompt + # response to - for instance - SIGTERM. + t.daemon = True + t.start() + + while True: + query_identifier = node._getUniqueId() + # The event-querying is single-threaded, which makes things slow as + # everything waits on the completion of the current operation. + # Asynchronous code would require changes on the plugin side but + # potentially have much lower latency. + # TODO: Can wrap away PLUGIN_NAME, SessionToken, and QueryIdentifier? + command = node.fcpPluginMessage(plugin_name=PLUGIN_NAME, + plugin_params= + {'Message': 'Ready', + 'SessionToken': session_token, + 'QueryIdentifier': query_identifier})[0] + + response = command['Replies.Message'] + if response == 'Error': + raise util.Abort(command['Replies.Description']) + + if response not in handlers: + raise util.Abort("Unsupported query '{0}'\n") + + # Handlers are indexed by the query message name, take the query + # message, and return (result_name, plugin_params). + result_name, plugin_params = handlers[response](command) + + plugin_params['Message'] = result_name + plugin_params['QueryIdentifier'] = query_identifier + plugin_params['SessionToken'] = session_token + + ack = node.fcpPluginMessage(plugin_name=PLUGIN_NAME, + plugin_params=plugin_params)[0] + + if ack['Replies.Message'] != "Ack": + raise util.Abort("Received unexpected message instead of result " + "acknowledgement:\n{0}\n".format(ack)) + + +# Handlers return two items: result message name, message-specific parameters. +# The sending code handles the plugin name, required parameters and plugin name. + + +def VoidQuery(query): + return "VoidResult", {} + +# TODO: Perhaps look up method by name directly? +handlers = {'VoidQuery': VoidQuery} diff --git a/infocalypse/wot.py b/infocalypse/wot.py --- a/infocalypse/wot.py +++ b/infocalypse/wot.py @@ -8,77 +8,20 @@ from keys import USK import yaml from email.mime.text import MIMEText import imaplib -import threading from wot_id import Local_WoT_ID, WoT_ID FREEMAIL_SMTP_PORT = 4025 FREEMAIL_IMAP_PORT = 4143 -# TODO: Is whitespace in the search key illegal? -VCS_TOKEN = "[vcs] " -PLUGIN_NAME = "org.freenetproject.plugin.infocalypse_webui.main.InfocalypsePlugin" - - -def connect(ui, repo): - node = fcp.FCPNode() - - # TODO: Should I be using this? Looks internal. The identifier needs to - # be consistent though. - fcp_id = node._getUniqueId() - - ui.status("Connecting as '%s'.\n" % fcp_id) - - def ping(): - pong = node.fcpPluginMessage(plugin_name=PLUGIN_NAME, id=fcp_id, - plugin_params={'Message': 'Ping'})[0] - if pong['Replies.Message'] == 'Error': - raise util.Abort(pong['Replies.Description']) - # Must be faster than the timeout threshold. (5 seconds) - threading.Timer(4.0, ping).start() - - # Start self-perpetuating pinging in the background. - t = threading.Timer(0.0, ping) - # Daemon threads do not hold up the process exiting. Allows prompt - # response to - for instance - SIGTERM. - t.daemon = True - t.start() - - while True: - sequenceID = node._getUniqueId() - # The event-querying is single-threaded, which makes things slow as - # everything waits on the completion of the current operation. - # Asynchronous code would require changes on the plugin side but - # potentially have much lower latency. - command = node.fcpPluginMessage(plugin_name=PLUGIN_NAME, id=fcp_id, - plugin_params= - {'Message': 'ClearToSend', - 'SequenceID': sequenceID})[0] - # TODO: Look up handlers in a dictionary. - print command - - # Reload the config each time - it may have changed between messages. - cfg = Config.from_ui(ui) - - response = command['Replies.Message'] - if response == 'Error': - raise util.Abort(command['Replies.Description']) - elif response == 'ListLocalRepos': - params = {'Message': 'RepoList', - 'SequenceID': sequenceID} - - # Request USKs are keyed by repo path. - repo_index = 0 - for path in cfg.request_usks.iterkeys(): - params['Repo%s' % repo_index] = path - repo_index += 1 - - ack = node.fcpPluginMessage(plugin_name=PLUGIN_NAME, id=fcp_id, - plugin_params=params)[0] - print ack +VCS_TOKEN = "[vcs]" +# "infocalypse" is lower case in case it is used somewhere mixed case can +# cause problems like a filesystem path. Used for machine-readable VCS name. +VCS_NAME = "infocalypse" def send_pull_request(ui, repo, from_identity, to_identity, to_repo_name): """ - + Prompt for a pull request message, and send a pull request from + from_identity to to_identity for the repository to_repo_name. :type to_identity: WoT_ID :type from_identity: Local_WoT_ID @@ -87,7 +30,7 @@ def send_pull_request(ui, repo, from_ide to_address = require_freemail(to_identity) cfg = Config.from_ui(ui) - password = cfg.get_freemail_password(from_identity.identity_id) + password = cfg.get_freemail_password(from_identity) to_repo = find_repo(ui, to_identity, to_repo_name) @@ -103,10 +46,8 @@ def send_pull_request(ui, repo, from_ide from_branch = repo_context.branch() # Use double-quoted scalars so that Unicode can be included. (Nicknames.) - # "infocalypse" is lower case in case it is used somewhere mixed case can - # cause problems like a filesystem path. footer = yaml.dump({'request': 'pull', - 'vcs': 'infocalypse', + 'vcs': VCS_NAME, 'source': from_uri + '#' + from_branch, 'target': to_repo}, default_style='"', explicit_start=True, explicit_end=True, @@ -119,8 +60,8 @@ def send_pull_request(ui, repo, from_ide HG: Enter pull request message here. Lines beginning with 'HG:' are removed. HG: The first line has "{0}" added before it in transit and is the subject. -HG: The second line should be blank. -HG: Following lines are the body of the message. +HG: The second line is ignored. +HG: Subsequent lines are the body of the message. """.format(VCS_TOKEN), str(from_identity)) # TODO: Save message and load later in case sending fails. @@ -133,7 +74,7 @@ HG: Following lines are the body of the # Body is third line and after. msg = MIMEText('\n'.join(source_lines[2:]) + footer) - msg['Subject'] = VCS_TOKEN + source_lines[0] + msg['Subject'] = VCS_TOKEN + ' ' + source_lines[0] msg['To'] = to_address msg['From'] = from_address @@ -147,6 +88,8 @@ HG: Following lines are the body of the def check_notifications(ui, local_identity): """ + Check Freemail for local_identity and print information on any VCS + messages received. :type local_identity: Local_WoT_ID """ @@ -203,9 +146,14 @@ def check_notifications(ui, local_identi def read_message_yaml(ui, from_address, subject, body): + """ + Print information about the given message. + """ # Get consistent line endings. body = '\n'.join(body.splitlines()) yaml_start = body.rfind('---\n') + # The .join() does not add a trailing newline, and the end token might be + # the last line. end_token = '...' yaml_end = body.rfind(end_token) @@ -238,9 +186,7 @@ def read_message_yaml(ui, from_address, " formatted. Details:\n%s\n" % (subject, e)) return - # "infocalypse" is lower case in case it is used somewhere mixed case can - # cause problems like a filesystem path. - if request['vcs'] != 'infocalypse': + if request['vcs'] != VCS_NAME: ui.status("Notification '%s' is for '%s', not Infocalypse.\n" % (subject, request['vcs'])) return @@ -270,6 +216,7 @@ def require_freemail(wot_identity): Return the given identity's Freemail address. Abort with an error message if the given identity does not have a Freemail address / context. + :type wot_identity: WoT_ID """ if not wot_identity.freemail_address: @@ -279,19 +226,20 @@ def require_freemail(wot_identity): def update_repo_listing(ui, for_identity): - # TODO: WoT property containing edition. Used when requesting. - # Version number to support possible format changes. """ + Insert list of repositories published by the given identity. :type for_identity: Local_WoT_ID """ + # TODO: WoT property containing repo list edition. Used when requesting. + # Version number to support possible format changes. root = ET.Element('vcs', {'version': '0'}) ui.status("Updating repo listing for '%s'\n" % for_identity) for request_uri in build_repo_list(ui, for_identity): repo = ET.SubElement(root, 'repository', { - 'vcs': 'Infocalypse', + 'vcs': VCS_NAME, }) repo.text = request_uri @@ -303,8 +251,10 @@ def update_repo_listing(ui, for_identity # TODO: Somehow store the edition, perhaps in ~/.infocalypse. WoT # properties are apparently not appropriate. + cfg = Config.from_ui(ui) + insert_uri.name = 'vcs' - insert_uri.edition = '0' + insert_uri.edition = cfg.get_repo_list_edition(for_identity) ui.status("Inserting with URI:\n{0}\n".format(insert_uri)) uri = node.put(uri=str(insert_uri), mimetype='application/xml', @@ -314,6 +264,8 @@ def update_repo_listing(ui, for_identity ui.warn("Failed to update repository listing.") else: ui.status("Updated repository listing:\n{0}\n".format(uri)) + cfg.set_repo_list_edition(for_identity, USK(uri).edition) + Config.to_file(cfg) def build_repo_list(ui, for_identity): @@ -342,6 +294,7 @@ def find_repo(ui, identity, repo_name): identity matching the given identifier. Raise util.Abort if unable to read repo listing or a repo by that name does not exist. + :type identity: WoT_ID """ listing = read_repo_listing(ui, identity) @@ -357,44 +310,99 @@ def read_repo_listing(ui, identity): """ Read a repo listing for a given identity. Return a dictionary of repository request URIs keyed by name. + :type identity: WoT_ID """ + cfg = Config.from_ui(ui) uri = identity.request_uri.clone() uri.name = 'vcs' - uri.edition = 0 + uri.edition = cfg.get_repo_list_edition(identity) # TODO: Set and read vcs edition property. - node = fcp.FCPNode() - ui.status("Fetching {0}\n".format(uri)) - # TODO: What exception can this throw on failure? Catch it, - # print its description, and return None. - mime_type, repo_xml, msg = node.get(str(uri), priority=1, - followRedirect=True) + ui.status("Fetching.\n") + mime_type, repo_xml, msg = fetch_edition(uri) + ui.status("Fetched {0}.\n".format(uri)) - ui.status("Parsing.\n") + cfg.set_repo_list_edition(identity, uri.edition) + Config.to_file(cfg) + repositories = {} + ambiguous = [] root = fromstring(repo_xml) for repository in root.iterfind('repository'): - if repository.get('vcs') == 'Infocalypse': - uri = repository.text - # Expecting key/reponame.R<num>/edition - name = uri.split('/')[1].split('.')[0] - ui.status("Found repository \"{0}\" at {1}\n".format(name, uri)) - repositories[name] = uri + if repository.get('vcs') == VCS_NAME: + uri = USK(repository.text) + name = uri.get_repo_name() + if name not in repositories: + repositories[name] = uri + else: + existing = repositories[name] + if uri.key == existing.key and uri.name == existing.name: + # Different edition of same key and complete name. + # Use the latest edition. + if uri.edition > existing.edition: + repositories[name] = uri + else: + # Different key or complete name. Later remove and give + # warning. + ambiguous.append(name) + + for name in ambiguous: + # Same repo name but different key or exact name. + ui.warn("\"{0}\" refers ambiguously to multiple paths. Ignoring.\n" + .format(name)) + del repositories[name] + + # TODO: Would it make sense to mention those for which multiple editions + # are specified? It has no practical impact from this perspective, + # and these problems should be pointed out (or prevented) for local repo + # lists. + + for name in repositories.iterkeys(): + ui.status("Found repository \"{0}\".\n".format(name)) + + # Convert values from USKs to strings - USKs are not expected elsewhere. + for key in repositories.keys(): + repositories[key] = str(repositories[key]) return repositories +def fetch_edition(uri): + """ + Fetch a USK uri, following redirects. Change the uri edition to the one + fetched. + :type uri: USK + """ + node = fcp.FCPNode() + # Following a redirect automatically does not provide the edition used, + # so manually following redirects is required. + # TODO: Is there ever legitimately more than one redirect? + try: + return node.get(str(uri), priority=1) + except fcp.FCPGetFailed, e: + # Error code 27 is permanent redirect: there's a newer edition of + # the USK. + # https://wiki.freenetproject.org/FCPv2/GetFailed#Fetch_Error_Codes + if not e.info['Code'] == 27: + raise + + uri.edition = USK(e.info['RedirectURI']).edition + + return node.get(str(uri), priority=1) + + def resolve_pull_uri(ui, path, truster): """ Return a pull URI for the given path. Print an error message and abort on failure. - :type truster: Local_WoT_ID + TODO: Is it appropriate to outline possible errors? Possible failures are being unable to fetch a repo list for the given identity, which may be a fetch failure or being unable to find the identity, and not finding the requested repo in the list. + :type truster: Local_WoT_ID :param ui: For feedback. :param path: path describing a repo. nick@key/reponame :param truster: identity whose trust list to use. @@ -411,11 +419,16 @@ def resolve_pull_uri(ui, path, truster): return find_repo(ui, identity, repo_name) -def resolve_push_uri(ui, path): +def resolve_push_uri(ui, path, resolve_edition=True): """ Return a push URI for the given path. Raise util.Abort if unable to resolve identity or repository. + :param resolve_edition: Defaults to True. If False, skips resolving the + repository, uses the edition number 0. and does + not modify the repository name. This is useful + for finding a push URI for a repository that does + not already exist. :param ui: For feedback. :param path: path describing a repo - nick@key/repo_name, where the identity is a local one. (Such that the insert URI is known.) @@ -425,26 +438,33 @@ def resolve_push_uri(ui, path): local_id = Local_WoT_ID(wot_id) - insert_uri = local_id.insert_uri + if resolve_edition: + # TODO: find_repo should make it clearer that it returns a request URI, + # and return a USK. + repo = find_repo(ui, local_id, repo_name) - # TODO: find_repo should make it clearer that it returns a request URI, - # and return a USK. - repo = find_repo(ui, local_id, repo_name) + # Request URI + repo_uri = USK(repo) - # Request URI - repo_uri = USK(repo) + # Maintains name, edition. + repo_uri.key = local_id.insert_uri.key - # Maintains name, edition. - repo_uri.key = insert_uri.key + return str(repo_uri) + else: + repo_uri = local_id.insert_uri.clone() - return str(repo_uri) + repo_uri.name = repo_name + repo_uri.edition = 0 -# Support for querying WoT for own identities and identities meeting various -# criteria. -# TODO: "cmds" suffix to module name to fit fms, arc, inf? + return str(repo_uri) def execute_setup_wot(ui_, local_id): + """ + Set WoT-related defaults. + + :type local_id: Local_WoT_ID + """ cfg = Config.from_ui(ui_) ui_.status("Setting default truster to {0}.\n".format(local_id)) @@ -456,6 +476,8 @@ def execute_setup_wot(ui_, local_id): def execute_setup_freemail(ui, local_id): """ Prompt for, test, and set a Freemail password for the identity. + + :type local_id: Local_WoT_ID """ address = require_freemail(local_id) diff --git a/infocalypse/wot_id.py b/infocalypse/wot_id.py --- a/infocalypse/wot_id.py +++ b/infocalypse/wot_id.py @@ -37,7 +37,7 @@ class WoT_ID(object): # (and only) identity described by an unspecified message, in which case # it queries WoT to produce one. if not message: - message = get_identity(wot_identifier, truster) + message = _get_identity(wot_identifier, truster) def get_attribute(attribute): return message['Replies.{0}{1}'.format(attribute, id_num)] @@ -107,21 +107,29 @@ class Local_WoT_ID(WoT_ID): """ def __init__(self, wot_identifier): - id_num, message = get_local_identity(wot_identifier) + """ + Create a WoT_ID for a local identity matching the identifier. + + :type wot_identifier: str + """ + id_num, message = _get_local_identity(wot_identifier) self.insert_uri = USK(message['Replies.InsertURI{0}'.format(id_num)]) WoT_ID.__init__(self, None, None, id_num=id_num, message=message) -def get_identity(wot_identifier, truster): +def _get_identity(wot_identifier, truster): """ Internal. Return an FCP reply from WoT for an identity on the truster's trust list matching the identifier. Abort if anything but exactly one match is found. + + :type wot_identifier: str + :type truster: Local_WoT_ID """ - nickname_prefix, key_prefix = parse_name(wot_identifier) + nickname_prefix, key_prefix = _parse_name(wot_identifier) # TODO: Support different FCP IP / port. node = fcp.FCPNode() @@ -131,71 +139,72 @@ def get_identity(wot_identifier, truster # TODO: Should this manually ensure an identity has a vcs context # otherwise? - # LCWoT can have * to allow a wildcard match, but a wildcard alone is - # not allowed. See Lucine Term Modifiers documentation. The nickname - # uses this syntax but the ID is inherently startswith(). - params = {'Message': 'GetIdentitiesByPartialNickname', - 'Truster': truster.identity_id, - 'PartialNickname': - nickname_prefix + '*' if nickname_prefix else '', - 'PartialID': key_prefix, - 'MaxIdentities': 2, - 'Context': 'vcs'} + # GetIdentitiesByPartialNickname does not support empty nicknames. + if nickname_prefix: + params = {'Message': 'GetIdentitiesByPartialNickname', + 'Truster': truster.identity_id, + 'PartialNickname': + nickname_prefix + '*', + 'PartialID': key_prefix, + 'MaxIdentities': 2, + 'Context': 'vcs'} - response = \ - node.fcpPluginMessage(async=False, - plugin_name="plugins.WebOfTrust.WebOfTrust", - plugin_params=params)[0] + response = \ + node.fcpPluginMessage(plugin_name="plugins.WebOfTrust.WebOfTrust", + plugin_params=params)[0] - if response['header'] != 'FCPPluginReply' or \ - 'Replies.Message' not in response: - raise util.Abort('Unexpected reply. Got {0}\n'.format(response)) - elif response['Replies.Message'] == 'Identities': - matches = response['Replies.IdentitiesMatched'] - if matches == 0: - raise util.Abort("No identities match '{0}'\n".format( - wot_identifier)) - elif matches == 1: - return response - else: - raise util.Abort("'{0}' is ambiguous.\n".format(wot_identifier)) + if response['header'] != 'FCPPluginReply' or \ + 'Replies.Message' not in response: + raise util.Abort('Unexpected reply. Got {0}\n'.format(response)) + elif response['Replies.Message'] == 'Identities': + matches = response['Replies.IdentitiesMatched'] + if matches == 0: + raise util.Abort("No identities match '{0}'\n".format( + wot_identifier)) + elif matches == 1: + return response + else: + raise util.Abort("'{0}' matches more than one identity.\n" + .format(wot_identifier)) - # Partial matching not supported, or unknown truster. The only - # difference in the errors is human-readable, so just try the exact - # match. - assert response['Replies.Message'] == 'Error' + # Partial matching not supported, or unknown truster. The only + # difference in the errors is human-readable, so try the exact match. + assert response['Replies.Message'] == 'Error' # key_prefix must be a complete key for the lookup to succeed. params = {'Message': 'GetIdentity', - 'Truster': truster, + 'Truster': truster.identity_id, 'Identity': key_prefix} response = \ - node.fcpPluginMessage(async=False, - plugin_name="plugins.WebOfTrust.WebOfTrust", + node.fcpPluginMessage(plugin_name="plugins.WebOfTrust.WebOfTrust", plugin_params=params)[0] if response['Replies.Message'] == 'Error': # Searching by exact public key hash, not matching. - raise util.Abort("No such identity '{0}'.\n".format(wot_identifier)) + raise util.Abort("No identity has the complete public key hash '{0}'. " + "({1}) To flexibly match by partial nickname and key " + "use LCWoT for now.\n".format(key_prefix, + wot_identifier)) # There should be only one result. # Depends on https://bugs.freenetproject.org/view.php?id=5729 return response -def get_local_identity(wot_identifier): +def _get_local_identity(wot_identifier): """ Internal. Return (id_number, FCP reply) from WoT for a local identity matching the identifier. Abort if anything but exactly one match is found. + + :type wot_identifier: str """ - nickname_prefix, key_prefix = parse_name(wot_identifier) + nickname_prefix, key_prefix = _parse_name(wot_identifier) node = fcp.FCPNode() response = \ - node.fcpPluginMessage(async=False, - plugin_name="plugins.WebOfTrust.WebOfTrust", + node.fcpPluginMessage(plugin_name="plugins.WebOfTrust.WebOfTrust", plugin_params={'Message': 'GetOwnIdentities'})[0] @@ -244,8 +253,10 @@ def get_local_identity(wot_identifier): return id_num, response -def parse_name(wot_identifier): +def _parse_name(wot_identifier): """ + Internal. + Parse identifier of the forms: nick nick@key @key