hive-nectar 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of hive-nectar might be problematic. Click here for more details.
- hive_nectar-0.0.2.dist-info/METADATA +182 -0
- hive_nectar-0.0.2.dist-info/RECORD +86 -0
- hive_nectar-0.0.2.dist-info/WHEEL +4 -0
- hive_nectar-0.0.2.dist-info/entry_points.txt +2 -0
- hive_nectar-0.0.2.dist-info/licenses/LICENSE.txt +23 -0
- nectar/__init__.py +32 -0
- nectar/account.py +4371 -0
- nectar/amount.py +475 -0
- nectar/asciichart.py +270 -0
- nectar/asset.py +82 -0
- nectar/block.py +446 -0
- nectar/blockchain.py +1178 -0
- nectar/blockchaininstance.py +2284 -0
- nectar/blockchainobject.py +221 -0
- nectar/blurt.py +563 -0
- nectar/cli.py +6285 -0
- nectar/comment.py +1217 -0
- nectar/community.py +513 -0
- nectar/constants.py +111 -0
- nectar/conveyor.py +309 -0
- nectar/discussions.py +1709 -0
- nectar/exceptions.py +149 -0
- nectar/hive.py +546 -0
- nectar/hivesigner.py +420 -0
- nectar/imageuploader.py +72 -0
- nectar/instance.py +129 -0
- nectar/market.py +1013 -0
- nectar/memo.py +449 -0
- nectar/message.py +357 -0
- nectar/nodelist.py +444 -0
- nectar/price.py +557 -0
- nectar/profile.py +65 -0
- nectar/rc.py +308 -0
- nectar/snapshot.py +726 -0
- nectar/steem.py +582 -0
- nectar/storage.py +53 -0
- nectar/transactionbuilder.py +622 -0
- nectar/utils.py +545 -0
- nectar/version.py +2 -0
- nectar/vote.py +557 -0
- nectar/wallet.py +472 -0
- nectar/witness.py +617 -0
- nectarapi/__init__.py +11 -0
- nectarapi/exceptions.py +123 -0
- nectarapi/graphenerpc.py +589 -0
- nectarapi/node.py +178 -0
- nectarapi/noderpc.py +229 -0
- nectarapi/rpcutils.py +97 -0
- nectarapi/version.py +2 -0
- nectarbase/__init__.py +14 -0
- nectarbase/ledgertransactions.py +75 -0
- nectarbase/memo.py +243 -0
- nectarbase/objects.py +429 -0
- nectarbase/objecttypes.py +22 -0
- nectarbase/operationids.py +102 -0
- nectarbase/operations.py +1297 -0
- nectarbase/signedtransactions.py +48 -0
- nectarbase/transactions.py +11 -0
- nectarbase/version.py +2 -0
- nectargrapheneapi/__init__.py +6 -0
- nectargraphenebase/__init__.py +27 -0
- nectargraphenebase/account.py +846 -0
- nectargraphenebase/aes.py +52 -0
- nectargraphenebase/base58.py +192 -0
- nectargraphenebase/bip32.py +494 -0
- nectargraphenebase/bip38.py +134 -0
- nectargraphenebase/chains.py +149 -0
- nectargraphenebase/dictionary.py +3 -0
- nectargraphenebase/ecdsasig.py +326 -0
- nectargraphenebase/objects.py +123 -0
- nectargraphenebase/objecttypes.py +6 -0
- nectargraphenebase/operationids.py +3 -0
- nectargraphenebase/operations.py +23 -0
- nectargraphenebase/prefix.py +11 -0
- nectargraphenebase/py23.py +38 -0
- nectargraphenebase/signedtransactions.py +201 -0
- nectargraphenebase/types.py +419 -0
- nectargraphenebase/unsignedtransactions.py +283 -0
- nectargraphenebase/version.py +2 -0
- nectarstorage/__init__.py +38 -0
- nectarstorage/base.py +306 -0
- nectarstorage/exceptions.py +16 -0
- nectarstorage/interfaces.py +237 -0
- nectarstorage/masterpassword.py +239 -0
- nectarstorage/ram.py +30 -0
- nectarstorage/sqlite.py +334 -0
nectar/comment.py
ADDED
|
@@ -0,0 +1,1217 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import math
|
|
5
|
+
from datetime import date, datetime, timezone
|
|
6
|
+
|
|
7
|
+
from nectar.constants import (
|
|
8
|
+
STEEM_REVERSE_AUCTION_WINDOW_SECONDS_HF6,
|
|
9
|
+
STEEM_REVERSE_AUCTION_WINDOW_SECONDS_HF20,
|
|
10
|
+
STEEM_REVERSE_AUCTION_WINDOW_SECONDS_HF21,
|
|
11
|
+
)
|
|
12
|
+
from nectarbase import operations
|
|
13
|
+
from nectargraphenebase.py23 import bytes_types, integer_types, string_types
|
|
14
|
+
|
|
15
|
+
from .account import Account
|
|
16
|
+
from .amount import Amount
|
|
17
|
+
from .blockchainobject import BlockchainObject
|
|
18
|
+
from .exceptions import ContentDoesNotExistsException, VotingInvalidOnArchivedPost
|
|
19
|
+
from .instance import shared_blockchain_instance
|
|
20
|
+
from .price import Price
|
|
21
|
+
from .utils import (
|
|
22
|
+
construct_authorperm,
|
|
23
|
+
formatTimeString,
|
|
24
|
+
formatToTimeStamp,
|
|
25
|
+
make_patch,
|
|
26
|
+
resolve_authorperm,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
log = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Comment(BlockchainObject):
|
|
33
|
+
"""Read data about a Comment/Post in the chain
|
|
34
|
+
|
|
35
|
+
:param str authorperm: identifier to post/comment in the form of
|
|
36
|
+
``@author/permlink``
|
|
37
|
+
:param str tags: defines which api is used. Can be bridge, tags, condenser or database (default = bridge)
|
|
38
|
+
:param Hive blockchain_instance: :class:`nectar.hive.Steem` instance to use when accessing a RPC
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
.. code-block:: python
|
|
42
|
+
|
|
43
|
+
>>> from nectar.comment import Comment
|
|
44
|
+
>>> from nectar.account import Account
|
|
45
|
+
>>> from nectar import Steem
|
|
46
|
+
>>> stm = Steem()
|
|
47
|
+
>>> acc = Account("gtg", blockchain_instance=stm)
|
|
48
|
+
>>> authorperm = acc.get_blog(limit=1)[0]["authorperm"]
|
|
49
|
+
>>> c = Comment(authorperm)
|
|
50
|
+
>>> postdate = c["created"]
|
|
51
|
+
>>> postdate_str = c.json()["created"]
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
type_id = 8
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
authorperm,
|
|
60
|
+
api="bridge",
|
|
61
|
+
observer="",
|
|
62
|
+
full=True,
|
|
63
|
+
lazy=False,
|
|
64
|
+
blockchain_instance=None,
|
|
65
|
+
**kwargs,
|
|
66
|
+
):
|
|
67
|
+
self.full = full
|
|
68
|
+
self.lazy = lazy
|
|
69
|
+
self.api = api
|
|
70
|
+
self.observer = observer
|
|
71
|
+
if blockchain_instance is None:
|
|
72
|
+
if kwargs.get("steem_instance"):
|
|
73
|
+
blockchain_instance = kwargs["steem_instance"]
|
|
74
|
+
elif kwargs.get("hive_instance"):
|
|
75
|
+
blockchain_instance = kwargs["hive_instance"]
|
|
76
|
+
self.blockchain = blockchain_instance or shared_blockchain_instance()
|
|
77
|
+
if isinstance(authorperm, string_types) and authorperm != "":
|
|
78
|
+
[author, permlink] = resolve_authorperm(authorperm)
|
|
79
|
+
self["id"] = 0
|
|
80
|
+
self["author"] = author
|
|
81
|
+
self["permlink"] = permlink
|
|
82
|
+
self["authorperm"] = authorperm
|
|
83
|
+
elif isinstance(authorperm, dict) and "author" in authorperm and "permlink" in authorperm:
|
|
84
|
+
authorperm["authorperm"] = construct_authorperm(
|
|
85
|
+
authorperm["author"], authorperm["permlink"]
|
|
86
|
+
)
|
|
87
|
+
authorperm = self._parse_json_data(authorperm)
|
|
88
|
+
super(Comment, self).__init__(
|
|
89
|
+
authorperm,
|
|
90
|
+
id_item="authorperm",
|
|
91
|
+
lazy=lazy,
|
|
92
|
+
full=full,
|
|
93
|
+
blockchain_instance=blockchain_instance,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def _parse_json_data(self, comment):
|
|
97
|
+
parse_times = [
|
|
98
|
+
"active",
|
|
99
|
+
"cashout_time",
|
|
100
|
+
"created",
|
|
101
|
+
"last_payout",
|
|
102
|
+
"last_update",
|
|
103
|
+
"updated",
|
|
104
|
+
"max_cashout_time",
|
|
105
|
+
]
|
|
106
|
+
for p in parse_times:
|
|
107
|
+
if p in comment and isinstance(comment.get(p), string_types):
|
|
108
|
+
comment[p] = formatTimeString(comment.get(p, "1970-01-01T00:00:00"))
|
|
109
|
+
# Parse Amounts
|
|
110
|
+
sbd_amounts = [
|
|
111
|
+
"total_payout_value",
|
|
112
|
+
"max_accepted_payout",
|
|
113
|
+
"pending_payout_value",
|
|
114
|
+
"curator_payout_value",
|
|
115
|
+
"total_pending_payout_value",
|
|
116
|
+
"promoted",
|
|
117
|
+
]
|
|
118
|
+
for p in sbd_amounts:
|
|
119
|
+
if p in comment and isinstance(comment.get(p), (string_types, list, dict)):
|
|
120
|
+
value = comment.get(p, "0.000 %s" % (self.blockchain.backed_token_symbol))
|
|
121
|
+
if (
|
|
122
|
+
isinstance(value, str)
|
|
123
|
+
and value.split(" ")[1] != self.blockchain.backed_token_symbol
|
|
124
|
+
):
|
|
125
|
+
value = value.split(" ")[0] + " " + self.blockchain.backed_token_symbol
|
|
126
|
+
comment[p] = Amount(value, blockchain_instance=self.blockchain)
|
|
127
|
+
|
|
128
|
+
# turn json_metadata into python dict
|
|
129
|
+
meta_str = comment.get("json_metadata", "{}")
|
|
130
|
+
if meta_str == "{}":
|
|
131
|
+
comment["json_metadata"] = meta_str
|
|
132
|
+
if isinstance(meta_str, (string_types, bytes_types, bytearray)):
|
|
133
|
+
try:
|
|
134
|
+
comment["json_metadata"] = json.loads(meta_str)
|
|
135
|
+
except Exception:
|
|
136
|
+
comment["json_metadata"] = {}
|
|
137
|
+
|
|
138
|
+
comment["tags"] = []
|
|
139
|
+
comment["community"] = ""
|
|
140
|
+
if isinstance(comment["json_metadata"], dict):
|
|
141
|
+
if "tags" in comment["json_metadata"]:
|
|
142
|
+
comment["tags"] = comment["json_metadata"]["tags"]
|
|
143
|
+
if "community" in comment["json_metadata"]:
|
|
144
|
+
comment["community"] = comment["json_metadata"]["community"]
|
|
145
|
+
|
|
146
|
+
parse_int = [
|
|
147
|
+
"author_reputation",
|
|
148
|
+
"net_rshares",
|
|
149
|
+
]
|
|
150
|
+
for p in parse_int:
|
|
151
|
+
if p in comment and isinstance(comment.get(p), string_types):
|
|
152
|
+
comment[p] = int(comment.get(p, "0"))
|
|
153
|
+
|
|
154
|
+
if "active_votes" in comment:
|
|
155
|
+
new_active_votes = []
|
|
156
|
+
for vote in comment["active_votes"]:
|
|
157
|
+
if "time" in vote and isinstance(vote.get("time"), string_types):
|
|
158
|
+
vote["time"] = formatTimeString(vote.get("time", "1970-01-01T00:00:00"))
|
|
159
|
+
parse_int = [
|
|
160
|
+
"rshares",
|
|
161
|
+
"reputation",
|
|
162
|
+
]
|
|
163
|
+
for p in parse_int:
|
|
164
|
+
if p in vote and isinstance(vote.get(p), string_types):
|
|
165
|
+
try:
|
|
166
|
+
vote[p] = int(vote.get(p, "0"))
|
|
167
|
+
except ValueError:
|
|
168
|
+
vote[p] = int(0)
|
|
169
|
+
new_active_votes.append(vote)
|
|
170
|
+
comment["active_votes"] = new_active_votes
|
|
171
|
+
return comment
|
|
172
|
+
|
|
173
|
+
def refresh(self):
|
|
174
|
+
if self.identifier == "":
|
|
175
|
+
return
|
|
176
|
+
if not self.blockchain.is_connected():
|
|
177
|
+
return
|
|
178
|
+
[author, permlink] = resolve_authorperm(self.identifier)
|
|
179
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(True)
|
|
180
|
+
if self.blockchain.rpc.get_use_appbase():
|
|
181
|
+
from nectarapi.exceptions import InvalidParameters
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
if self.api == "tags":
|
|
185
|
+
content = self.blockchain.rpc.get_discussion(
|
|
186
|
+
{"author": author, "permlink": permlink}, api="tags"
|
|
187
|
+
)
|
|
188
|
+
elif self.api == "database":
|
|
189
|
+
content = self.blockchain.rpc.list_comments(
|
|
190
|
+
{"start": [author, permlink], "limit": 1, "order": "by_permlink"},
|
|
191
|
+
api="database",
|
|
192
|
+
)
|
|
193
|
+
elif self.api == "bridge":
|
|
194
|
+
content = self.blockchain.rpc.get_post(
|
|
195
|
+
{"author": author, "permlink": permlink, "observer": self.observer},
|
|
196
|
+
api="bridge",
|
|
197
|
+
)
|
|
198
|
+
elif self.api == "condenser":
|
|
199
|
+
content = self.blockchain.rpc.get_content(author, permlink, api="condenser")
|
|
200
|
+
else:
|
|
201
|
+
raise ValueError("api must be: tags, database, bridge or condenser")
|
|
202
|
+
if content is not None and "comments" in content:
|
|
203
|
+
content = content["comments"]
|
|
204
|
+
if isinstance(content, list) and len(content) > 0:
|
|
205
|
+
content = content[0]
|
|
206
|
+
except InvalidParameters:
|
|
207
|
+
raise ContentDoesNotExistsException(self.identifier)
|
|
208
|
+
else:
|
|
209
|
+
from nectarapi.exceptions import InvalidParameters
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
content = self.blockchain.rpc.get_content(author, permlink)
|
|
213
|
+
except InvalidParameters:
|
|
214
|
+
raise ContentDoesNotExistsException(self.identifier)
|
|
215
|
+
if not content or not content["author"] or not content["permlink"]:
|
|
216
|
+
raise ContentDoesNotExistsException(self.identifier)
|
|
217
|
+
content = self._parse_json_data(content)
|
|
218
|
+
content["authorperm"] = construct_authorperm(content["author"], content["permlink"])
|
|
219
|
+
super(Comment, self).__init__(
|
|
220
|
+
content,
|
|
221
|
+
id_item="authorperm",
|
|
222
|
+
lazy=self.lazy,
|
|
223
|
+
full=self.full,
|
|
224
|
+
blockchain_instance=self.blockchain,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
def json(self):
|
|
228
|
+
output = self.copy()
|
|
229
|
+
if "authorperm" in output:
|
|
230
|
+
output.pop("authorperm")
|
|
231
|
+
if "json_metadata" in output:
|
|
232
|
+
output["json_metadata"] = json.dumps(output["json_metadata"], separators=[",", ":"])
|
|
233
|
+
if "tags" in output:
|
|
234
|
+
output.pop("tags")
|
|
235
|
+
if "community" in output:
|
|
236
|
+
output.pop("community")
|
|
237
|
+
parse_times = [
|
|
238
|
+
"active",
|
|
239
|
+
"cashout_time",
|
|
240
|
+
"created",
|
|
241
|
+
"last_payout",
|
|
242
|
+
"last_update",
|
|
243
|
+
"updated",
|
|
244
|
+
"max_cashout_time",
|
|
245
|
+
]
|
|
246
|
+
for p in parse_times:
|
|
247
|
+
if p in output:
|
|
248
|
+
p_date = output.get(p, datetime(1970, 1, 1, 0, 0))
|
|
249
|
+
if isinstance(p_date, (datetime, date)):
|
|
250
|
+
output[p] = formatTimeString(p_date)
|
|
251
|
+
else:
|
|
252
|
+
output[p] = p_date
|
|
253
|
+
sbd_amounts = [
|
|
254
|
+
"total_payout_value",
|
|
255
|
+
"max_accepted_payout",
|
|
256
|
+
"pending_payout_value",
|
|
257
|
+
"curator_payout_value",
|
|
258
|
+
"total_pending_payout_value",
|
|
259
|
+
"promoted",
|
|
260
|
+
]
|
|
261
|
+
for p in sbd_amounts:
|
|
262
|
+
if p in output and isinstance(output[p], Amount):
|
|
263
|
+
output[p] = output[p].json()
|
|
264
|
+
parse_int = [
|
|
265
|
+
"author_reputation",
|
|
266
|
+
"net_rshares",
|
|
267
|
+
]
|
|
268
|
+
for p in parse_int:
|
|
269
|
+
if p in output and isinstance(output[p], integer_types):
|
|
270
|
+
output[p] = str(output[p])
|
|
271
|
+
if "active_votes" in output:
|
|
272
|
+
new_active_votes = []
|
|
273
|
+
for vote in output["active_votes"]:
|
|
274
|
+
if "time" in vote:
|
|
275
|
+
p_date = vote.get("time", datetime(1970, 1, 1, 0, 0))
|
|
276
|
+
if isinstance(p_date, (datetime, date)):
|
|
277
|
+
vote["time"] = formatTimeString(p_date)
|
|
278
|
+
else:
|
|
279
|
+
vote["time"] = p_date
|
|
280
|
+
parse_int = [
|
|
281
|
+
"rshares",
|
|
282
|
+
"reputation",
|
|
283
|
+
]
|
|
284
|
+
for p in parse_int:
|
|
285
|
+
if p in vote and isinstance(vote[p], integer_types):
|
|
286
|
+
vote[p] = str(vote[p])
|
|
287
|
+
new_active_votes.append(vote)
|
|
288
|
+
output["active_votes"] = new_active_votes
|
|
289
|
+
return json.loads(str(json.dumps(output)))
|
|
290
|
+
|
|
291
|
+
@property
|
|
292
|
+
def id(self):
|
|
293
|
+
return self["id"]
|
|
294
|
+
|
|
295
|
+
@property
|
|
296
|
+
def author(self):
|
|
297
|
+
return self["author"]
|
|
298
|
+
|
|
299
|
+
@property
|
|
300
|
+
def permlink(self):
|
|
301
|
+
return self["permlink"]
|
|
302
|
+
|
|
303
|
+
@property
|
|
304
|
+
def authorperm(self):
|
|
305
|
+
return construct_authorperm(self["author"], self["permlink"])
|
|
306
|
+
|
|
307
|
+
@property
|
|
308
|
+
def category(self):
|
|
309
|
+
if "category" in self:
|
|
310
|
+
return self["category"]
|
|
311
|
+
else:
|
|
312
|
+
return ""
|
|
313
|
+
|
|
314
|
+
@property
|
|
315
|
+
def parent_author(self):
|
|
316
|
+
if "parent_author" in self:
|
|
317
|
+
return self["parent_author"]
|
|
318
|
+
else:
|
|
319
|
+
return ""
|
|
320
|
+
|
|
321
|
+
@property
|
|
322
|
+
def parent_permlink(self):
|
|
323
|
+
if "parent_permlink" in self:
|
|
324
|
+
return self["parent_permlink"]
|
|
325
|
+
else:
|
|
326
|
+
return ""
|
|
327
|
+
|
|
328
|
+
@property
|
|
329
|
+
def depth(self):
|
|
330
|
+
return self["depth"]
|
|
331
|
+
|
|
332
|
+
@property
|
|
333
|
+
def title(self):
|
|
334
|
+
if "title" in self:
|
|
335
|
+
return self["title"]
|
|
336
|
+
else:
|
|
337
|
+
return ""
|
|
338
|
+
|
|
339
|
+
@property
|
|
340
|
+
def body(self):
|
|
341
|
+
if "body" in self:
|
|
342
|
+
return self["body"]
|
|
343
|
+
else:
|
|
344
|
+
return ""
|
|
345
|
+
|
|
346
|
+
@property
|
|
347
|
+
def json_metadata(self):
|
|
348
|
+
if "json_metadata" in self:
|
|
349
|
+
return self["json_metadata"]
|
|
350
|
+
else:
|
|
351
|
+
return {}
|
|
352
|
+
|
|
353
|
+
def is_main_post(self):
|
|
354
|
+
"""Returns True if main post, and False if this is a comment (reply)."""
|
|
355
|
+
if "depth" in self:
|
|
356
|
+
return self["depth"] == 0
|
|
357
|
+
else:
|
|
358
|
+
return self["parent_author"] == ""
|
|
359
|
+
|
|
360
|
+
def is_comment(self):
|
|
361
|
+
"""Returns True if post is a comment"""
|
|
362
|
+
if "depth" in self:
|
|
363
|
+
return self["depth"] > 0
|
|
364
|
+
else:
|
|
365
|
+
return self["parent_author"] != ""
|
|
366
|
+
|
|
367
|
+
@property
|
|
368
|
+
def reward(self):
|
|
369
|
+
"""Return the estimated total SBD reward."""
|
|
370
|
+
a_zero = Amount(0, self.blockchain.backed_token_symbol, blockchain_instance=self.blockchain)
|
|
371
|
+
author = Amount(self.get("total_payout_value", a_zero), blockchain_instance=self.blockchain)
|
|
372
|
+
curator = Amount(
|
|
373
|
+
self.get("curator_payout_value", a_zero), blockchain_instance=self.blockchain
|
|
374
|
+
)
|
|
375
|
+
pending = Amount(
|
|
376
|
+
self.get("pending_payout_value", a_zero), blockchain_instance=self.blockchain
|
|
377
|
+
)
|
|
378
|
+
return author + curator + pending
|
|
379
|
+
|
|
380
|
+
def is_pending(self):
|
|
381
|
+
"""Returns if the payout is pending (the post/comment
|
|
382
|
+
is younger than 7 days)
|
|
383
|
+
"""
|
|
384
|
+
a_zero = Amount(0, self.blockchain.backed_token_symbol, blockchain_instance=self.blockchain)
|
|
385
|
+
total = Amount(self.get("total_payout_value", a_zero), blockchain_instance=self.blockchain)
|
|
386
|
+
post_age_days = self.time_elapsed().total_seconds() / 60 / 60 / 24
|
|
387
|
+
return post_age_days < 7.0 and float(total) == 0
|
|
388
|
+
|
|
389
|
+
def time_elapsed(self):
|
|
390
|
+
"""Returns a timedelta on how old the post is."""
|
|
391
|
+
return datetime.now(timezone.utc) - self["created"]
|
|
392
|
+
|
|
393
|
+
def curation_penalty_compensation_SBD(self):
|
|
394
|
+
"""Returns The required post payout amount after 15 minutes
|
|
395
|
+
which will compentsate the curation penalty, if voting earlier than 15 minutes
|
|
396
|
+
"""
|
|
397
|
+
self.refresh()
|
|
398
|
+
if self.blockchain.hardfork >= 21:
|
|
399
|
+
reverse_auction_window_seconds = STEEM_REVERSE_AUCTION_WINDOW_SECONDS_HF21
|
|
400
|
+
elif self.blockchain.hardfork >= 20:
|
|
401
|
+
reverse_auction_window_seconds = STEEM_REVERSE_AUCTION_WINDOW_SECONDS_HF20
|
|
402
|
+
else:
|
|
403
|
+
reverse_auction_window_seconds = STEEM_REVERSE_AUCTION_WINDOW_SECONDS_HF6
|
|
404
|
+
return (
|
|
405
|
+
self.reward
|
|
406
|
+
* reverse_auction_window_seconds
|
|
407
|
+
/ ((self.time_elapsed()).total_seconds() / 60) ** 2
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
def estimate_curation_SBD(self, vote_value_SBD, estimated_value_SBD=None):
|
|
411
|
+
"""Estimates curation reward
|
|
412
|
+
|
|
413
|
+
:param float vote_value_SBD: The vote value in SBD for which the curation
|
|
414
|
+
should be calculated
|
|
415
|
+
:param float estimated_value_SBD: When set, this value is used for calculate
|
|
416
|
+
the curation. When not set, the current post value is used.
|
|
417
|
+
"""
|
|
418
|
+
self.refresh()
|
|
419
|
+
if estimated_value_SBD is None:
|
|
420
|
+
estimated_value_SBD = float(self.reward)
|
|
421
|
+
t = 1.0 - self.get_curation_penalty()
|
|
422
|
+
k = vote_value_SBD / (vote_value_SBD + float(self.reward))
|
|
423
|
+
K = (1 - math.sqrt(1 - k)) / 4 / k
|
|
424
|
+
return K * vote_value_SBD * t * math.sqrt(estimated_value_SBD)
|
|
425
|
+
|
|
426
|
+
def get_curation_penalty(self, vote_time=None):
|
|
427
|
+
"""If post is less than 5 minutes old, it will incur a curation
|
|
428
|
+
reward penalty.
|
|
429
|
+
|
|
430
|
+
:param datetime vote_time: A vote time can be given and the curation
|
|
431
|
+
penalty is calculated regarding the given time (default is None)
|
|
432
|
+
When set to None, the current date is used.
|
|
433
|
+
:returns: Float number between 0 and 1 (0.0 -> no penalty, 1.0 -> 100 % curation penalty)
|
|
434
|
+
:rtype: float
|
|
435
|
+
|
|
436
|
+
"""
|
|
437
|
+
if vote_time is None:
|
|
438
|
+
elapsed_seconds = self.time_elapsed().total_seconds()
|
|
439
|
+
elif isinstance(vote_time, str):
|
|
440
|
+
elapsed_seconds = (formatTimeString(vote_time) - self["created"]).total_seconds()
|
|
441
|
+
elif isinstance(vote_time, (datetime, date)):
|
|
442
|
+
elapsed_seconds = (vote_time - self["created"]).total_seconds()
|
|
443
|
+
else:
|
|
444
|
+
raise ValueError("vote_time must be a string or a datetime")
|
|
445
|
+
if self.blockchain.hardfork >= 21:
|
|
446
|
+
reward = elapsed_seconds / STEEM_REVERSE_AUCTION_WINDOW_SECONDS_HF21
|
|
447
|
+
elif self.blockchain.hardfork >= 20:
|
|
448
|
+
reward = elapsed_seconds / STEEM_REVERSE_AUCTION_WINDOW_SECONDS_HF20
|
|
449
|
+
else:
|
|
450
|
+
reward = elapsed_seconds / STEEM_REVERSE_AUCTION_WINDOW_SECONDS_HF6
|
|
451
|
+
if reward > 1:
|
|
452
|
+
reward = 1.0
|
|
453
|
+
return 1.0 - reward
|
|
454
|
+
|
|
455
|
+
def get_vote_with_curation(self, voter=None, raw_data=False, pending_payout_value=None):
|
|
456
|
+
"""Returns vote for voter. Returns None, if the voter cannot be found in `active_votes`.
|
|
457
|
+
|
|
458
|
+
:param str voter: Voter for which the vote should be returned
|
|
459
|
+
:param bool raw_data: If True, the raw data are returned
|
|
460
|
+
:param pending_payout_SBD: When not None this value instead of the current
|
|
461
|
+
value is used for calculating the rewards
|
|
462
|
+
:type pending_payout_SBD: float, str
|
|
463
|
+
|
|
464
|
+
"""
|
|
465
|
+
specific_vote = None
|
|
466
|
+
if voter is None:
|
|
467
|
+
voter = Account(self["author"], blockchain_instance=self.blockchain)
|
|
468
|
+
else:
|
|
469
|
+
voter = Account(voter, blockchain_instance=self.blockchain)
|
|
470
|
+
if "active_votes" in self:
|
|
471
|
+
for vote in self["active_votes"]:
|
|
472
|
+
if voter["name"] == vote["voter"]:
|
|
473
|
+
specific_vote = vote
|
|
474
|
+
else:
|
|
475
|
+
active_votes = self.get_votes()
|
|
476
|
+
for vote in active_votes:
|
|
477
|
+
if voter["name"] == vote["voter"]:
|
|
478
|
+
specific_vote = vote
|
|
479
|
+
if specific_vote is not None and (raw_data or not self.is_pending()):
|
|
480
|
+
return specific_vote
|
|
481
|
+
elif specific_vote is not None:
|
|
482
|
+
curation_reward = self.get_curation_rewards(
|
|
483
|
+
pending_payout_SBD=True, pending_payout_value=pending_payout_value
|
|
484
|
+
)
|
|
485
|
+
specific_vote["curation_reward"] = curation_reward["active_votes"][voter["name"]]
|
|
486
|
+
specific_vote["ROI"] = (
|
|
487
|
+
float(curation_reward["active_votes"][voter["name"]])
|
|
488
|
+
/ float(voter.get_voting_value_SBD(voting_weight=specific_vote["percent"] / 100))
|
|
489
|
+
* 100
|
|
490
|
+
)
|
|
491
|
+
return specific_vote
|
|
492
|
+
else:
|
|
493
|
+
return None
|
|
494
|
+
|
|
495
|
+
def get_beneficiaries_pct(self):
|
|
496
|
+
"""Returns the sum of all post beneficiaries in percentage"""
|
|
497
|
+
beneficiaries = self["beneficiaries"]
|
|
498
|
+
weight = 0
|
|
499
|
+
for b in beneficiaries:
|
|
500
|
+
weight += b["weight"]
|
|
501
|
+
return weight / 100.0
|
|
502
|
+
|
|
503
|
+
def get_rewards(self):
|
|
504
|
+
"""Returns the total_payout, author_payout and the curator payout in SBD.
|
|
505
|
+
When the payout is still pending, the estimated payout is given out.
|
|
506
|
+
|
|
507
|
+
.. note:: Potential beneficiary rewards were already deducted from the
|
|
508
|
+
`author_payout` and the `total_payout`
|
|
509
|
+
|
|
510
|
+
Example:::
|
|
511
|
+
|
|
512
|
+
{
|
|
513
|
+
'total_payout': 9.956 SBD,
|
|
514
|
+
'author_payout': 7.166 SBD,
|
|
515
|
+
'curator_payout': 2.790 SBD
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
"""
|
|
519
|
+
if self.is_pending():
|
|
520
|
+
total_payout = Amount(self["pending_payout_value"], blockchain_instance=self.blockchain)
|
|
521
|
+
author_payout = self.get_author_rewards()["total_payout_SBD"]
|
|
522
|
+
curator_payout = total_payout - author_payout
|
|
523
|
+
else:
|
|
524
|
+
author_payout = Amount(self["total_payout_value"], blockchain_instance=self.blockchain)
|
|
525
|
+
curator_payout = Amount(
|
|
526
|
+
self["curator_payout_value"], blockchain_instance=self.blockchain
|
|
527
|
+
)
|
|
528
|
+
total_payout = author_payout + curator_payout
|
|
529
|
+
return {
|
|
530
|
+
"total_payout": total_payout,
|
|
531
|
+
"author_payout": author_payout,
|
|
532
|
+
"curator_payout": curator_payout,
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
def get_author_rewards(self):
|
|
536
|
+
"""Returns the author rewards.
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
Example::
|
|
541
|
+
|
|
542
|
+
{
|
|
543
|
+
'pending_rewards': True,
|
|
544
|
+
'payout_SP': 0.912 STEEM,
|
|
545
|
+
'payout_SBD': 3.583 SBD,
|
|
546
|
+
'total_payout_SBD': 7.166 SBD
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
"""
|
|
550
|
+
if not self.is_pending():
|
|
551
|
+
return {
|
|
552
|
+
"pending_rewards": False,
|
|
553
|
+
"payout_SP": Amount(
|
|
554
|
+
0, self.blockchain.token_symbol, blockchain_instance=self.blockchain
|
|
555
|
+
),
|
|
556
|
+
"payout_SBD": Amount(
|
|
557
|
+
0, self.blockchain.backed_token_symbol, blockchain_instance=self.blockchain
|
|
558
|
+
),
|
|
559
|
+
"total_payout_SBD": Amount(
|
|
560
|
+
self["total_payout_value"], blockchain_instance=self.blockchain
|
|
561
|
+
),
|
|
562
|
+
}
|
|
563
|
+
author_reward_factor = 0.5
|
|
564
|
+
median_hist = self.blockchain.get_current_median_history()
|
|
565
|
+
if median_hist is not None:
|
|
566
|
+
median_price = Price(median_hist, blockchain_instance=self.blockchain)
|
|
567
|
+
beneficiaries_pct = self.get_beneficiaries_pct()
|
|
568
|
+
curation_tokens = self.reward * author_reward_factor
|
|
569
|
+
author_tokens = self.reward - curation_tokens
|
|
570
|
+
curation_rewards = self.get_curation_rewards()
|
|
571
|
+
if self.blockchain.hardfork >= 20 and median_hist is not None:
|
|
572
|
+
author_tokens += median_price * curation_rewards["unclaimed_rewards"]
|
|
573
|
+
|
|
574
|
+
benefactor_tokens = author_tokens * beneficiaries_pct / 100.0
|
|
575
|
+
author_tokens -= benefactor_tokens
|
|
576
|
+
|
|
577
|
+
if median_hist is not None and "percent_steem_dollars" in self:
|
|
578
|
+
sbd_steem = author_tokens * self["percent_steem_dollars"] / 20000.0
|
|
579
|
+
vesting_steem = median_price.as_base(self.blockchain.token_symbol) * (
|
|
580
|
+
author_tokens - sbd_steem
|
|
581
|
+
)
|
|
582
|
+
return {
|
|
583
|
+
"pending_rewards": True,
|
|
584
|
+
"payout_SP": vesting_steem,
|
|
585
|
+
"payout_SBD": sbd_steem,
|
|
586
|
+
"total_payout_SBD": author_tokens,
|
|
587
|
+
}
|
|
588
|
+
elif median_hist is not None and "percent_hbd" in self:
|
|
589
|
+
sbd_steem = author_tokens * self["percent_hbd"] / 20000.0
|
|
590
|
+
vesting_steem = median_price.as_base(self.blockchain.token_symbol) * (
|
|
591
|
+
author_tokens - sbd_steem
|
|
592
|
+
)
|
|
593
|
+
return {
|
|
594
|
+
"pending_rewards": True,
|
|
595
|
+
"payout_SP": vesting_steem,
|
|
596
|
+
"payout_SBD": sbd_steem,
|
|
597
|
+
"total_payout_SBD": author_tokens,
|
|
598
|
+
}
|
|
599
|
+
else:
|
|
600
|
+
return {
|
|
601
|
+
"pending_rewards": True,
|
|
602
|
+
"total_payout": author_tokens,
|
|
603
|
+
"payout_SBD": None,
|
|
604
|
+
"total_payout_SBD": None,
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
def get_curation_rewards(self, pending_payout_SBD=False, pending_payout_value=None):
|
|
608
|
+
"""Returns the curation rewards. The split between creator/curator is currently 50%/50%.
|
|
609
|
+
|
|
610
|
+
:param bool pending_payout_SBD: If True, the rewards are returned in SBD and not in STEEM (default is False)
|
|
611
|
+
:param pending_payout_value: When not None this value instead of the current
|
|
612
|
+
value is used for calculating the rewards
|
|
613
|
+
:type pending_payout_value: float, str
|
|
614
|
+
|
|
615
|
+
`pending_rewards` is True when
|
|
616
|
+
the post is younger than 7 days. `unclaimed_rewards` is the
|
|
617
|
+
amount of curation_rewards that goes to the author (self-vote or votes within
|
|
618
|
+
the first 30 minutes). `active_votes` contains all voter with their curation reward.
|
|
619
|
+
|
|
620
|
+
Example::
|
|
621
|
+
|
|
622
|
+
{
|
|
623
|
+
'pending_rewards': True, 'unclaimed_rewards': 0.245 STEEM,
|
|
624
|
+
'active_votes': {
|
|
625
|
+
'leprechaun': 0.006 STEEM, 'timcliff': 0.186 STEEM,
|
|
626
|
+
'st3llar': 0.000 STEEM, 'crokkon': 0.015 STEEM, 'feedyourminnows': 0.003 STEEM,
|
|
627
|
+
'isnochys': 0.003 STEEM, 'loshcat': 0.001 STEEM, 'greenorange': 0.000 STEEM,
|
|
628
|
+
'qustodian': 0.123 STEEM, 'jpphotography': 0.002 STEEM, 'thinkingmind': 0.001 STEEM,
|
|
629
|
+
'oups': 0.006 STEEM, 'mattockfs': 0.001 STEEM, 'thecrazygm': 0.003 STEEM, 'michaelizer': 0.004 STEEM,
|
|
630
|
+
'flugschwein': 0.010 STEEM, 'ulisessabeque': 0.000 STEEM, 'hakancelik': 0.002 STEEM, 'sbi2': 0.008 STEEM,
|
|
631
|
+
'zcool': 0.000 STEEM, 'steemhq': 0.002 STEEM, 'rowdiya': 0.000 STEEM, 'qurator-tier-1-2': 0.012 STEEM
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
"""
|
|
636
|
+
median_hist = self.blockchain.get_current_median_history()
|
|
637
|
+
if median_hist is not None:
|
|
638
|
+
median_price = Price(median_hist, blockchain_instance=self.blockchain)
|
|
639
|
+
pending_rewards = False
|
|
640
|
+
active_votes_list = self.get_votes()
|
|
641
|
+
curator_reward_factor = 0.5
|
|
642
|
+
|
|
643
|
+
if "total_vote_weight" in self:
|
|
644
|
+
total_vote_weight = self["total_vote_weight"]
|
|
645
|
+
active_votes_json_list = []
|
|
646
|
+
for vote in active_votes_list:
|
|
647
|
+
if "weight" not in vote:
|
|
648
|
+
vote.refresh()
|
|
649
|
+
active_votes_json_list.append(vote.json())
|
|
650
|
+
else:
|
|
651
|
+
active_votes_json_list.append(vote.json())
|
|
652
|
+
|
|
653
|
+
total_vote_weight = 0
|
|
654
|
+
for vote in active_votes_json_list:
|
|
655
|
+
total_vote_weight += vote["weight"]
|
|
656
|
+
|
|
657
|
+
if not self.is_pending():
|
|
658
|
+
if pending_payout_SBD or median_hist is None:
|
|
659
|
+
max_rewards = Amount(
|
|
660
|
+
self["curator_payout_value"], blockchain_instance=self.blockchain
|
|
661
|
+
)
|
|
662
|
+
else:
|
|
663
|
+
max_rewards = median_price.as_base(self.blockchain.token_symbol) * Amount(
|
|
664
|
+
self["curator_payout_value"], blockchain_instance=self.blockchain
|
|
665
|
+
)
|
|
666
|
+
unclaimed_rewards = Amount(
|
|
667
|
+
0, self.blockchain.token_symbol, blockchain_instance=self.blockchain
|
|
668
|
+
)
|
|
669
|
+
else:
|
|
670
|
+
if pending_payout_value is None and "pending_payout_value" in self:
|
|
671
|
+
pending_payout_value = Amount(
|
|
672
|
+
self["pending_payout_value"], blockchain_instance=self.blockchain
|
|
673
|
+
)
|
|
674
|
+
elif pending_payout_value is None:
|
|
675
|
+
pending_payout_value = 0
|
|
676
|
+
elif isinstance(pending_payout_value, (float, integer_types)):
|
|
677
|
+
pending_payout_value = Amount(
|
|
678
|
+
pending_payout_value,
|
|
679
|
+
self.blockchain.backed_token_symbol,
|
|
680
|
+
blockchain_instance=self.blockchain,
|
|
681
|
+
)
|
|
682
|
+
elif isinstance(pending_payout_value, str):
|
|
683
|
+
pending_payout_value = Amount(
|
|
684
|
+
pending_payout_value, blockchain_instance=self.blockchain
|
|
685
|
+
)
|
|
686
|
+
if pending_payout_SBD or median_hist is None:
|
|
687
|
+
max_rewards = pending_payout_value * curator_reward_factor
|
|
688
|
+
else:
|
|
689
|
+
max_rewards = median_price.as_base(self.blockchain.token_symbol) * (
|
|
690
|
+
pending_payout_value * curator_reward_factor
|
|
691
|
+
)
|
|
692
|
+
unclaimed_rewards = max_rewards.copy()
|
|
693
|
+
pending_rewards = True
|
|
694
|
+
|
|
695
|
+
active_votes = {}
|
|
696
|
+
|
|
697
|
+
for vote in active_votes_json_list:
|
|
698
|
+
if total_vote_weight > 0:
|
|
699
|
+
claim = max_rewards * int(vote["weight"]) / total_vote_weight
|
|
700
|
+
else:
|
|
701
|
+
claim = 0
|
|
702
|
+
if claim > 0 and pending_rewards:
|
|
703
|
+
unclaimed_rewards -= claim
|
|
704
|
+
if claim > 0:
|
|
705
|
+
active_votes[vote["voter"]] = claim
|
|
706
|
+
else:
|
|
707
|
+
active_votes[vote["voter"]] = 0
|
|
708
|
+
|
|
709
|
+
return {
|
|
710
|
+
"pending_rewards": pending_rewards,
|
|
711
|
+
"unclaimed_rewards": unclaimed_rewards,
|
|
712
|
+
"active_votes": active_votes,
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
def get_reblogged_by(self, identifier=None):
|
|
716
|
+
"""Shows in which blogs this post appears"""
|
|
717
|
+
if not identifier:
|
|
718
|
+
post_author = self["author"]
|
|
719
|
+
post_permlink = self["permlink"]
|
|
720
|
+
else:
|
|
721
|
+
[post_author, post_permlink] = resolve_authorperm(identifier)
|
|
722
|
+
if not self.blockchain.is_connected():
|
|
723
|
+
return None
|
|
724
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
725
|
+
if self.blockchain.rpc.get_use_appbase():
|
|
726
|
+
return self.blockchain.rpc.get_reblogged_by(
|
|
727
|
+
{"author": post_author, "permlink": post_permlink}, api="condenser"
|
|
728
|
+
)["accounts"]
|
|
729
|
+
else:
|
|
730
|
+
return self.blockchain.rpc.get_reblogged_by(post_author, post_permlink, api="condenser")
|
|
731
|
+
|
|
732
|
+
def get_replies(self, raw_data=False, identifier=None):
|
|
733
|
+
"""Returns content replies
|
|
734
|
+
|
|
735
|
+
:param bool raw_data: When set to False, the replies will be returned as Comment class objects
|
|
736
|
+
"""
|
|
737
|
+
if not identifier:
|
|
738
|
+
post_author = self["author"]
|
|
739
|
+
post_permlink = self["permlink"]
|
|
740
|
+
else:
|
|
741
|
+
[post_author, post_permlink] = resolve_authorperm(identifier)
|
|
742
|
+
if not self.blockchain.is_connected():
|
|
743
|
+
return None
|
|
744
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
745
|
+
|
|
746
|
+
# Use bridge.get_discussion API
|
|
747
|
+
content_replies = self.blockchain.rpc.get_discussion(
|
|
748
|
+
{"author": post_author, "permlink": post_permlink, "observer": self.observer},
|
|
749
|
+
api="bridge",
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
if not content_replies:
|
|
753
|
+
return []
|
|
754
|
+
|
|
755
|
+
# The response format is a dict with keys in 'author/permlink' format
|
|
756
|
+
# We need to extract the replies by filtering out the original post
|
|
757
|
+
original_key = f"{post_author}/{post_permlink}"
|
|
758
|
+
replies = []
|
|
759
|
+
|
|
760
|
+
for key, content in content_replies.items():
|
|
761
|
+
# Skip the original post
|
|
762
|
+
if key == original_key:
|
|
763
|
+
continue
|
|
764
|
+
# Add the reply
|
|
765
|
+
replies.append(content)
|
|
766
|
+
|
|
767
|
+
if raw_data:
|
|
768
|
+
return replies
|
|
769
|
+
return [Comment(c, blockchain_instance=self.blockchain) for c in replies]
|
|
770
|
+
|
|
771
|
+
def get_all_replies(self, parent=None):
|
|
772
|
+
"""Returns all content replies"""
|
|
773
|
+
if parent is None:
|
|
774
|
+
parent = self
|
|
775
|
+
if parent["children"] > 0:
|
|
776
|
+
children = parent.get_replies()
|
|
777
|
+
if children is None:
|
|
778
|
+
return []
|
|
779
|
+
for cc in children[:]:
|
|
780
|
+
children.extend(self.get_all_replies(parent=cc))
|
|
781
|
+
return children
|
|
782
|
+
return []
|
|
783
|
+
|
|
784
|
+
def get_parent(self, children=None):
|
|
785
|
+
"""Returns the parent post with depth == 0"""
|
|
786
|
+
if children is None:
|
|
787
|
+
children = self
|
|
788
|
+
while children["depth"] > 0:
|
|
789
|
+
children = Comment(
|
|
790
|
+
construct_authorperm(children["parent_author"], children["parent_permlink"]),
|
|
791
|
+
blockchain_instance=self.blockchain,
|
|
792
|
+
)
|
|
793
|
+
return children
|
|
794
|
+
|
|
795
|
+
def get_votes(self, raw_data=False):
|
|
796
|
+
"""Returns all votes as ActiveVotes object"""
|
|
797
|
+
if raw_data and "active_votes" in self:
|
|
798
|
+
return self["active_votes"]
|
|
799
|
+
from .vote import ActiveVotes
|
|
800
|
+
|
|
801
|
+
authorperm = construct_authorperm(self["author"], self["permlink"])
|
|
802
|
+
return ActiveVotes(authorperm, lazy=False, blockchain_instance=self.blockchain)
|
|
803
|
+
|
|
804
|
+
def upvote(self, weight=+100, voter=None):
|
|
805
|
+
"""Upvote the post
|
|
806
|
+
|
|
807
|
+
:param float weight: (optional) Weight for posting (-100.0 -
|
|
808
|
+
+100.0) defaults to +100.0
|
|
809
|
+
:param str voter: (optional) Voting account
|
|
810
|
+
|
|
811
|
+
"""
|
|
812
|
+
if weight < 0:
|
|
813
|
+
raise ValueError("Weight must be >= 0.")
|
|
814
|
+
last_payout = self.get("last_payout", None)
|
|
815
|
+
if last_payout is not None:
|
|
816
|
+
if formatToTimeStamp(last_payout) > 0:
|
|
817
|
+
raise VotingInvalidOnArchivedPost
|
|
818
|
+
return self.vote(weight, account=voter)
|
|
819
|
+
|
|
820
|
+
def downvote(self, weight=100, voter=None):
|
|
821
|
+
"""Downvote the post
|
|
822
|
+
|
|
823
|
+
:param float weight: (optional) Weight for posting (-100.0 -
|
|
824
|
+
+100.0) defaults to -100.0
|
|
825
|
+
:param str voter: (optional) Voting account
|
|
826
|
+
|
|
827
|
+
"""
|
|
828
|
+
if weight < 0:
|
|
829
|
+
raise ValueError("Weight must be >= 0.")
|
|
830
|
+
last_payout = self.get("last_payout", None)
|
|
831
|
+
if last_payout is not None:
|
|
832
|
+
if formatToTimeStamp(last_payout) > 0:
|
|
833
|
+
raise VotingInvalidOnArchivedPost
|
|
834
|
+
return self.vote(-weight, account=voter)
|
|
835
|
+
|
|
836
|
+
def vote(self, weight, account=None, identifier=None, **kwargs):
|
|
837
|
+
"""Vote for a post
|
|
838
|
+
|
|
839
|
+
:param float weight: Voting weight. Range: -100.0 - +100.0.
|
|
840
|
+
:param str account: (optional) Account to use for voting. If
|
|
841
|
+
``account`` is not defined, the ``default_account`` will be used
|
|
842
|
+
or a ValueError will be raised
|
|
843
|
+
:param str identifier: Identifier for the post to vote. Takes the
|
|
844
|
+
form ``@author/permlink``.
|
|
845
|
+
|
|
846
|
+
"""
|
|
847
|
+
if not identifier:
|
|
848
|
+
identifier = construct_authorperm(self["author"], self["permlink"])
|
|
849
|
+
|
|
850
|
+
return self.blockchain.vote(weight, identifier, account=account)
|
|
851
|
+
|
|
852
|
+
def edit(self, body, meta=None, replace=False):
|
|
853
|
+
"""Edit an existing post
|
|
854
|
+
|
|
855
|
+
:param str body: Body of the reply
|
|
856
|
+
:param json meta: JSON meta object that can be attached to the
|
|
857
|
+
post. (optional)
|
|
858
|
+
:param bool replace: Instead of calculating a *diff*, replace
|
|
859
|
+
the post entirely (defaults to ``False``)
|
|
860
|
+
|
|
861
|
+
"""
|
|
862
|
+
if not meta:
|
|
863
|
+
meta = {}
|
|
864
|
+
original_post = self
|
|
865
|
+
|
|
866
|
+
if replace:
|
|
867
|
+
newbody = body
|
|
868
|
+
else:
|
|
869
|
+
newbody = make_patch(original_post["body"], body)
|
|
870
|
+
if not newbody:
|
|
871
|
+
log.info("No changes made! Skipping ...")
|
|
872
|
+
return
|
|
873
|
+
|
|
874
|
+
reply_identifier = construct_authorperm(
|
|
875
|
+
original_post["parent_author"], original_post["parent_permlink"]
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
new_meta = {}
|
|
879
|
+
if meta is not None:
|
|
880
|
+
if bool(original_post["json_metadata"]):
|
|
881
|
+
new_meta = original_post["json_metadata"]
|
|
882
|
+
for key in meta:
|
|
883
|
+
new_meta[key] = meta[key]
|
|
884
|
+
else:
|
|
885
|
+
new_meta = meta
|
|
886
|
+
|
|
887
|
+
return self.blockchain.post(
|
|
888
|
+
original_post["title"],
|
|
889
|
+
newbody,
|
|
890
|
+
reply_identifier=reply_identifier,
|
|
891
|
+
author=original_post["author"],
|
|
892
|
+
permlink=original_post["permlink"],
|
|
893
|
+
json_metadata=new_meta,
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
def reply(self, body, title="", author="", meta=None):
|
|
897
|
+
"""Reply to an existing post
|
|
898
|
+
|
|
899
|
+
:param str body: Body of the reply
|
|
900
|
+
:param str title: Title of the reply post
|
|
901
|
+
:param str author: Author of reply (optional) if not provided
|
|
902
|
+
``default_user`` will be used, if present, else
|
|
903
|
+
a ``ValueError`` will be raised.
|
|
904
|
+
:param json meta: JSON meta object that can be attached to the
|
|
905
|
+
post. (optional)
|
|
906
|
+
|
|
907
|
+
"""
|
|
908
|
+
return self.blockchain.post(
|
|
909
|
+
title, body, json_metadata=meta, author=author, reply_identifier=self.identifier
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
def delete(self, account=None, identifier=None):
|
|
913
|
+
"""Delete an existing post/comment
|
|
914
|
+
|
|
915
|
+
:param str account: (optional) Account to use for deletion. If
|
|
916
|
+
``account`` is not defined, the ``default_account`` will be
|
|
917
|
+
taken or a ValueError will be raised.
|
|
918
|
+
|
|
919
|
+
:param str identifier: (optional) Identifier for the post to delete.
|
|
920
|
+
Takes the form ``@author/permlink``. By default the current post
|
|
921
|
+
will be used.
|
|
922
|
+
|
|
923
|
+
.. note:: A post/comment can only be deleted as long as it has no
|
|
924
|
+
replies and no positive rshares on it.
|
|
925
|
+
|
|
926
|
+
"""
|
|
927
|
+
if not account:
|
|
928
|
+
if "default_account" in self.blockchain.config:
|
|
929
|
+
account = self.blockchain.config["default_account"]
|
|
930
|
+
if not account:
|
|
931
|
+
raise ValueError("You need to provide an account")
|
|
932
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
933
|
+
if not identifier:
|
|
934
|
+
post_author = self["author"]
|
|
935
|
+
post_permlink = self["permlink"]
|
|
936
|
+
else:
|
|
937
|
+
[post_author, post_permlink] = resolve_authorperm(identifier)
|
|
938
|
+
op = operations.Delete_comment(**{"author": post_author, "permlink": post_permlink})
|
|
939
|
+
return self.blockchain.finalizeOp(op, account, "posting")
|
|
940
|
+
|
|
941
|
+
def resteem(self, identifier=None, account=None):
|
|
942
|
+
"""Resteem a post
|
|
943
|
+
|
|
944
|
+
:param str identifier: post identifier (@<account>/<permlink>)
|
|
945
|
+
:param str account: (optional) the account to allow access
|
|
946
|
+
to (defaults to ``default_account``)
|
|
947
|
+
|
|
948
|
+
"""
|
|
949
|
+
if not account:
|
|
950
|
+
account = self.blockchain.configStorage.get("default_account")
|
|
951
|
+
if not account:
|
|
952
|
+
raise ValueError("You need to provide an account")
|
|
953
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
954
|
+
if identifier is None:
|
|
955
|
+
identifier = self.identifier
|
|
956
|
+
author, permlink = resolve_authorperm(identifier)
|
|
957
|
+
json_body = ["reblog", {"account": account["name"], "author": author, "permlink": permlink}]
|
|
958
|
+
return self.blockchain.custom_json(
|
|
959
|
+
id="follow", json_data=json_body, required_posting_auths=[account["name"]]
|
|
960
|
+
)
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
class RecentReplies(list):
|
|
964
|
+
"""Obtain a list of recent replies
|
|
965
|
+
|
|
966
|
+
:param str author: author
|
|
967
|
+
:param bool skip_own: (optional) Skip replies of the author to him/herself.
|
|
968
|
+
Default: True
|
|
969
|
+
:param Steem blockchain_instance: Steem() instance to use when accesing a RPC
|
|
970
|
+
"""
|
|
971
|
+
|
|
972
|
+
def __init__(
|
|
973
|
+
self,
|
|
974
|
+
author,
|
|
975
|
+
skip_own=True,
|
|
976
|
+
start_permlink="",
|
|
977
|
+
limit=100,
|
|
978
|
+
lazy=False,
|
|
979
|
+
full=True,
|
|
980
|
+
blockchain_instance=None,
|
|
981
|
+
**kwargs,
|
|
982
|
+
):
|
|
983
|
+
if blockchain_instance is None:
|
|
984
|
+
if kwargs.get("steem_instance"):
|
|
985
|
+
blockchain_instance = kwargs["steem_instance"]
|
|
986
|
+
elif kwargs.get("hive_instance"):
|
|
987
|
+
blockchain_instance = kwargs["hive_instance"]
|
|
988
|
+
self.blockchain = blockchain_instance or shared_blockchain_instance()
|
|
989
|
+
if not self.blockchain.is_connected():
|
|
990
|
+
return None
|
|
991
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(True)
|
|
992
|
+
account = Account(author, blockchain_instance=self.blockchain)
|
|
993
|
+
replies = account.get_account_posts(sort="replies", raw_data=True)
|
|
994
|
+
comments = []
|
|
995
|
+
if replies is None:
|
|
996
|
+
replies = []
|
|
997
|
+
for post in replies:
|
|
998
|
+
if skip_own and post["author"] == author:
|
|
999
|
+
continue
|
|
1000
|
+
comments.append(
|
|
1001
|
+
Comment(post, lazy=lazy, full=full, blockchain_instance=self.blockchain)
|
|
1002
|
+
)
|
|
1003
|
+
super(RecentReplies, self).__init__(comments)
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
class RecentByPath(list):
|
|
1007
|
+
"""Obtain a list of posts recent by path, does the same as RankedPosts
|
|
1008
|
+
|
|
1009
|
+
:param str path: path
|
|
1010
|
+
:param str tag: tag
|
|
1011
|
+
:param str observer: observer
|
|
1012
|
+
:param Steem blockchain_instance: Steem() instance to use when accesing a RPC
|
|
1013
|
+
"""
|
|
1014
|
+
|
|
1015
|
+
def __init__(
|
|
1016
|
+
self,
|
|
1017
|
+
path="trending",
|
|
1018
|
+
tag="",
|
|
1019
|
+
observer="",
|
|
1020
|
+
lazy=False,
|
|
1021
|
+
full=True,
|
|
1022
|
+
limit=20,
|
|
1023
|
+
blockchain_instance=None,
|
|
1024
|
+
**kwargs,
|
|
1025
|
+
):
|
|
1026
|
+
if blockchain_instance is None:
|
|
1027
|
+
if kwargs.get("steem_instance"):
|
|
1028
|
+
blockchain_instance = kwargs["steem_instance"]
|
|
1029
|
+
elif kwargs.get("hive_instance"):
|
|
1030
|
+
blockchain_instance = kwargs["hive_instance"]
|
|
1031
|
+
self.blockchain = blockchain_instance or shared_blockchain_instance()
|
|
1032
|
+
|
|
1033
|
+
# Create RankedPosts with proper parameters
|
|
1034
|
+
ranked_posts = RankedPosts(
|
|
1035
|
+
sort=path,
|
|
1036
|
+
tag=tag,
|
|
1037
|
+
observer=observer,
|
|
1038
|
+
limit=limit,
|
|
1039
|
+
lazy=lazy,
|
|
1040
|
+
full=full,
|
|
1041
|
+
blockchain_instance=self.blockchain,
|
|
1042
|
+
**kwargs,
|
|
1043
|
+
)
|
|
1044
|
+
|
|
1045
|
+
super(RecentByPath, self).__init__(ranked_posts)
|
|
1046
|
+
|
|
1047
|
+
|
|
1048
|
+
class RankedPosts(list):
|
|
1049
|
+
"""Obtain a list of ranked posts
|
|
1050
|
+
|
|
1051
|
+
:param str sort: can be: trending, hot, created, promoted, payout, payout_comments, muted
|
|
1052
|
+
:param str tag: tag, when used my, the community posts of the observer are shown
|
|
1053
|
+
:param str observer: Observer name
|
|
1054
|
+
:param int limit: limits the number of returns comments
|
|
1055
|
+
:param str start_author: start author
|
|
1056
|
+
:param str start_permlink: start permlink
|
|
1057
|
+
:param Steem blockchain_instance: Steem() instance to use when accesing a RPC
|
|
1058
|
+
"""
|
|
1059
|
+
|
|
1060
|
+
def __init__(
|
|
1061
|
+
self,
|
|
1062
|
+
sort,
|
|
1063
|
+
tag="",
|
|
1064
|
+
observer="",
|
|
1065
|
+
limit=21,
|
|
1066
|
+
start_author="",
|
|
1067
|
+
start_permlink="",
|
|
1068
|
+
lazy=False,
|
|
1069
|
+
full=True,
|
|
1070
|
+
raw_data=False,
|
|
1071
|
+
blockchain_instance=None,
|
|
1072
|
+
**kwargs,
|
|
1073
|
+
):
|
|
1074
|
+
if blockchain_instance is None:
|
|
1075
|
+
if kwargs.get("steem_instance"):
|
|
1076
|
+
blockchain_instance = kwargs["steem_instance"]
|
|
1077
|
+
elif kwargs.get("hive_instance"):
|
|
1078
|
+
blockchain_instance = kwargs["hive_instance"]
|
|
1079
|
+
self.blockchain = blockchain_instance or shared_blockchain_instance()
|
|
1080
|
+
if not self.blockchain.is_connected():
|
|
1081
|
+
return None
|
|
1082
|
+
comments = []
|
|
1083
|
+
api_limit = limit
|
|
1084
|
+
if api_limit > 100:
|
|
1085
|
+
api_limit = 100
|
|
1086
|
+
last_n = -1
|
|
1087
|
+
while len(comments) < limit and last_n != len(comments):
|
|
1088
|
+
last_n = len(comments)
|
|
1089
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
1090
|
+
try:
|
|
1091
|
+
posts = self.blockchain.rpc.get_ranked_posts(
|
|
1092
|
+
{
|
|
1093
|
+
"sort": sort,
|
|
1094
|
+
"tag": tag,
|
|
1095
|
+
"observer": observer,
|
|
1096
|
+
"limit": api_limit,
|
|
1097
|
+
"start_author": start_author,
|
|
1098
|
+
"start_permlink": start_permlink,
|
|
1099
|
+
},
|
|
1100
|
+
api="bridge",
|
|
1101
|
+
)
|
|
1102
|
+
if posts is None:
|
|
1103
|
+
continue
|
|
1104
|
+
for post in posts:
|
|
1105
|
+
if (
|
|
1106
|
+
len(comments) > 0
|
|
1107
|
+
and comments[-1]["author"] == post["author"]
|
|
1108
|
+
and comments[-1]["permlink"] == post["permlink"]
|
|
1109
|
+
):
|
|
1110
|
+
continue
|
|
1111
|
+
if len(comments) >= limit:
|
|
1112
|
+
continue
|
|
1113
|
+
if raw_data:
|
|
1114
|
+
comments.append(post)
|
|
1115
|
+
else:
|
|
1116
|
+
comments.append(
|
|
1117
|
+
Comment(post, lazy=lazy, full=full, blockchain_instance=self.blockchain)
|
|
1118
|
+
)
|
|
1119
|
+
if len(comments) > 0:
|
|
1120
|
+
start_author = comments[-1]["author"]
|
|
1121
|
+
start_permlink = comments[-1]["permlink"]
|
|
1122
|
+
if limit - len(comments) < 100:
|
|
1123
|
+
api_limit = limit - len(comments) + 1
|
|
1124
|
+
except Exception as e:
|
|
1125
|
+
# If we get an error but have some posts, return what we have
|
|
1126
|
+
if len(comments) > 0:
|
|
1127
|
+
logging.warning(f"Error in RankedPosts: {str(e)}. Returning partial results.")
|
|
1128
|
+
break
|
|
1129
|
+
# Otherwise, re-raise the exception
|
|
1130
|
+
raise
|
|
1131
|
+
super(RankedPosts, self).__init__(comments)
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
class AccountPosts(list):
|
|
1135
|
+
"""Obtain a list of account related posts
|
|
1136
|
+
|
|
1137
|
+
:param str sort: can be: comments, posts, blog, replies, feed
|
|
1138
|
+
:param str account: Account name
|
|
1139
|
+
:param str observer: Observer name
|
|
1140
|
+
:param int limit: limits the number of returns comments
|
|
1141
|
+
:param str start_author: start author
|
|
1142
|
+
:param str start_permlink: start permlink
|
|
1143
|
+
:param Hive blockchain_instance: Hive() instance to use when accesing a RPC
|
|
1144
|
+
"""
|
|
1145
|
+
|
|
1146
|
+
def __init__(
|
|
1147
|
+
self,
|
|
1148
|
+
sort,
|
|
1149
|
+
account,
|
|
1150
|
+
observer="",
|
|
1151
|
+
limit=20,
|
|
1152
|
+
start_author="",
|
|
1153
|
+
start_permlink="",
|
|
1154
|
+
lazy=False,
|
|
1155
|
+
full=True,
|
|
1156
|
+
raw_data=False,
|
|
1157
|
+
blockchain_instance=None,
|
|
1158
|
+
**kwargs,
|
|
1159
|
+
):
|
|
1160
|
+
if blockchain_instance is None:
|
|
1161
|
+
if kwargs.get("steem_instance"):
|
|
1162
|
+
blockchain_instance = kwargs["steem_instance"]
|
|
1163
|
+
elif kwargs.get("hive_instance"):
|
|
1164
|
+
blockchain_instance = kwargs["hive_instance"]
|
|
1165
|
+
self.blockchain = blockchain_instance or shared_blockchain_instance()
|
|
1166
|
+
if not self.blockchain.is_connected():
|
|
1167
|
+
return None
|
|
1168
|
+
comments = []
|
|
1169
|
+
api_limit = limit
|
|
1170
|
+
if api_limit > 100:
|
|
1171
|
+
api_limit = 100
|
|
1172
|
+
last_n = -1
|
|
1173
|
+
while len(comments) < limit and last_n != len(comments):
|
|
1174
|
+
last_n = len(comments)
|
|
1175
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
1176
|
+
try:
|
|
1177
|
+
posts = self.blockchain.rpc.get_account_posts(
|
|
1178
|
+
{
|
|
1179
|
+
"sort": sort,
|
|
1180
|
+
"account": account,
|
|
1181
|
+
"observer": observer,
|
|
1182
|
+
"limit": api_limit,
|
|
1183
|
+
"start_author": start_author,
|
|
1184
|
+
"start_permlink": start_permlink,
|
|
1185
|
+
},
|
|
1186
|
+
api="bridge",
|
|
1187
|
+
)
|
|
1188
|
+
if posts is None:
|
|
1189
|
+
continue
|
|
1190
|
+
for post in posts:
|
|
1191
|
+
if (
|
|
1192
|
+
len(comments) > 0
|
|
1193
|
+
and comments[-1]["author"] == post["author"]
|
|
1194
|
+
and comments[-1]["permlink"] == post["permlink"]
|
|
1195
|
+
):
|
|
1196
|
+
continue
|
|
1197
|
+
if len(comments) >= limit:
|
|
1198
|
+
continue
|
|
1199
|
+
if raw_data:
|
|
1200
|
+
comments.append(post)
|
|
1201
|
+
else:
|
|
1202
|
+
comments.append(
|
|
1203
|
+
Comment(post, lazy=lazy, full=full, blockchain_instance=self.blockchain)
|
|
1204
|
+
)
|
|
1205
|
+
if len(comments) > 0:
|
|
1206
|
+
start_author = comments[-1]["author"]
|
|
1207
|
+
start_permlink = comments[-1]["permlink"]
|
|
1208
|
+
if limit - len(comments) < 100:
|
|
1209
|
+
api_limit = limit - len(comments) + 1
|
|
1210
|
+
except Exception as e:
|
|
1211
|
+
# If we get an error but have some posts, return what we have
|
|
1212
|
+
if len(comments) > 0:
|
|
1213
|
+
logging.warning(f"Error in AccountPosts: {str(e)}. Returning partial results.")
|
|
1214
|
+
break
|
|
1215
|
+
# Otherwise, re-raise the exception
|
|
1216
|
+
raise
|
|
1217
|
+
super(AccountPosts, self).__init__(comments)
|