(Arne Babenhauserheide)
2013-07-02: merge changes from operhiem1 with some comments. merge changes from operhiem1 with some comments.
diff --git a/Readme.txt b/Readme.txt --- a/Readme.txt +++ b/Readme.txt @@ -14,3 +14,15 @@ and many other features. And it works transparently: To publish, just clone to freenet: hg clone REPO freenet://USK@/REPO + +It supports WoT too: + + hg clone REPO freenet://wot_nick@public_key_hash/repo + + - Push / pull by name + - Pull requests + +Dependencies for full WoT support: + - PyYAML http://pyyaml.org/wiki/PyYAMLDocumentation + - lib-pyFreenet https://github.com/freenet/lib-pyFreenet-official + - DefusedXML https://pypi.python.org/pypi/defusedxml/ \ No newline at end of file diff --git a/infocalypse/__init__.py b/infocalypse/__init__.py --- a/infocalypse/__init__.py +++ b/infocalypse/__init__.py @@ -390,9 +390,11 @@ cmdtable = { "[options]"), "fn-pull-request": (infocalypse_pull_request, - WOT_OPTS + - FCP_OPTS, - "--wot id@key/repo"), + [('', 'wot', '', 'WoT nick@key/repo to send request ' + 'to')] + + WOT_OPTS + + FCP_OPTS, + "[--truster nick@key] --wot nick@key/repo"), "fn-push": (infocalypse_push, [('', 'uri', '', 'insert URI to push to'), @@ -692,9 +694,16 @@ def freenetclone(orig, *args, **opts): # check whether to create, pull or copy pulluri, pushuri = None, None if isfreenetpath(source): - pulluri = freenetpathtouri(source) + pulluri = freenetpathtouri(ui, source) + + if not pulluri: + raise util.Abort() + if isfreenetpath(dest): - pushuri = freenetpathtouri(dest) + pushuri = freenetpathtouri(ui, dest, pull=False) + + if not pushuri: + return # decide which infocalypse command to use. if pulluri and pushuri: diff --git a/infocalypse/commands.py b/infocalypse/commands.py --- a/infocalypse/commands.py +++ b/infocalypse/commands.py @@ -107,9 +107,9 @@ def infocalypse_create(ui_, repo, **opts params['INSERT_URI'] = insert_uri inserted_to = execute_create(ui_, repo, params, stored_cfg) - # TODO: Move into some function. How to separate local success context? - if inserted_to is not None and attributes is not None and \ - stored_cfg.has_wot_identity(stored_cfg.get_request_uri(repo.root)): + if inserted_to and opts['wot']: + # TODO: Imports don't go out of scope, right? The variables + # from the import are only visible in the function, so yes. import wot wot.update_repo_listing(ui_, attributes['Identity']) @@ -213,10 +213,15 @@ def infocalypse_pull(ui_, repo, **opts): def infocalypse_pull_request(ui, repo, **opts): + import wot if not opts['wot']: - ui.warning("Who do you want to send the pull request to? Set --wot.") + ui.warn("Who do you want to send the pull request to? Set --wot.\n") return + wot_id, repo_name = opts['wot'].split('/', 1) + wot.send_pull_request(ui, repo, get_truster(ui, repo, opts), wot_id, + repo_name) + def infocalypse_push(ui_, repo, **opts): """ Push to an Infocalypse repository in Freenet. """ diff --git a/infocalypse/config.py b/infocalypse/config.py --- a/infocalypse/config.py +++ b/infocalypse/config.py @@ -136,7 +136,14 @@ class Config: self.file_name = None # Use a dict instead of members to avoid pylint R0902. - self.defaults = {} # REDFLAG: Why is this called defaults? BAD NAME + + # REDFLAG: Why is this called defaults? BAD NAME. Perhaps 'values'? + # That would conflict with the .values() method of + # dictionaries. config.config sounds OK, I think (though it is + # a bit awkward to have the same name twice. But that is also + # done at other places, IIRC, so it should not be too + # surprising). + self.defaults = {} self.defaults['HOST'] = '127.0.0.1' self.defaults['PORT'] = 9481 self.defaults['TMP_DIR'] = None diff --git a/infocalypse/infcmds.py b/infocalypse/infcmds.py --- a/infocalypse/infcmds.py +++ b/infocalypse/infcmds.py @@ -495,8 +495,13 @@ def is_redundant(uri): ############################################################ # User feedback? success, failure? def execute_create(ui_, repo, params, stored_cfg): - """ Run the create command. """ + """ + Run the create command. + + Return the request URI on success, and None on failure. + """ update_sm = None + inserted_to = None try: update_sm = setup(ui_, repo, params, stored_cfg) # REDFLAG: Do better. @@ -516,9 +521,8 @@ def execute_create(ui_, repo, params, st run_until_quiescent(update_sm, params['POLL_SECS']) if update_sm.get_state(QUIESCENT).arrived_from(((FINISHING,))): - ui_.status("Inserted to:\n%s\n" % - '\n'.join(update_sm.get_state(INSERTING_URI). - get_request_uris())) + inserted_to = update_sm.get_state(INSERTING_URI).get_request_uris() + ui_.status("Inserted to:\n%s\n" % '\n'.join(inserted_to)) else: ui_.status("Create failed.\n") @@ -526,6 +530,8 @@ def execute_create(ui_, repo, params, st finally: cleanup(update_sm) + return inserted_to + # REDFLAG: LATER: make this work without a repo? def execute_copy(ui_, repo, params, stored_cfg): """ Run the copy command. """ diff --git a/infocalypse/wot.py b/infocalypse/wot.py --- a/infocalypse/wot.py +++ b/infocalypse/wot.py @@ -7,38 +7,133 @@ import smtplib from base64 import b32encode from fcp.node import base64decode from keys import USK +import yaml +from email.mime.text import MIMEText +import imaplib +FREEMAIL_SMTP_PORT = 4025 +FREEMAIL_IMAP_PORT = 4143 +PULL_REQUEST_PREFIX = "[vcs] " -def send_pull_request(ui, from_identity, to_identity): - local_identity = resolve_local_identity(ui, from_identity) - target_identity = resolve_identity(ui, from_identity, to_identity) - if local_identity is None or target_identity is None: - # Error. +def send_pull_request(ui, repo, from_identifier, to_identifier, to_repo_name): + local_identity = resolve_local_identity(ui, from_identifier) + if local_identity is None: + return + + target_identity = resolve_identity(ui, local_identity['Identity'], + to_identifier) + if target_identity is None: return from_address = to_freemail_address(local_identity) - to_address = to_freemail_address(to_identity) + to_address = to_freemail_address(target_identity) if from_address is None or to_address is None: if from_address is None: - ui.warn("{0} is not using Freemail.\n".format(from_identity[ - 'Nickname'])) + ui.warn("{0}@{1} is not using Freemail.\n".format(local_identity[ + 'Nickname'], local_identity['Identity'])) if to_address is None: - ui.warn("{0} is not using Freemail.\n".format(to_identity[ - 'Nickname'])) + ui.warn("{0}@{1} is not using Freemail.\n".format(target_identity[ + 'Nickname'], target_identity['Identity'])) return - # TODO: Use FCP host; default port. - smtp = smtplib.SMTP() - # TODO: Where to configure Freemail password? - smtp.login(from_address, ) - smtp.sendmail() + # Check that a password is set. + cfg = Config.from_ui(ui) + password = cfg.get_freemail_password(local_identity['Identity']) + if password is None: + ui.warn("{0} does not have a Freemail password set.\n" + "Run hg fn-setupfreemail --truster {0}@{1}\n" + .format(local_identity['Nickname'], local_identity['Identity'])) + return + + to_repo = find_repo(ui, local_identity['Identity'], to_identifier, + to_repo_name) + + # TODO: Frequently doing {0}@{1} ... would a WoTIdentity class make sense? + if to_repo is None: + ui.warn("{0}@{1} has not published a repository named '{2}'.\n" + .format(target_identity['Nickname'], + target_identity['Identifier'], + to_repo_name)) + return + + repo_context = repo['tip'] + # TODO: Will there always be a request URI set in the config? What about + # a path? The repo could be missing a request URI, if that URI is + # set manually. We could check whether the default path is a + # freenet path. We cannot be sure whether the request uri will + # always be the uri we want to send the pull-request to, though: + # It might be an URI we used to get some changes which we now want + # to send back to the maintainer of the canonical repo. + from_uri = cfg.get_request_uri(repo.root) + from_branch = repo_context.branch() + + # Use double-quoted scalars so that Unicode can be included. (Nicknames.) + footer = yaml.dump({'request': 'pull', + 'vcs': 'Infocalypse', + 'source': from_uri + '#' + from_branch, + 'target': to_repo}, default_style='"', + explicit_start=True, explicit_end=True, + allow_unicode=True) + + # TODO: Break config sanity check and sending apart so that further + # things can check config, prompt for whatever, then send. + + source_text = ui.edit(""" + +HG: Enter pull request message here. Lines beginning with 'HG:' are removed. +HG: The first line has "{0}" added before it and is the email subject. +HG: The second line should be blank. +HG: Following lines are the body of the message. +HG: Below is the machine-readable footer describing the request. Modifying it +HG: or putting things below it has the potential to cause problems. + +{1} +""".format(PULL_REQUEST_PREFIX, footer), from_identifier) + # TODO: Abort in the case of a blank message? + # Markdown support would be on receiving end. Maybe CLI preview eventually. + # (Would that even work?) + # TODO: Save message and load later in case sending fails. + + # TODO: What if the editor uses different line endings? How to slice + # by-line? Just use .splitlines() + source_lines = source_text.split('\n') + + # Body is third line and after. + msg = MIMEText('\n'.join(source_lines[2:])) + msg['Subject'] = PULL_REQUEST_PREFIX + source_lines[0] + msg['To'] = to_address + msg['From'] = from_address + + smtp = smtplib.SMTP(cfg.defaults['HOST'], FREEMAIL_SMTP_PORT) + smtp.login(from_address, password) + # TODO: Catch exceptions and give nice error messages. + smtp.sendmail(from_address, to_address, msg.as_string()) + + +def receive_pull_requests(ui): + # TODO: Terminology - send/receive different from resolve/read elsewhere. + # TODO: How to find YAML? Look from end backwards for "---\n" then forward + # from there for "...\n"? Yepp, that should be the simplest way. If the + # end is ... (without linebreak) that could be a user-error which we might + # accept: It is valid: http://www.yaml.org/spec/1.2/spec.html#id2760395 + # TODO: How to match local repo with the "target" URI? Based on repo list. + # all keys which have an insert key for this path would be pull-targets + # to check. Maybe retrieve the insert keys and invert them to get the + # request keys (or can we just use the insert keys to query Freemail?). + + cfg = Config.from_ui(ui) + imap = imaplib.IMAP4(cfg.defaults['HOST'], FREEMAIL_IMAP_PORT) + + type, message_numbers = imap.search(None, "SUBJECT", PULL_REQUEST_PREFIX) + print(type, message_numbers) def update_repo_listing(ui, for_identity): # TODO: WoT property containing edition. Used when requesting. config = Config.from_ui(ui) + # Version number to support possible format changes. root = ET.Element('vcs', {'version': '0'}) # Add request URIs associated with the given identity. @@ -417,14 +512,34 @@ def read_identity(message, id_num): # depend on and would allow just returning all properties for the identity. #property_prefix = "Replies.Properties{0}".format(id_num) - # Add contexts for the identity too. + # Add contexts and other properties. # TODO: Unflattening WoT response? Several places check for prefix like # this. - prefix = "Replies.Contexts{0}.Context".format(id_num) + context_prefix = "Replies.Contexts{0}.Context".format(id_num) + property_prefix = "Replies.Properties{0}.Property".format(id_num) for key in message.iterkeys(): - if key.startswith(prefix): - num = key[len(prefix):] + if key.startswith(context_prefix): + num = key[len(context_prefix):] result["Context{0}".format(num)] = message[key] + elif key.startswith(property_prefix) and key.endswith(".Name"): + # ".Name" is 5 characters, before which is the number. + num = key[len(property_prefix):-5] + + # Example: + # Replies.Properties1.Property1.Name = IntroductionPuzzleCount + # Replies.Properties1.Property1.Value = 10 + name = message[key] + value = message[property_prefix + num + '.Value'] + + # LCWoT returns many things with duplicates in properties, + # so this conflict is something that can happen. Checking for + # value conflict restricts the message to cases where it actually + # has an effect. + if name in result and value != result[name]: + print("WARNING: '{0}' has a different value as a property." + .format(name)) + + result[name] = value return result