hive-nectar 0.2.9__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.
- hive_nectar-0.2.9.dist-info/METADATA +194 -0
- hive_nectar-0.2.9.dist-info/RECORD +87 -0
- hive_nectar-0.2.9.dist-info/WHEEL +4 -0
- hive_nectar-0.2.9.dist-info/entry_points.txt +2 -0
- hive_nectar-0.2.9.dist-info/licenses/LICENSE.txt +23 -0
- nectar/__init__.py +37 -0
- nectar/account.py +5076 -0
- nectar/amount.py +553 -0
- nectar/asciichart.py +303 -0
- nectar/asset.py +122 -0
- nectar/block.py +574 -0
- nectar/blockchain.py +1242 -0
- nectar/blockchaininstance.py +2590 -0
- nectar/blockchainobject.py +263 -0
- nectar/cli.py +5937 -0
- nectar/comment.py +1552 -0
- nectar/community.py +854 -0
- nectar/constants.py +95 -0
- nectar/discussions.py +1437 -0
- nectar/exceptions.py +152 -0
- nectar/haf.py +381 -0
- nectar/hive.py +630 -0
- nectar/imageuploader.py +114 -0
- nectar/instance.py +113 -0
- nectar/market.py +876 -0
- nectar/memo.py +542 -0
- nectar/message.py +379 -0
- nectar/nodelist.py +309 -0
- nectar/price.py +603 -0
- nectar/profile.py +74 -0
- nectar/py.typed +0 -0
- nectar/rc.py +333 -0
- nectar/snapshot.py +1024 -0
- nectar/storage.py +62 -0
- nectar/transactionbuilder.py +659 -0
- nectar/utils.py +630 -0
- nectar/version.py +3 -0
- nectar/vote.py +722 -0
- nectar/wallet.py +472 -0
- nectar/witness.py +728 -0
- nectarapi/__init__.py +12 -0
- nectarapi/exceptions.py +126 -0
- nectarapi/graphenerpc.py +596 -0
- nectarapi/node.py +194 -0
- nectarapi/noderpc.py +79 -0
- nectarapi/openapi.py +107 -0
- nectarapi/py.typed +0 -0
- nectarapi/rpcutils.py +98 -0
- nectarapi/version.py +3 -0
- nectarbase/__init__.py +15 -0
- nectarbase/ledgertransactions.py +106 -0
- nectarbase/memo.py +242 -0
- nectarbase/objects.py +521 -0
- nectarbase/objecttypes.py +21 -0
- nectarbase/operationids.py +102 -0
- nectarbase/operations.py +1357 -0
- nectarbase/py.typed +0 -0
- nectarbase/signedtransactions.py +89 -0
- nectarbase/transactions.py +11 -0
- nectarbase/version.py +3 -0
- nectargraphenebase/__init__.py +27 -0
- nectargraphenebase/account.py +1121 -0
- nectargraphenebase/aes.py +49 -0
- nectargraphenebase/base58.py +197 -0
- nectargraphenebase/bip32.py +575 -0
- nectargraphenebase/bip38.py +110 -0
- nectargraphenebase/chains.py +15 -0
- nectargraphenebase/dictionary.py +2 -0
- nectargraphenebase/ecdsasig.py +309 -0
- nectargraphenebase/objects.py +130 -0
- nectargraphenebase/objecttypes.py +8 -0
- nectargraphenebase/operationids.py +5 -0
- nectargraphenebase/operations.py +25 -0
- nectargraphenebase/prefix.py +13 -0
- nectargraphenebase/py.typed +0 -0
- nectargraphenebase/signedtransactions.py +221 -0
- nectargraphenebase/types.py +557 -0
- nectargraphenebase/unsignedtransactions.py +288 -0
- nectargraphenebase/version.py +3 -0
- nectarstorage/__init__.py +57 -0
- nectarstorage/base.py +317 -0
- nectarstorage/exceptions.py +15 -0
- nectarstorage/interfaces.py +244 -0
- nectarstorage/masterpassword.py +237 -0
- nectarstorage/py.typed +0 -0
- nectarstorage/ram.py +27 -0
- nectarstorage/sqlite.py +343 -0
nectar/community.py
ADDED
|
@@ -0,0 +1,854 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from datetime import date, datetime, time
|
|
6
|
+
|
|
7
|
+
from prettytable import PrettyTable
|
|
8
|
+
|
|
9
|
+
from nectar.instance import shared_blockchain_instance
|
|
10
|
+
|
|
11
|
+
from .blockchainobject import BlockchainObject
|
|
12
|
+
from .exceptions import AccountDoesNotExistsException, OfflineHasNoRPCException
|
|
13
|
+
from .utils import (
|
|
14
|
+
addTzInfo,
|
|
15
|
+
formatTimeString,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
log = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Community(BlockchainObject):
|
|
22
|
+
"""A class representing a Hive community with methods to interact with it.
|
|
23
|
+
|
|
24
|
+
This class provides an interface to access and manipulate community data on the Hive blockchain.
|
|
25
|
+
It extends BlockchainObject and provides additional community-specific functionality.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
community: Either a community name (str) or a dictionary containing community data
|
|
29
|
+
observer: Observer account for personalized results (default: "")
|
|
30
|
+
full: If True, fetch full community data (default: True)
|
|
31
|
+
lazy: If True, use lazy loading (default: False)
|
|
32
|
+
blockchain_instance: Blockchain instance for RPC access
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
type_id (int): Type identifier for blockchain objects (2 for communities)
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
>>> from nectar.community import Community
|
|
39
|
+
>>> from nectar import Hive
|
|
40
|
+
>>> from nectar.nodelist import NodeList
|
|
41
|
+
>>> nodelist = NodeList()
|
|
42
|
+
>>> nodelist.update_nodes()
|
|
43
|
+
>>> hv = Hive(node=nodelist.get_hive_nodes())
|
|
44
|
+
>>> community = Community("hive-139531", blockchain_instance=hv)
|
|
45
|
+
>>> print(community)
|
|
46
|
+
<Community hive-139531>
|
|
47
|
+
|
|
48
|
+
Note:
|
|
49
|
+
This class includes caching to reduce API server load. Use refresh() to update
|
|
50
|
+
the data and clear_cache() to clear the cache.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
type_id = 2
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
community: str | dict,
|
|
58
|
+
observer: str = "",
|
|
59
|
+
full: bool = True,
|
|
60
|
+
lazy: bool = False,
|
|
61
|
+
blockchain_instance=None,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""
|
|
64
|
+
Create a Community wrapper for the given community identifier or raw data.
|
|
65
|
+
|
|
66
|
+
If `community` is a dict, it will be normalized via _parse_json_data before initialization.
|
|
67
|
+
This sets instance flags (full, lazy, observer) and resolves the blockchain instance used
|
|
68
|
+
for RPC calls (falls back to the shared global instance). The object is constructed with
|
|
69
|
+
its identifier field set to "name".
|
|
70
|
+
|
|
71
|
+
Parameters:
|
|
72
|
+
community: Community name (str) or a dict with community data.
|
|
73
|
+
observer: Account name used to request personalized data (optional).
|
|
74
|
+
full: If True, load complete community data when available.
|
|
75
|
+
lazy: If True, defer loading detail until accessed.
|
|
76
|
+
"""
|
|
77
|
+
self.full = full
|
|
78
|
+
self.lazy = lazy
|
|
79
|
+
self.observer = observer
|
|
80
|
+
self.blockchain = blockchain_instance or shared_blockchain_instance()
|
|
81
|
+
if isinstance(community, dict):
|
|
82
|
+
community = self._parse_json_data(community)
|
|
83
|
+
super().__init__(
|
|
84
|
+
community, lazy=lazy, full=full, id_item="name", blockchain_instance=self.blockchain
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def refresh(self) -> None:
|
|
88
|
+
"""
|
|
89
|
+
Refresh the community's data from the blockchain.
|
|
90
|
+
|
|
91
|
+
Fetches the latest community record for this community's name via the bridge RPC and
|
|
92
|
+
reinitializes the Community object with the returned data (updating identifier and all fields).
|
|
93
|
+
If the instance is offline, the method returns without performing any RPC call.
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
AccountDoesNotExistsException: If no community data is returned for this community name.
|
|
97
|
+
"""
|
|
98
|
+
if not self.blockchain.is_connected():
|
|
99
|
+
return
|
|
100
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(True)
|
|
101
|
+
community = self.blockchain.rpc.get_community(
|
|
102
|
+
{"name": self.identifier, "observer": self.observer}
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if not community:
|
|
106
|
+
raise AccountDoesNotExistsException(self.identifier)
|
|
107
|
+
community = self._parse_json_data(community)
|
|
108
|
+
self.identifier = community["name"]
|
|
109
|
+
# self.blockchain.refresh_data()
|
|
110
|
+
|
|
111
|
+
super().__init__(
|
|
112
|
+
community,
|
|
113
|
+
id_item="name",
|
|
114
|
+
lazy=self.lazy,
|
|
115
|
+
full=self.full,
|
|
116
|
+
blockchain_instance=self.blockchain,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def _parse_json_data(self, community: dict) -> dict:
|
|
120
|
+
"""Parse and convert community JSON data into proper Python types.
|
|
121
|
+
|
|
122
|
+
This internal method converts string representations of numbers to integers
|
|
123
|
+
and parses date strings into datetime objects with timezone information.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
community: Dictionary containing raw community data from the API
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
dict: Processed community data with proper Python types
|
|
130
|
+
"""
|
|
131
|
+
# Convert string numbers to integers
|
|
132
|
+
int_fields = [
|
|
133
|
+
"sum_pending",
|
|
134
|
+
"subscribers",
|
|
135
|
+
"num_pending",
|
|
136
|
+
"num_authors",
|
|
137
|
+
]
|
|
138
|
+
for field in int_fields:
|
|
139
|
+
if field in community and isinstance(community.get(field), str):
|
|
140
|
+
community[field] = int(community.get(field, 0))
|
|
141
|
+
|
|
142
|
+
# Parse date strings into datetime objects
|
|
143
|
+
date_fields = ["created_at"]
|
|
144
|
+
for field in date_fields:
|
|
145
|
+
if field in community and isinstance(community.get(field), str):
|
|
146
|
+
community[field] = addTzInfo(
|
|
147
|
+
datetime.strptime(
|
|
148
|
+
community.get(field, "1970-01-01 00:00:00"), "%Y-%m-%d %H:%M:%S"
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return community
|
|
153
|
+
|
|
154
|
+
def json(self) -> dict:
|
|
155
|
+
"""Convert the community data to a JSON-serializable dictionary.
|
|
156
|
+
|
|
157
|
+
This method prepares the community data for JSON serialization by converting
|
|
158
|
+
non-JSON-serializable types (like datetime objects) to strings.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
dict: A dictionary containing the community data in a JSON-serializable format
|
|
162
|
+
"""
|
|
163
|
+
output = self.copy()
|
|
164
|
+
|
|
165
|
+
# Convert integer fields to strings for JSON serialization
|
|
166
|
+
int_fields = [
|
|
167
|
+
"sum_pending",
|
|
168
|
+
"subscribers",
|
|
169
|
+
"num_pending",
|
|
170
|
+
"num_authors",
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
# Fields that should only be converted if non-zero
|
|
174
|
+
int_non_zero_fields = []
|
|
175
|
+
|
|
176
|
+
# Convert regular integer fields
|
|
177
|
+
for field in int_fields:
|
|
178
|
+
if field in output and isinstance(output[field], int):
|
|
179
|
+
output[field] = str(output[field])
|
|
180
|
+
|
|
181
|
+
# Convert non-zero integer fields
|
|
182
|
+
for field in int_non_zero_fields:
|
|
183
|
+
if field in output and isinstance(output[field], int) and output[field] != 0:
|
|
184
|
+
output[field] = str(output[field])
|
|
185
|
+
|
|
186
|
+
# Convert datetime fields to ISO format strings
|
|
187
|
+
date_fields = ["created_at"]
|
|
188
|
+
for field in date_fields:
|
|
189
|
+
if field in output:
|
|
190
|
+
date_val = output.get(field, datetime(1970, 1, 1, 0, 0))
|
|
191
|
+
if isinstance(date_val, (datetime, date, time)):
|
|
192
|
+
output[field] = formatTimeString(date_val).replace("T", " ")
|
|
193
|
+
else:
|
|
194
|
+
output[field] = date_val
|
|
195
|
+
return json.loads(str(json.dumps(output)))
|
|
196
|
+
|
|
197
|
+
def get_community_roles(self, limit: int = 100, last: str | None = None) -> list:
|
|
198
|
+
"""Lists community roles
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
limit: Maximum number of roles to return (default: 100)
|
|
202
|
+
last: Account name of the last role from previous page for pagination
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
list: List of community roles
|
|
206
|
+
|
|
207
|
+
Raises:
|
|
208
|
+
OfflineHasNoRPCException: If not connected to the blockchain
|
|
209
|
+
"""
|
|
210
|
+
community = self["name"]
|
|
211
|
+
if not self.blockchain.is_connected():
|
|
212
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
213
|
+
|
|
214
|
+
params = {"community": community, "limit": limit}
|
|
215
|
+
if last is not None:
|
|
216
|
+
params["last"] = last
|
|
217
|
+
|
|
218
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
219
|
+
return self.blockchain.rpc.list_community_roles(params)
|
|
220
|
+
|
|
221
|
+
def get_subscribers(self, limit: int = 100, last: str | None = None) -> list:
|
|
222
|
+
"""Returns subscribers
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
limit: Maximum number of subscribers to return (default: 100)
|
|
226
|
+
last: Account name of the last subscriber from previous page for pagination
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
list: List of subscribers
|
|
230
|
+
|
|
231
|
+
Raises:
|
|
232
|
+
OfflineHasNoRPCException: If not connected to the blockchain
|
|
233
|
+
"""
|
|
234
|
+
community = self["name"]
|
|
235
|
+
if not self.blockchain.is_connected():
|
|
236
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
237
|
+
|
|
238
|
+
params = {"community": community, "limit": limit}
|
|
239
|
+
if last is not None:
|
|
240
|
+
params["last"] = last
|
|
241
|
+
|
|
242
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
243
|
+
return self.blockchain.rpc.list_subscribers(params)
|
|
244
|
+
|
|
245
|
+
def get_activities(self, limit: int = 100, last_id: str | None = None) -> list:
|
|
246
|
+
"""Returns community activity
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
limit: Maximum number of activities to return (default: 100)
|
|
250
|
+
last_id: ID of the last activity from previous page for pagination
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
list: List of community activities
|
|
254
|
+
|
|
255
|
+
Raises:
|
|
256
|
+
OfflineHasNoRPCException: If not connected to the blockchain
|
|
257
|
+
"""
|
|
258
|
+
community = self["name"]
|
|
259
|
+
if not self.blockchain.is_connected():
|
|
260
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
261
|
+
|
|
262
|
+
params = {"account": community, "limit": limit}
|
|
263
|
+
if last_id is not None:
|
|
264
|
+
params["last_id"] = last_id
|
|
265
|
+
|
|
266
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
267
|
+
return self.blockchain.rpc.account_notifications(params)
|
|
268
|
+
|
|
269
|
+
def get_ranked_posts(
|
|
270
|
+
self,
|
|
271
|
+
observer: str | None = None,
|
|
272
|
+
limit: int = 100,
|
|
273
|
+
start_author: str | None = None,
|
|
274
|
+
start_permlink: str | None = None,
|
|
275
|
+
sort: str = "created",
|
|
276
|
+
) -> list:
|
|
277
|
+
"""Returns community posts
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
observer: Account name of the observer (optional)
|
|
281
|
+
limit: Maximum number of posts to return (default: 100)
|
|
282
|
+
start_author: Author of the post to start from for pagination (optional)
|
|
283
|
+
start_permlink: Permlink of the post to start from for pagination (optional)
|
|
284
|
+
sort: Sort order (default: "created")
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
list: List of community posts
|
|
288
|
+
|
|
289
|
+
Raises:
|
|
290
|
+
OfflineHasNoRPCException: If not connected to the blockchain
|
|
291
|
+
"""
|
|
292
|
+
community = self["name"]
|
|
293
|
+
if not self.blockchain.is_connected():
|
|
294
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
295
|
+
|
|
296
|
+
params = {"tag": community, "limit": limit, "sort": sort}
|
|
297
|
+
|
|
298
|
+
if observer is not None:
|
|
299
|
+
params["observer"] = observer
|
|
300
|
+
if start_author is not None:
|
|
301
|
+
params["start_author"] = start_author
|
|
302
|
+
if start_permlink is not None:
|
|
303
|
+
params["start_permlink"] = start_permlink
|
|
304
|
+
|
|
305
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
306
|
+
return self.blockchain.rpc.get_ranked_posts(params)
|
|
307
|
+
|
|
308
|
+
def set_role(self, account: str, role: str, mod_account: str) -> dict:
|
|
309
|
+
"""Set role for a given account in the community.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
account: Account name to set the role for
|
|
313
|
+
role: Role to assign (member, mod, admin, owner, or guest)
|
|
314
|
+
mod_account: Account name of the moderator performing this action (must be mod or higher)
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
dict: Transaction result
|
|
318
|
+
|
|
319
|
+
Raises:
|
|
320
|
+
OfflineHasNoRPCException: If not connected to the blockchain
|
|
321
|
+
ValueError: If role is not one of the allowed values
|
|
322
|
+
"""
|
|
323
|
+
valid_roles = {"member", "mod", "admin", "owner", "guest"}
|
|
324
|
+
if role.lower() not in valid_roles:
|
|
325
|
+
raise ValueError(f"Invalid role. Must be one of: {', '.join(valid_roles)}")
|
|
326
|
+
|
|
327
|
+
community = self["name"]
|
|
328
|
+
if not self.blockchain.is_connected():
|
|
329
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
330
|
+
|
|
331
|
+
json_body = [
|
|
332
|
+
"setRole",
|
|
333
|
+
{
|
|
334
|
+
"community": community,
|
|
335
|
+
"account": account,
|
|
336
|
+
"role": role.lower(),
|
|
337
|
+
},
|
|
338
|
+
]
|
|
339
|
+
return self.blockchain.custom_json(
|
|
340
|
+
"community", json_body, required_posting_auths=[mod_account]
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
def set_user_title(self, account: str, title: str, mod_account: str) -> dict:
|
|
344
|
+
"""Set the title for a given account in the community.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
account: Account name to set the title for
|
|
348
|
+
title: Title to assign to the account
|
|
349
|
+
mod_account: Account name of the moderator performing this action (must be mod or higher)
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
dict: Transaction result
|
|
353
|
+
|
|
354
|
+
Raises:
|
|
355
|
+
OfflineHasNoRPCException: If not connected to the blockchain
|
|
356
|
+
ValueError: If account or title is empty
|
|
357
|
+
"""
|
|
358
|
+
if not account or not isinstance(account, str):
|
|
359
|
+
raise ValueError("Account must be a non-empty string")
|
|
360
|
+
|
|
361
|
+
if not title or not isinstance(title, str):
|
|
362
|
+
raise ValueError("Title must be a non-empty string")
|
|
363
|
+
|
|
364
|
+
community = self["name"]
|
|
365
|
+
if not self.blockchain.is_connected():
|
|
366
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
367
|
+
|
|
368
|
+
json_body = [
|
|
369
|
+
"setUserTitle",
|
|
370
|
+
{
|
|
371
|
+
"community": community,
|
|
372
|
+
"account": account,
|
|
373
|
+
"title": title.strip(),
|
|
374
|
+
},
|
|
375
|
+
]
|
|
376
|
+
return self.blockchain.custom_json(
|
|
377
|
+
"community", json_body, required_posting_auths=[mod_account]
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
def mute_post(self, account: str, permlink: str, notes: str, mod_account: str) -> dict:
|
|
381
|
+
"""Mutes a post in the community.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
account: Author of the post to mute
|
|
385
|
+
permlink: Permlink of the post to mute
|
|
386
|
+
notes: Reason for muting the post
|
|
387
|
+
mod_account: Account name of the moderator performing this action (must be mod or higher)
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
dict: Transaction result
|
|
391
|
+
|
|
392
|
+
Raises:
|
|
393
|
+
OfflineHasNoRPCException: If not connected to the blockchain
|
|
394
|
+
ValueError: If any required parameter is invalid
|
|
395
|
+
"""
|
|
396
|
+
if not account or not isinstance(account, str):
|
|
397
|
+
raise ValueError("Account must be a non-empty string")
|
|
398
|
+
if not permlink or not isinstance(permlink, str):
|
|
399
|
+
raise ValueError("Permlink must be a non-empty string")
|
|
400
|
+
if not isinstance(notes, str):
|
|
401
|
+
raise ValueError("Notes must be a string")
|
|
402
|
+
if not mod_account or not isinstance(mod_account, str):
|
|
403
|
+
raise ValueError("Moderator account must be a non-empty string")
|
|
404
|
+
|
|
405
|
+
community = self["name"]
|
|
406
|
+
if not self.blockchain.is_connected():
|
|
407
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
408
|
+
|
|
409
|
+
json_body = [
|
|
410
|
+
"mutePost",
|
|
411
|
+
{
|
|
412
|
+
"community": community,
|
|
413
|
+
"account": account,
|
|
414
|
+
"permlink": permlink,
|
|
415
|
+
"notes": notes.strip(),
|
|
416
|
+
},
|
|
417
|
+
]
|
|
418
|
+
return self.blockchain.custom_json(
|
|
419
|
+
"community", json_body, required_posting_auths=[mod_account]
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
def unmute_post(self, account: str, permlink: str, notes: str, mod_account: str) -> dict:
|
|
423
|
+
"""Unmute a previously muted post in the community.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
account: Author of the post to unmute
|
|
427
|
+
permlink: Permlink of the post to unmute
|
|
428
|
+
notes: Reason for unmuting the post
|
|
429
|
+
mod_account: Account name of the moderator performing this action (must be mod or higher)
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
dict: Transaction result
|
|
433
|
+
|
|
434
|
+
Raises:
|
|
435
|
+
OfflineHasNoRPCException: If not connected to the blockchain
|
|
436
|
+
ValueError: If any required parameter is invalid
|
|
437
|
+
"""
|
|
438
|
+
if not account or not isinstance(account, str):
|
|
439
|
+
raise ValueError("Account must be a non-empty string")
|
|
440
|
+
if not permlink or not isinstance(permlink, str):
|
|
441
|
+
raise ValueError("Permlink must be a non-empty string")
|
|
442
|
+
if not isinstance(notes, str):
|
|
443
|
+
raise ValueError("Notes must be a string")
|
|
444
|
+
if not mod_account or not isinstance(mod_account, str):
|
|
445
|
+
raise ValueError("Moderator account must be a non-empty string")
|
|
446
|
+
|
|
447
|
+
community = self["name"]
|
|
448
|
+
if not self.blockchain.is_connected():
|
|
449
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
450
|
+
|
|
451
|
+
json_body = [
|
|
452
|
+
"unmutePost",
|
|
453
|
+
{
|
|
454
|
+
"community": community,
|
|
455
|
+
"account": account,
|
|
456
|
+
"permlink": permlink,
|
|
457
|
+
"notes": notes.strip(),
|
|
458
|
+
},
|
|
459
|
+
]
|
|
460
|
+
return self.blockchain.custom_json(
|
|
461
|
+
"community", json_body, required_posting_auths=[mod_account]
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
def update_props(
|
|
465
|
+
self,
|
|
466
|
+
title: str,
|
|
467
|
+
about: str,
|
|
468
|
+
is_nsfw: bool,
|
|
469
|
+
description: str,
|
|
470
|
+
flag_text: str,
|
|
471
|
+
admin_account: str,
|
|
472
|
+
) -> dict:
|
|
473
|
+
"""Update community properties.
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
title: New title for the community (must be non-empty)
|
|
477
|
+
about: Brief description of the community
|
|
478
|
+
is_nsfw: Whether the community contains NSFW content
|
|
479
|
+
description: Detailed description of the community
|
|
480
|
+
flag_text: Text shown when flagging content in this community
|
|
481
|
+
admin_account: Account name of the admin performing this action
|
|
482
|
+
|
|
483
|
+
Returns:
|
|
484
|
+
dict: Transaction result
|
|
485
|
+
|
|
486
|
+
Raises:
|
|
487
|
+
OfflineHasNoRPCException: If not connected to the blockchain
|
|
488
|
+
ValueError: If any required parameter is invalid
|
|
489
|
+
"""
|
|
490
|
+
if not title or not isinstance(title, str):
|
|
491
|
+
raise ValueError("Title must be a non-empty string")
|
|
492
|
+
if not isinstance(about, str):
|
|
493
|
+
about = ""
|
|
494
|
+
if not isinstance(description, str):
|
|
495
|
+
description = ""
|
|
496
|
+
if not isinstance(flag_text, str):
|
|
497
|
+
flag_text = ""
|
|
498
|
+
if not admin_account or not isinstance(admin_account, str):
|
|
499
|
+
raise ValueError("Admin account must be a non-empty string")
|
|
500
|
+
|
|
501
|
+
community = self["name"]
|
|
502
|
+
if not self.blockchain.is_connected():
|
|
503
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
504
|
+
|
|
505
|
+
json_body = [
|
|
506
|
+
"updateProps",
|
|
507
|
+
{
|
|
508
|
+
"community": community,
|
|
509
|
+
"props": {
|
|
510
|
+
"title": title.strip(),
|
|
511
|
+
"about": about.strip(),
|
|
512
|
+
"is_nsfw": bool(is_nsfw),
|
|
513
|
+
"description": description.strip(),
|
|
514
|
+
"flag_text": flag_text.strip(),
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
]
|
|
518
|
+
return self.blockchain.custom_json(
|
|
519
|
+
"community", json_body, required_posting_auths=[admin_account]
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
def subscribe(self, account: str) -> dict:
|
|
523
|
+
"""Subscribe an account to this community.
|
|
524
|
+
|
|
525
|
+
The account that calls this method will be subscribed to the community.
|
|
526
|
+
The same account must be used to sign the transaction.
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
account: Account name that wants to subscribe to the community
|
|
530
|
+
|
|
531
|
+
Returns:
|
|
532
|
+
dict: Transaction result
|
|
533
|
+
|
|
534
|
+
Raises:
|
|
535
|
+
OfflineHasNoRPCException: If not connected to the blockchain
|
|
536
|
+
ValueError: If account is invalid
|
|
537
|
+
"""
|
|
538
|
+
if not account or not isinstance(account, str):
|
|
539
|
+
raise ValueError("Account must be a non-empty string")
|
|
540
|
+
|
|
541
|
+
community = self["name"]
|
|
542
|
+
if not self.blockchain.is_connected():
|
|
543
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
544
|
+
|
|
545
|
+
json_body = [
|
|
546
|
+
"subscribe",
|
|
547
|
+
{
|
|
548
|
+
"community": community,
|
|
549
|
+
},
|
|
550
|
+
]
|
|
551
|
+
return self.blockchain.custom_json("community", json_body, required_posting_auths=[account])
|
|
552
|
+
|
|
553
|
+
def pin_post(self, account: str, permlink: str, mod_account: str) -> dict:
|
|
554
|
+
"""Pin a post to the top of the community feed.
|
|
555
|
+
|
|
556
|
+
This method allows community moderators to pin a specific post to the top of the
|
|
557
|
+
community's feed. The post will remain pinned until it is manually unpinned.
|
|
558
|
+
|
|
559
|
+
Args:
|
|
560
|
+
account: Author of the post to pin
|
|
561
|
+
permlink: Permlink of the post to pin
|
|
562
|
+
mod_account: Account name of the moderator performing this action (must be mod or higher)
|
|
563
|
+
|
|
564
|
+
Returns:
|
|
565
|
+
dict: Transaction result
|
|
566
|
+
|
|
567
|
+
Raises:
|
|
568
|
+
OfflineHasNoRPCException: If not connected to the blockchain
|
|
569
|
+
ValueError: If any required parameter is invalid
|
|
570
|
+
"""
|
|
571
|
+
if not account or not isinstance(account, str):
|
|
572
|
+
raise ValueError("Account must be a non-empty string")
|
|
573
|
+
if not permlink or not isinstance(permlink, str):
|
|
574
|
+
raise ValueError("Permlink must be a non-empty string")
|
|
575
|
+
if not mod_account or not isinstance(mod_account, str):
|
|
576
|
+
raise ValueError("Moderator account must be a non-empty string")
|
|
577
|
+
|
|
578
|
+
community = self["name"]
|
|
579
|
+
if not self.blockchain.is_connected():
|
|
580
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
581
|
+
|
|
582
|
+
json_body = [
|
|
583
|
+
"pinPost",
|
|
584
|
+
{
|
|
585
|
+
"community": community,
|
|
586
|
+
"account": account,
|
|
587
|
+
"permlink": permlink,
|
|
588
|
+
},
|
|
589
|
+
]
|
|
590
|
+
return self.blockchain.custom_json(
|
|
591
|
+
"community", json_body, required_posting_auths=[mod_account]
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
def unsubscribe(self, account: str) -> dict:
|
|
595
|
+
"""Unsubscribe an account from this community.
|
|
596
|
+
|
|
597
|
+
The account that calls this method will be unsubscribed from the community.
|
|
598
|
+
The same account must be used to sign the transaction.
|
|
599
|
+
|
|
600
|
+
Args:
|
|
601
|
+
account: Account name that wants to unsubscribe from the community
|
|
602
|
+
|
|
603
|
+
Returns:
|
|
604
|
+
dict: Transaction result
|
|
605
|
+
|
|
606
|
+
Raises:
|
|
607
|
+
OfflineHasNoRPCException: If not connected to the blockchain
|
|
608
|
+
ValueError: If account is invalid
|
|
609
|
+
"""
|
|
610
|
+
if not account or not isinstance(account, str):
|
|
611
|
+
raise ValueError("Account must be a non-empty string")
|
|
612
|
+
|
|
613
|
+
community = self["name"]
|
|
614
|
+
if not self.blockchain.is_connected():
|
|
615
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
616
|
+
|
|
617
|
+
json_body = [
|
|
618
|
+
"unsubscribe",
|
|
619
|
+
{
|
|
620
|
+
"community": community,
|
|
621
|
+
},
|
|
622
|
+
]
|
|
623
|
+
return self.blockchain.custom_json("community", json_body, required_posting_auths=[account])
|
|
624
|
+
|
|
625
|
+
def unpin_post(self, account: str, permlink: str, mod_account: str) -> dict:
|
|
626
|
+
"""Remove a post from being pinned at the top of the community feed.
|
|
627
|
+
|
|
628
|
+
This method allows community moderators to unpin a previously pinned post.
|
|
629
|
+
After unpinning, the post will return to its normal position in the feed.
|
|
630
|
+
|
|
631
|
+
Args:
|
|
632
|
+
account: Author of the post to unpin
|
|
633
|
+
permlink: Permlink of the post to unpin
|
|
634
|
+
mod_account: Account name of the moderator performing this action (must be mod or higher)
|
|
635
|
+
|
|
636
|
+
Returns:
|
|
637
|
+
dict: Transaction result
|
|
638
|
+
|
|
639
|
+
Raises:
|
|
640
|
+
OfflineHasNoRPCException: If not connected to the blockchain
|
|
641
|
+
ValueError: If any required parameter is invalid
|
|
642
|
+
"""
|
|
643
|
+
if not account or not isinstance(account, str):
|
|
644
|
+
raise ValueError("Account must be a non-empty string")
|
|
645
|
+
if not permlink or not isinstance(permlink, str):
|
|
646
|
+
raise ValueError("Permlink must be a non-empty string")
|
|
647
|
+
if not mod_account or not isinstance(mod_account, str):
|
|
648
|
+
raise ValueError("Moderator account must be a non-empty string")
|
|
649
|
+
|
|
650
|
+
community = self["name"]
|
|
651
|
+
if not self.blockchain.is_connected():
|
|
652
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
653
|
+
|
|
654
|
+
json_body = [
|
|
655
|
+
"unpinPost",
|
|
656
|
+
{
|
|
657
|
+
"community": community,
|
|
658
|
+
"account": account,
|
|
659
|
+
"permlink": permlink,
|
|
660
|
+
},
|
|
661
|
+
]
|
|
662
|
+
return self.blockchain.custom_json(
|
|
663
|
+
"community", json_body, required_posting_auths=[mod_account]
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
def flag_post(self, account: str, permlink: str, notes: str, reporter: str) -> dict:
|
|
667
|
+
"""Report a post to the community moderators for review.
|
|
668
|
+
|
|
669
|
+
This method allows community members to flag posts that may violate
|
|
670
|
+
community guidelines. The post will be added to the community's
|
|
671
|
+
review queue for moderators to evaluate.
|
|
672
|
+
|
|
673
|
+
Args:
|
|
674
|
+
account: Author of the post being reported
|
|
675
|
+
permlink: Permlink of the post being reported
|
|
676
|
+
notes: Explanation of why the post is being reported
|
|
677
|
+
reporter: Account name of the user reporting the post
|
|
678
|
+
|
|
679
|
+
Returns:
|
|
680
|
+
dict: Transaction result
|
|
681
|
+
|
|
682
|
+
Raises:
|
|
683
|
+
OfflineHasNoRPCException: If not connected to the blockchain
|
|
684
|
+
ValueError: If any required parameter is invalid
|
|
685
|
+
"""
|
|
686
|
+
if not account or not isinstance(account, str):
|
|
687
|
+
raise ValueError("Account must be a non-empty string")
|
|
688
|
+
if not permlink or not isinstance(permlink, str):
|
|
689
|
+
raise ValueError("Permlink must be a non-empty string")
|
|
690
|
+
if not notes or not isinstance(notes, str):
|
|
691
|
+
raise ValueError("Notes must be a string")
|
|
692
|
+
if not reporter or not isinstance(reporter, str):
|
|
693
|
+
raise ValueError("Reporter account must be a non-empty string")
|
|
694
|
+
|
|
695
|
+
community = self["name"]
|
|
696
|
+
if not self.blockchain.is_connected():
|
|
697
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
698
|
+
|
|
699
|
+
json_body = [
|
|
700
|
+
"flagPost",
|
|
701
|
+
{
|
|
702
|
+
"community": community,
|
|
703
|
+
"account": account,
|
|
704
|
+
"permlink": permlink,
|
|
705
|
+
"notes": notes.strip(),
|
|
706
|
+
},
|
|
707
|
+
]
|
|
708
|
+
return self.blockchain.custom_json(
|
|
709
|
+
"community", json_body, required_posting_auths=[reporter]
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
class CommunityObject(list):
|
|
714
|
+
"""A list-like container for Community objects with additional utility methods."""
|
|
715
|
+
|
|
716
|
+
def printAsTable(self) -> None:
|
|
717
|
+
"""Print a formatted table of communities with key metrics.
|
|
718
|
+
|
|
719
|
+
The table includes the following columns:
|
|
720
|
+
- Nr.: Sequential number
|
|
721
|
+
- Name: Community name
|
|
722
|
+
- Title: Community title
|
|
723
|
+
- lang: Language code
|
|
724
|
+
- subscribers: Number of subscribers
|
|
725
|
+
- sum_pending: Sum of pending payouts
|
|
726
|
+
- num_pending: Number of pending posts
|
|
727
|
+
- num_authors: Number of unique authors
|
|
728
|
+
"""
|
|
729
|
+
t = PrettyTable(
|
|
730
|
+
[
|
|
731
|
+
"Nr.",
|
|
732
|
+
"Name",
|
|
733
|
+
"Title",
|
|
734
|
+
"lang",
|
|
735
|
+
"subscribers",
|
|
736
|
+
"sum_pending",
|
|
737
|
+
"num_pending",
|
|
738
|
+
"num_authors",
|
|
739
|
+
]
|
|
740
|
+
)
|
|
741
|
+
t.align = "l"
|
|
742
|
+
count = 0
|
|
743
|
+
for community in self:
|
|
744
|
+
count += 1
|
|
745
|
+
t.add_row(
|
|
746
|
+
[
|
|
747
|
+
str(count),
|
|
748
|
+
community["name"],
|
|
749
|
+
community["title"],
|
|
750
|
+
community["lang"],
|
|
751
|
+
community["subscribers"],
|
|
752
|
+
community["sum_pending"],
|
|
753
|
+
community["num_pending"],
|
|
754
|
+
community["num_authors"],
|
|
755
|
+
]
|
|
756
|
+
)
|
|
757
|
+
print(t)
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
class Communities(CommunityObject):
|
|
761
|
+
"""A list of communities with additional querying capabilities.
|
|
762
|
+
|
|
763
|
+
This class extends CommunityObject to provide methods for fetching and
|
|
764
|
+
searching communities from the blockchain.
|
|
765
|
+
|
|
766
|
+
Args:
|
|
767
|
+
sort: Sort order for communities (default: "rank")
|
|
768
|
+
observer: Observer account for personalized results (optional)
|
|
769
|
+
last: Last community name for pagination (optional)
|
|
770
|
+
limit: Maximum number of communities to fetch (default: 100)
|
|
771
|
+
lazy: If True, use lazy loading (default: False)
|
|
772
|
+
full: If True, fetch full community data (default: True)
|
|
773
|
+
blockchain_instance: Blockchain instance to use for RPC access
|
|
774
|
+
"""
|
|
775
|
+
|
|
776
|
+
def __init__(
|
|
777
|
+
self,
|
|
778
|
+
sort: str = "rank",
|
|
779
|
+
observer: str | None = None,
|
|
780
|
+
last: str | None = None,
|
|
781
|
+
limit: int = 100,
|
|
782
|
+
lazy: bool = False,
|
|
783
|
+
full: bool = True,
|
|
784
|
+
blockchain_instance=None,
|
|
785
|
+
) -> None:
|
|
786
|
+
"""
|
|
787
|
+
Initialize a Communities collection by querying the blockchain for community metadata.
|
|
788
|
+
|
|
789
|
+
Fetches up to `limit` communities from the resolved blockchain instance using paginated bridge RPC calls and constructs Community objects from the results.
|
|
790
|
+
|
|
791
|
+
Parameters:
|
|
792
|
+
sort (str): Sort order for results (e.g., "rank"). Defaults to "rank".
|
|
793
|
+
observer (str | None): Account used to personalize results; passed through to the RPC call.
|
|
794
|
+
last (str | None): Starting community name for pagination; used as the RPC `last` parameter.
|
|
795
|
+
limit (int): Maximum number of communities to fetch (clamped per-request to 100). Defaults to 100.
|
|
796
|
+
lazy (bool): If True, created Community objects will use lazy loading. Defaults to False.
|
|
797
|
+
full (bool): If True, created Community objects will request full data. Defaults to True.
|
|
798
|
+
|
|
799
|
+
Notes:
|
|
800
|
+
- If no blockchain instance is connected, initialization returns early and yields an empty collection.
|
|
801
|
+
- The constructor ensures at most `limit` Community objects are created.
|
|
802
|
+
"""
|
|
803
|
+
self.blockchain = blockchain_instance or shared_blockchain_instance()
|
|
804
|
+
|
|
805
|
+
if not self.blockchain.is_connected():
|
|
806
|
+
return
|
|
807
|
+
|
|
808
|
+
communities = []
|
|
809
|
+
community_cnt = 0
|
|
810
|
+
batch_limit = min(100, limit) # Ensure we don't exceed the limit
|
|
811
|
+
|
|
812
|
+
while community_cnt < limit:
|
|
813
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
814
|
+
batch = self.blockchain.rpc.list_communities(
|
|
815
|
+
{"sort": sort, "observer": observer, "last": last, "limit": batch_limit},
|
|
816
|
+
)
|
|
817
|
+
if not batch: # No more communities to fetch
|
|
818
|
+
break
|
|
819
|
+
|
|
820
|
+
communities.extend(batch)
|
|
821
|
+
community_cnt += len(batch)
|
|
822
|
+
last = communities[-1]["name"]
|
|
823
|
+
|
|
824
|
+
# Adjust batch size for the next iteration if needed
|
|
825
|
+
if community_cnt + batch_limit > limit:
|
|
826
|
+
batch_limit = limit - community_cnt
|
|
827
|
+
|
|
828
|
+
super().__init__(
|
|
829
|
+
[
|
|
830
|
+
Community(x, lazy=lazy, full=full, blockchain_instance=self.blockchain)
|
|
831
|
+
for x in communities[:limit] # Ensure we don't exceed the limit
|
|
832
|
+
]
|
|
833
|
+
)
|
|
834
|
+
|
|
835
|
+
def search_title(self, title: str) -> CommunityObject:
|
|
836
|
+
"""Search for communities with titles containing the given string.
|
|
837
|
+
|
|
838
|
+
The search is case-insensitive.
|
|
839
|
+
|
|
840
|
+
Args:
|
|
841
|
+
title: Text to search for in community titles
|
|
842
|
+
|
|
843
|
+
Returns:
|
|
844
|
+
CommunityObject: A new CommunityObject containing matching communities
|
|
845
|
+
"""
|
|
846
|
+
if not title or not isinstance(title, str):
|
|
847
|
+
raise ValueError("Title must be a non-empty string")
|
|
848
|
+
|
|
849
|
+
ret = CommunityObject()
|
|
850
|
+
title_lower = title.lower()
|
|
851
|
+
for community in self:
|
|
852
|
+
if title_lower in community["title"].lower():
|
|
853
|
+
ret.append(community)
|
|
854
|
+
return ret
|