(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