diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8a533892c5392c34977dbdc93dcb72d832283c69..f86be7ae2bf86fb5e0e527ed959c5ddc257e369f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,5 +1,13 @@ Changelog ========= +0.23.2 +------ +* post detects now communities and set category correctly +* option added to remove time based suffix in derive_permlink +* beempy download added to save posts as markdown file +* beempy post imporved, automatic image upload, community support, patch generation on edit +* Unit test added for beempy download + 0.23.1 ------ * setproxy function added to Account (thanks to @flugschwein) diff --git a/beem/blockchaininstance.py b/beem/blockchaininstance.py index c1fd7c1c9d8c32eb7539110c12d9f6c920a832e6..3b220762d2f45b8c9c6fcaf20294df50cebd9f7d 100644 --- a/beem/blockchaininstance.py +++ b/beem/blockchaininstance.py @@ -1796,12 +1796,12 @@ class BlockChainInstance(object): :param str community: (Optional) Name of the community we are posting into. This will also override the community specified in - `json_metadata`. + `json_metadata` and the category :param str app: (Optional) Name of the app which are used for posting when not set, beem/<version> is used :param tags: (Optional) A list of tags to go with the post. This will also override the tags specified in - `json_metadata`. The first tag will be used as a 'category'. If + `json_metadata`. The first tag will be used as a 'category' when community is not specified. If provided as a string, it should be space separated. :type tags: str, list :param list beneficiaries: (Optional) A list of beneficiaries @@ -1836,7 +1836,7 @@ class BlockChainInstance(object): if app: json_metadata.update({'app': app}) elif 'app' not in json_metadata: - json_metadata.update({'app': 'beempy/%s' % (beem_version)}) + json_metadata.update({'app': 'beem/%s' % (beem_version)}) if not author and self.config["default_account"]: author = self.config["default_account"] @@ -1847,7 +1847,6 @@ class BlockChainInstance(object): if isinstance(tags, str): tags = list(set([_f for _f in (re.split(r"[\W_]", tags)) if _f])) - category = None tags = tags or json_metadata.get('tags', []) if parse_body: @@ -1891,8 +1890,15 @@ class BlockChainInstance(object): if tags: # first tag should be a category - category = tags[0] + if community is None: + category = tags[0] + else: + category = community json_metadata.update({"tags": tags}) + elif community: + category = community + else: + category = None # can't provide a category while replying to a post if reply_identifier and category: @@ -1917,11 +1923,11 @@ class BlockChainInstance(object): post_op = operations.Comment( **{ - "parent_author": parent_author, - "parent_permlink": parent_permlink, + "parent_author": parent_author.strip(), + "parent_permlink": parent_permlink.strip(), "author": account["name"], - "permlink": permlink, - "title": title, + "permlink": permlink.strip(), + "title": title.strip(), "body": body, "json_metadata": json_metadata }) diff --git a/beem/cli.py b/beem/cli.py index 757b60bacf918d9d78e407951a9dbc88ba887dd0..8b3186bcd967ebeba893005a05f70d1bb21801d3 100644 --- a/beem/cli.py +++ b/beem/cli.py @@ -38,7 +38,7 @@ from beem.hivesigner import HiveSigner from beem.asset import Asset from beem.witness import Witness, WitnessesRankedByVote, WitnessesVotedByAccount from beem.blockchain import Blockchain -from beem.utils import formatTimeString, construct_authorperm, derive_beneficiaries, derive_tags, seperate_yaml_dict_from_body +from beem.utils import formatTimeString, construct_authorperm, derive_beneficiaries, derive_tags, seperate_yaml_dict_from_body, derive_permlink from beem.vote import AccountVotes, ActiveVotes, Vote from beem import exceptions from beem.version import version as __version__ @@ -609,7 +609,6 @@ def walletinfo(unlock, lock): else: t.add_row(["keyring installed", "no"]) - if unlock: if unlock_wallet(stm, allow_wif=False): t.add_row(["Wallet unlock", "successful"]) @@ -1926,41 +1925,92 @@ def uploadimage(image, account, image_name): else: print("" % (image_name, tx["url"])) +@cli.command() +@click.argument('permlink', nargs=1) +@click.option('--account', '-a', help='Account are you posting from') +@click.option('--export', '-e', default=None, help="Export markdown to a md-file") +def download(permlink, account, export): + """Download body with yaml header""" + stm = shared_blockchain_instance() + if stm.rpc is not None: + stm.rpc.rpcconnect() + if account is None: + account = stm.config["default_account"] + if permlink[0] == "@": + authorperm = permlink + else: + authorperm = construct_authorperm(account, permlink) + comment = Comment(authorperm, blockchain_instance=stm) + if comment["parent_author"] != "" and comment["parent_permlink"] != "": + reply_identifier = construct_authorperm(comment["parent_author"], comment["parent_permlink"]) + else: + reply_identifier = None + + yaml_prefix = '---\n' + if comment["title"] != "": + yaml_prefix += 'title: %s\n' % comment["title"] + yaml_prefix += 'permlink: %s\n' % comment["permlink"] + yaml_prefix += 'author: %s\n' % comment["author"] + if "tags" in comment.json_metadata: + if len(comment.json_metadata["tags"]) > 0 and comment["category"] != comment.json_metadata["tags"][0] and len(comment["category"]) > 0: + yaml_prefix += 'community: %s\n' % comment["category"] + yaml_prefix += 'tags: %s\n' % ",".join(comment.json_metadata["tags"]) + if reply_identifier is not None: + yaml_prefix += 'reply_identifier: %s\n' % reply_identifier + yaml_prefix += '---\n' + if export is not None: + if export[-3:] != ".md": + export += ".md" + with open(export, "w", encoding="utf-8") as f: + f.write(yaml_prefix + comment["body"]) + else: + print(yaml_prefix + comment["body"]) + @cli.command() -@click.argument('body', nargs=1) +@click.argument('markdown-file', nargs=1) @click.option('--account', '-a', help='Account are you posting from') @click.option('--title', '-t', help='Title of the post') @click.option('--permlink', '-p', help='Manually set the permlink (optional)') -@click.option('--tags', help='A komma separated list of tags to go with the post.') -@click.option('--reply_identifier', help=' Identifier of the parent post/comment, when set a comment is broadcasted') -@click.option('--community', help=' Name of the community (optional)') +@click.option('--tags', '-g', help='A komma separated list of tags to go with the post.') +@click.option('--reply_identifier', '-r', help=' Identifier of the parent post/comment, when set a comment is broadcasted') +@click.option('--community', '-c', help=' Name of the community (optional)') @click.option('--beneficiaries', '-b', help='Post beneficiaries (komma separated, e.g. a:10%,b:20%)') -@click.option('--percent-steem-dollars', '-b', help='50% SBD /50% SP is 10000 (default), 100% SP is 0') -@click.option('--max-accepted-payout', '-b', help='Default is 1000000.000 [SBD]') -@click.option('--no-parse-body', help='Disable parsing of links, tags and images', is_flag=True, default=False) -def post(body, account, title, permlink, tags, reply_identifier, community, beneficiaries, percent_steem_dollars, max_accepted_payout, no_parse_body): - """broadcasts a post/comment""" +@click.option('--percent-steem-dollars', '-d', help='50% SBD /50% SP is 10000 (default), 100% SP is 0') +@click.option('--max-accepted-payout', '-m', help='Default is 1000000.000 [SBD]') +@click.option('--no-parse-body', '-n', help='Disable parsing of links, tags and images', is_flag=True, default=False) +@click.option('--no-patch-on-edit', '-e', help='Disable patch posting on edits (when the permlink already exists)', is_flag=True, default=False) +def post(markdown_file, account, title, permlink, tags, reply_identifier, community, beneficiaries, percent_steem_dollars, max_accepted_payout, no_parse_body, no_patch_on_edit): + """broadcasts a post/comment. All image links which links to a file will be uploaded. + The yaml header can contain: + + --- + title: your title + tags: tag1,tag2 + community: hive-100000 + beneficiaries: beempy:5%,holger80:5% + --- + + """ stm = shared_blockchain_instance() if stm.rpc is not None: stm.rpc.rpcconnect() - if not account: - account = stm.config["default_account"] - author = account - if not unlock_wallet(stm): - return - with open(body) as f: + with open(markdown_file) as f: content = f.read() body, parameter = seperate_yaml_dict_from_body(content) if title is not None: parameter["title"] = title + if account is not None: + parameter["author"] = account if tags is not None: parameter["tags"] = tags if permlink is not None: parameter["permlink"] = permlink if beneficiaries is not None: parameter["beneficiaries"] = beneficiaries + if community is not None: + parameter["community"] = community if reply_identifier is not None: parameter["reply_identifier"] = reply_identifier if percent_steem_dollars is not None: @@ -1971,6 +2021,10 @@ def post(body, account, title, permlink, tags, reply_identifier, community, bene parameter["max_accepted_payout"] = max_accepted_payout elif "max-accepted-payout" in parameter: parameter["max_accepted_payout"] = parameter["max-accepted-payout"] + + if not unlock_wallet(stm): + return + tags = None if "tags" in parameter: tags = derive_tags(parameter["tags"]) @@ -1979,6 +2033,8 @@ def post(body, account, title, permlink, tags, reply_identifier, community, bene title = parameter["title"] if "author" in parameter: author = parameter["author"] + else: + author = stm.config["default_account"] permlink = None if "permlink" in parameter: permlink = parameter["permlink"] @@ -2014,8 +2070,54 @@ def post(body, account, title, permlink, tags, reply_identifier, community, bene beneficiaries = derive_beneficiaries(parameter["beneficiaries"]) for b in beneficiaries: Account(b["account"], blockchain_instance=stm) - tx = stm.post(title, body, author=author, permlink=permlink, reply_identifier=reply_identifier, community=community, - tags=tags, comment_options=comment_options, beneficiaries=beneficiaries, parse_body=parse_body) + + if permlink is not None: + try: + comment = Comment(construct_authorperm(author, permlink), blockchain_instance=stm) + except: + comment = None + else: + comment = None + + iu = ImageUploader(blockchain_instance=stm) + for link in list(re.findall(r'!\[[^"\'@\]\(]*\]\([^"\'@\(\)]*\.(?:png|jpg|jpeg|gif|png|svg)\)', body)): + image = link.split("(")[1].split(")")[0] + image_name = link.split("![")[1].split("]")[0] + if image[:4] == "http": + continue + if stm.unsigned: + continue + if os.path.exists(image): + tx = iu.upload(image, author, image_name) + body = body.replace(image, tx["url"]) + + + if comment is None and permlink is None and reply_identifier is None: + permlink = derive_permlink(title, with_suffix=False) + try: + comment = Comment(construct_authorperm(author, permlink), blockchain_instance=stm) + except: + comment = None + if comment is not None: + edit_ok = click.prompt("Should I edit %s [y/n]" % (str(permlink))) + if edit_ok not in ["y", "ye", "yes"]: + permlink = derive_permlink(title, with_suffix=True) + comment = None + + if comment is None or no_patch_on_edit: + + if reply_identifier is None and (len(tags) == 0 or tags is None): + raise ValueError("Tags must not be empty!") + tx = stm.post(title, body, author=author, permlink=permlink, reply_identifier=reply_identifier, community=community, + tags=tags, comment_options=comment_options, beneficiaries=beneficiaries, parse_body=parse_body, + app='beempy/%s' % (__version__)) + else: + import diff_match_patch as dmp_module + dmp = dmp_module.diff_match_patch() + patch = dmp.patch_make(comment.body, body) + patch_text = dmp.patch_toText(patch) + tx = stm.post(title, patch_text, author=author, permlink=permlink, + tags=tags, parse_body=parse_body, app='beempy/%s' % (__version__)) if stm.unsigned and stm.nobroadcast and stm.steemconnect is not None: tx = stm.steemconnect.url_from_tx(tx) elif stm.unsigned and stm.nobroadcast and stm.hivesigner is not None: @@ -2042,7 +2144,8 @@ def reply(authorperm, body, account, title): if title is None: title = "" - tx = stm.post(title, body, json_metadata=None, author=account, reply_identifier=authorperm) + tx = stm.post(title, body, json_metadata=None, author=account, reply_identifier=authorperm, + app='beempy/%s' % (__version__)) if stm.unsigned and stm.nobroadcast and stm.steemconnect is not None: tx = stm.steemconnect.url_from_tx(tx) elif stm.unsigned and stm.nobroadcast and stm.hivesigner is not None: diff --git a/beem/utils.py b/beem/utils.py index 5c410cdbea5d52f5b26a36b2f75a85ac33b5097f..43ac0a9df55291c629b15c35614ced89e0e984a5 100644 --- a/beem/utils.py +++ b/beem/utils.py @@ -111,7 +111,7 @@ def sanitize_permlink(permlink): def derive_permlink(title, parent_permlink=None, parent_author=None, - max_permlink_length=256): + max_permlink_length=256, with_suffix=True): """Derive a permlink from a comment title (for root level comments) or the parent permlink and optionally the parent author (for replies). @@ -120,20 +120,38 @@ def derive_permlink(title, parent_permlink=None, parent_author=None, suffix = "-" + formatTime(datetime.utcnow()) + "z" if parent_permlink and parent_author: prefix = "re-" + sanitize_permlink(parent_author) + "-" - rem_chars = max_permlink_length - len(suffix) - len(prefix) + if with_suffix: + rem_chars = max_permlink_length - len(suffix) - len(prefix) + else: + rem_chars = max_permlink_length - len(prefix) body = sanitize_permlink(parent_permlink)[:rem_chars] - return prefix + body + suffix + if with_suffix: + return prefix + body + suffix + else: + return prefix + body elif parent_permlink: prefix = "re-" - rem_chars = max_permlink_length - len(suffix) - len(prefix) + if with_suffix: + rem_chars = max_permlink_length - len(suffix) - len(prefix) + else: + rem_chars = max_permlink_length - len(prefix) body = sanitize_permlink(parent_permlink)[:rem_chars] - return prefix + body + suffix + if with_suffix: + return prefix + body + suffix + else: + return prefix + body else: - rem_chars = max_permlink_length - len(suffix) + if with_suffix: + rem_chars = max_permlink_length - len(suffix) + else: + rem_chars = max_permlink_length body = sanitize_permlink(title)[:rem_chars] if len(body) == 0: # empty title or title consisted of only special chars return suffix[1:] # use timestamp only, strip leading "-" - return body + suffix + if with_suffix: + return body + suffix + else: + return body def resolve_authorperm(identifier): @@ -347,6 +365,8 @@ def derive_tags(tags): elif len(tags.split(" ")) > 1: for tag in tags.split(" "): tags_list.append(tag.strip()) + elif len(tags) > 0: + tags_list.append(tags.strip()) return tags_list diff --git a/tests/beem/test_cli.py b/tests/beem/test_cli.py index e5ae55495c42019d5783a67900f1df2de47d7c5e..4db91caea9f24d59d863043524a47150b3cd1cca 100644 --- a/tests/beem/test_cli.py +++ b/tests/beem/test_cli.py @@ -172,6 +172,13 @@ class Testcases(unittest.TestCase): result = runner.invoke(cli, ['-dt', 'downvote', '--weight', '100', '@steemit/firstpost'], input="test\n") self.assertEqual(result.exit_code, 0) + def test_download(self): + runner = CliRunner() + result = runner.invoke(cli, ['-dt', 'download', '-a', 'steemit', 'firstpost']) + self.assertEqual(result.exit_code, 0) + result = runner.invoke(cli, ['-dt', 'download', '@steemit/firstpost']) + self.assertEqual(result.exit_code, 0) + def test_transfer(self): stm = shared_steem_instance() runner = CliRunner()