rednote-cli 0.1.0__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.
- rednote_cli/__init__.py +5 -0
- rednote_cli/_runtime/__init__.py +0 -0
- rednote_cli/_runtime/common/__init__.py +0 -0
- rednote_cli/_runtime/common/app_utils.py +77 -0
- rednote_cli/_runtime/common/config.py +83 -0
- rednote_cli/_runtime/common/enums.py +17 -0
- rednote_cli/_runtime/common/errors.py +22 -0
- rednote_cli/_runtime/core/__init__.py +0 -0
- rednote_cli/_runtime/core/account_manager.py +349 -0
- rednote_cli/_runtime/core/browser/__init__.py +0 -0
- rednote_cli/_runtime/core/browser/manager.py +247 -0
- rednote_cli/_runtime/core/database/__init__.py +0 -0
- rednote_cli/_runtime/core/database/manager.py +334 -0
- rednote_cli/_runtime/platforms/__init__.py +0 -0
- rednote_cli/_runtime/platforms/base.py +62 -0
- rednote_cli/_runtime/platforms/factory.py +55 -0
- rednote_cli/_runtime/platforms/publishing/__init__.py +12 -0
- rednote_cli/_runtime/platforms/publishing/media.py +275 -0
- rednote_cli/_runtime/platforms/publishing/models.py +59 -0
- rednote_cli/_runtime/platforms/publishing/validator.py +124 -0
- rednote_cli/_runtime/services/__init__.py +1 -0
- rednote_cli/_runtime/services/scraper_service.py +235 -0
- rednote_cli/adapters/__init__.py +1 -0
- rednote_cli/adapters/output/__init__.py +1 -0
- rednote_cli/adapters/output/event_stream.py +29 -0
- rednote_cli/adapters/output/formatter_json.py +23 -0
- rednote_cli/adapters/output/formatter_table.py +39 -0
- rednote_cli/adapters/output/writer.py +17 -0
- rednote_cli/adapters/persistence/__init__.py +1 -0
- rednote_cli/adapters/persistence/file_account_repo.py +51 -0
- rednote_cli/adapters/platform/__init__.py +1 -0
- rednote_cli/adapters/platform/rednote/__init__.py +1 -0
- rednote_cli/adapters/platform/rednote/extractor.py +65 -0
- rednote_cli/adapters/platform/rednote/publisher.py +26 -0
- rednote_cli/adapters/platform/rednote/runtime_extractor.py +818 -0
- rednote_cli/adapters/platform/rednote/runtime_publisher.py +373 -0
- rednote_cli/adapters/platform/rednote/runtime_registration.py +20 -0
- rednote_cli/application/__init__.py +1 -0
- rednote_cli/application/dto/__init__.py +1 -0
- rednote_cli/application/dto/input_models.py +121 -0
- rednote_cli/application/dto/output_models.py +78 -0
- rednote_cli/application/use_cases/__init__.py +1 -0
- rednote_cli/application/use_cases/account_list.py +9 -0
- rednote_cli/application/use_cases/account_mutation.py +22 -0
- rednote_cli/application/use_cases/auth_login.py +64 -0
- rednote_cli/application/use_cases/auth_status.py +96 -0
- rednote_cli/application/use_cases/doctor.py +49 -0
- rednote_cli/application/use_cases/init_runtime.py +20 -0
- rednote_cli/application/use_cases/note_get.py +22 -0
- rednote_cli/application/use_cases/note_search.py +26 -0
- rednote_cli/application/use_cases/publish_note.py +25 -0
- rednote_cli/application/use_cases/user_get.py +18 -0
- rednote_cli/application/use_cases/user_search.py +8 -0
- rednote_cli/application/use_cases/user_self.py +8 -0
- rednote_cli/cli/__init__.py +1 -0
- rednote_cli/cli/__main__.py +5 -0
- rednote_cli/cli/commands/__init__.py +1 -0
- rednote_cli/cli/commands/account.py +204 -0
- rednote_cli/cli/commands/doctor.py +20 -0
- rednote_cli/cli/commands/init.py +20 -0
- rednote_cli/cli/commands/note.py +101 -0
- rednote_cli/cli/commands/publish.py +147 -0
- rednote_cli/cli/commands/search.py +185 -0
- rednote_cli/cli/commands/user.py +113 -0
- rednote_cli/cli/main.py +163 -0
- rednote_cli/cli/options.py +13 -0
- rednote_cli/cli/runtime.py +142 -0
- rednote_cli/cli/utils.py +74 -0
- rednote_cli/domain/__init__.py +1 -0
- rednote_cli/domain/errors.py +50 -0
- rednote_cli/domain/note_search_filters.py +155 -0
- rednote_cli/infra/__init__.py +1 -0
- rednote_cli/infra/exit_codes.py +30 -0
- rednote_cli/infra/logger.py +11 -0
- rednote_cli/infra/paths.py +31 -0
- rednote_cli/infra/platforms.py +4 -0
- rednote_cli-0.1.0.dist-info/METADATA +81 -0
- rednote_cli-0.1.0.dist-info/RECORD +81 -0
- rednote_cli-0.1.0.dist-info/WHEEL +5 -0
- rednote_cli-0.1.0.dist-info/entry_points.txt +2 -0
- rednote_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
from typing import List, Optional, Union
|
|
2
|
+
|
|
3
|
+
from rednote_cli._runtime.common.enums import Platform, XsecSource
|
|
4
|
+
from rednote_cli._runtime.common.errors import PublishExecutionError, PublishNoteException
|
|
5
|
+
from rednote_cli._runtime.core.account_manager import AccountLeaseManager, AccountManager
|
|
6
|
+
from rednote_cli._runtime.core.browser.manager import browser_manager
|
|
7
|
+
from rednote_cli._runtime.platforms.factory import PlatformFactory
|
|
8
|
+
from rednote_cli._runtime.platforms.base import RiskControlException
|
|
9
|
+
from rednote_cli._runtime.platforms.publishing.media import MediaPreprocessor
|
|
10
|
+
from rednote_cli._runtime.platforms.publishing.models import PublishResult, PublishStage, PublishTarget
|
|
11
|
+
from rednote_cli._runtime.platforms.publishing import normalize_publish_request
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _normalize_xsec_source(xsec_source: Union[XsecSource, str, None]) -> XsecSource:
|
|
15
|
+
if isinstance(xsec_source, XsecSource):
|
|
16
|
+
return xsec_source
|
|
17
|
+
if isinstance(xsec_source, str) and xsec_source:
|
|
18
|
+
for source in XsecSource:
|
|
19
|
+
if source.value == xsec_source:
|
|
20
|
+
return source
|
|
21
|
+
return XsecSource.PC_FEED
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _normalize_platform(platform: Union[Platform, str, None]) -> Platform:
|
|
25
|
+
if isinstance(platform, Platform):
|
|
26
|
+
return platform
|
|
27
|
+
if isinstance(platform, str) and platform:
|
|
28
|
+
for item in Platform:
|
|
29
|
+
if item.value == platform:
|
|
30
|
+
return item
|
|
31
|
+
return Platform.REDNOTE
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _strip_sensitive_account_fields(accounts: List[dict]) -> List[dict]:
|
|
35
|
+
cleaned = []
|
|
36
|
+
for account in accounts:
|
|
37
|
+
item = dict(account)
|
|
38
|
+
item.pop("storage_state", None)
|
|
39
|
+
cleaned.append(item)
|
|
40
|
+
return cleaned
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _build_publish_result_from_raw(raw_result) -> PublishResult:
|
|
44
|
+
if isinstance(raw_result, dict):
|
|
45
|
+
uploaded_count = int(raw_result.get("uploaded_count", 0) or 0)
|
|
46
|
+
status = str(raw_result.get("status", "")).strip().lower()
|
|
47
|
+
if status in {"uploaded", "submitted", "success", "verified"}:
|
|
48
|
+
if status == "uploaded":
|
|
49
|
+
stage = PublishStage.UPLOAD_DONE
|
|
50
|
+
elif status == "verified":
|
|
51
|
+
stage = PublishStage.VERIFIED
|
|
52
|
+
else:
|
|
53
|
+
stage = PublishStage.SUBMITTED
|
|
54
|
+
return PublishResult(
|
|
55
|
+
success=True,
|
|
56
|
+
stage=stage,
|
|
57
|
+
uploaded_count=uploaded_count,
|
|
58
|
+
publish_url=str(raw_result.get("publish_url") or ""),
|
|
59
|
+
publish_id=str(raw_result.get("publish_id") or ""),
|
|
60
|
+
message=str(raw_result.get("message") or ""),
|
|
61
|
+
data=raw_result,
|
|
62
|
+
)
|
|
63
|
+
return PublishResult(
|
|
64
|
+
success=False,
|
|
65
|
+
stage=PublishStage.FAILED,
|
|
66
|
+
uploaded_count=uploaded_count,
|
|
67
|
+
message=str(raw_result.get("message") or "发布失败"),
|
|
68
|
+
data=raw_result,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return PublishResult(
|
|
72
|
+
success=False,
|
|
73
|
+
stage=PublishStage.FAILED,
|
|
74
|
+
message=f"无法识别的发布返回值类型: {type(raw_result).__name__}",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def search_notes(
|
|
79
|
+
keyword: str,
|
|
80
|
+
size: int = 20,
|
|
81
|
+
sort_by: Optional[str] = None,
|
|
82
|
+
note_type: Optional[str] = None,
|
|
83
|
+
publish_time: Optional[str] = None,
|
|
84
|
+
search_scope: Optional[str] = None,
|
|
85
|
+
location: Optional[str] = None,
|
|
86
|
+
account_uid: Optional[str] = None,
|
|
87
|
+
platform: Union[Platform, str, None] = Platform.REDNOTE
|
|
88
|
+
) -> List[dict]:
|
|
89
|
+
platform_enum = _normalize_platform(platform)
|
|
90
|
+
async with AccountLeaseManager.lease_account(platform_enum, account_uid) as account:
|
|
91
|
+
storage_state = account.get("storage_state")
|
|
92
|
+
async with browser_manager.get_page(storage_state, account_key=account.get("user_no")) as page:
|
|
93
|
+
extractor = PlatformFactory.get_extractor(platform_enum, page)
|
|
94
|
+
notes = await extractor.search_notes(
|
|
95
|
+
keyword=keyword,
|
|
96
|
+
size=size,
|
|
97
|
+
sort_by=sort_by,
|
|
98
|
+
note_type=note_type,
|
|
99
|
+
publish_time=publish_time,
|
|
100
|
+
search_scope=search_scope,
|
|
101
|
+
location=location,
|
|
102
|
+
)
|
|
103
|
+
return notes if isinstance(notes, list) else []
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
async def search_users(
|
|
107
|
+
keyword: str,
|
|
108
|
+
size: int = 20,
|
|
109
|
+
account_uid: Optional[str] = None,
|
|
110
|
+
platform: Union[Platform, str, None] = Platform.REDNOTE
|
|
111
|
+
) -> List[dict]:
|
|
112
|
+
platform_enum = _normalize_platform(platform)
|
|
113
|
+
async with AccountLeaseManager.lease_account(platform_enum, account_uid) as account:
|
|
114
|
+
storage_state = account.get("storage_state")
|
|
115
|
+
async with browser_manager.get_page(storage_state, account_key=account.get("user_no")) as page:
|
|
116
|
+
extractor = PlatformFactory.get_extractor(platform_enum, page)
|
|
117
|
+
users = await extractor.search_users(keyword, size)
|
|
118
|
+
return users if isinstance(users, list) else []
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
async def get_user_info(
|
|
122
|
+
user_id: str,
|
|
123
|
+
xsec_token: Optional[str] = None,
|
|
124
|
+
xsec_source: Union[XsecSource, str, None] = XsecSource.PC_FEED,
|
|
125
|
+
account_uid: Optional[str] = None,
|
|
126
|
+
platform: Union[Platform, str, None] = Platform.REDNOTE
|
|
127
|
+
) -> dict:
|
|
128
|
+
platform_enum = _normalize_platform(platform)
|
|
129
|
+
normalized_source = _normalize_xsec_source(xsec_source)
|
|
130
|
+
async with AccountLeaseManager.lease_account(platform_enum, account_uid) as account:
|
|
131
|
+
storage_state = account.get("storage_state")
|
|
132
|
+
async with browser_manager.get_page(storage_state, account_key=account.get("user_no")) as page:
|
|
133
|
+
extractor = PlatformFactory.get_extractor(platform_enum, page)
|
|
134
|
+
return await extractor.get_user_info(user_id, xsec_token, normalized_source)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def get_note_info(
|
|
138
|
+
note_id: str,
|
|
139
|
+
xsec_token: Optional[str] = None,
|
|
140
|
+
xsec_source: Union[XsecSource, str, None] = XsecSource.PC_FEED,
|
|
141
|
+
account_uid: Optional[str] = None,
|
|
142
|
+
comment_size: int = 10,
|
|
143
|
+
sub_comment_size: int = 5,
|
|
144
|
+
platform: Union[Platform, str, None] = Platform.REDNOTE,
|
|
145
|
+
) -> dict:
|
|
146
|
+
platform_enum = _normalize_platform(platform)
|
|
147
|
+
normalized_source = _normalize_xsec_source(xsec_source)
|
|
148
|
+
async with AccountLeaseManager.lease_account(platform_enum, account_uid) as account:
|
|
149
|
+
storage_state = account.get("storage_state")
|
|
150
|
+
async with browser_manager.get_page(storage_state, account_key=account.get("user_no")) as page:
|
|
151
|
+
extractor = PlatformFactory.get_extractor(platform_enum, page)
|
|
152
|
+
return await extractor.get_note_info(
|
|
153
|
+
note_id=note_id,
|
|
154
|
+
xsec_token=xsec_token,
|
|
155
|
+
xsec_source=normalized_source,
|
|
156
|
+
comment_size=comment_size,
|
|
157
|
+
sub_comment_size=sub_comment_size,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
async def get_self_info(
|
|
162
|
+
account_uid: Optional[str] = None,
|
|
163
|
+
platform: Union[Platform, str, None] = Platform.REDNOTE,
|
|
164
|
+
) -> dict:
|
|
165
|
+
platform_enum = _normalize_platform(platform)
|
|
166
|
+
async with AccountLeaseManager.lease_account(platform_enum, account_uid) as account:
|
|
167
|
+
storage_state = account.get("storage_state")
|
|
168
|
+
async with browser_manager.get_page(storage_state, account_key=account.get("user_no")) as page:
|
|
169
|
+
extractor = PlatformFactory.get_extractor(platform_enum, page)
|
|
170
|
+
return await extractor.get_self_info()
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
async def publish_note(
|
|
174
|
+
target: str,
|
|
175
|
+
image_list: Optional[list] = None,
|
|
176
|
+
title: str = "",
|
|
177
|
+
content: str = "",
|
|
178
|
+
tags: Optional[list] = None,
|
|
179
|
+
schedule_at: Optional[str] = None,
|
|
180
|
+
account_uid: Optional[str] = None,
|
|
181
|
+
platform: Union[Platform, str, None] = Platform.REDNOTE,
|
|
182
|
+
) -> dict:
|
|
183
|
+
platform_enum = _normalize_platform(platform)
|
|
184
|
+
request = normalize_publish_request(
|
|
185
|
+
target=target,
|
|
186
|
+
title=title,
|
|
187
|
+
content=content,
|
|
188
|
+
media_list=image_list,
|
|
189
|
+
tags=tags,
|
|
190
|
+
schedule_at=schedule_at,
|
|
191
|
+
account_uid=account_uid,
|
|
192
|
+
)
|
|
193
|
+
stage = PublishStage.INIT
|
|
194
|
+
try:
|
|
195
|
+
async with MediaPreprocessor() as preprocessor:
|
|
196
|
+
if request.target == PublishTarget.IMAGE:
|
|
197
|
+
prepared_media = await preprocessor.prepare_images(request.media_list)
|
|
198
|
+
elif request.target == PublishTarget.VIDEO:
|
|
199
|
+
prepared_media = await preprocessor.prepare_videos(request.media_list)
|
|
200
|
+
else:
|
|
201
|
+
prepared_media = list(request.media_list or [])
|
|
202
|
+
stage = PublishStage.MEDIA_READY
|
|
203
|
+
|
|
204
|
+
async with AccountLeaseManager.lease_account(platform_enum, request.account_uid) as account:
|
|
205
|
+
storage_state = account.get("storage_state")
|
|
206
|
+
account_key = account.get("user_no")
|
|
207
|
+
async with browser_manager.get_page(storage_state, account_key=account_key) as page:
|
|
208
|
+
extractor = PlatformFactory.get_extractor(platform_enum, page)
|
|
209
|
+
raw_result = await extractor.publish_note(
|
|
210
|
+
target=request.target.value,
|
|
211
|
+
image_list=prepared_media,
|
|
212
|
+
title=request.title,
|
|
213
|
+
content=request.content,
|
|
214
|
+
tags=request.tags,
|
|
215
|
+
schedule_at=request.schedule_at,
|
|
216
|
+
)
|
|
217
|
+
result = _build_publish_result_from_raw(raw_result)
|
|
218
|
+
if result.stage == PublishStage.UPLOAD_DONE:
|
|
219
|
+
result.data["pipeline_stage"] = PublishStage.UPLOAD_DONE.value
|
|
220
|
+
return result.to_dict()
|
|
221
|
+
except RiskControlException:
|
|
222
|
+
raise
|
|
223
|
+
except PublishNoteException:
|
|
224
|
+
raise
|
|
225
|
+
except Exception as exc:
|
|
226
|
+
raise PublishExecutionError(f"发布编排执行失败(stage={stage.value}): {exc}") from exc
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
async def list_accounts(
|
|
230
|
+
platform: str = Platform.REDNOTE.value,
|
|
231
|
+
only_active: bool = True
|
|
232
|
+
) -> List[dict]:
|
|
233
|
+
accounts = AccountManager.list_accounts(only_active=only_active)
|
|
234
|
+
filtered = [acc for acc in accounts if acc.get("platform") == platform]
|
|
235
|
+
return _strip_sensitive_account_fields(filtered)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Adapters layer."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Output adapters."""
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from rednote_cli.adapters.output.writer import write_error
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _now_iso() -> str:
|
|
11
|
+
return datetime.now(timezone.utc).isoformat()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def emit_event(
|
|
15
|
+
*,
|
|
16
|
+
command: str,
|
|
17
|
+
event: str,
|
|
18
|
+
data: dict[str, Any] | None = None,
|
|
19
|
+
trace_id: str | None = None,
|
|
20
|
+
) -> None:
|
|
21
|
+
payload = {
|
|
22
|
+
"type": "event",
|
|
23
|
+
"command": command,
|
|
24
|
+
"event": event,
|
|
25
|
+
"timestamp": _now_iso(),
|
|
26
|
+
"trace_id": trace_id,
|
|
27
|
+
"data": data or {},
|
|
28
|
+
}
|
|
29
|
+
write_error(json.dumps(payload, ensure_ascii=False, separators=(",", ":")))
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def format_json(payload: Any, ensure_ascii: bool = False, indent: int = 2) -> str:
|
|
10
|
+
if isinstance(payload, BaseModel):
|
|
11
|
+
data = payload.model_dump(mode="json")
|
|
12
|
+
else:
|
|
13
|
+
data = payload
|
|
14
|
+
return json.dumps(data, ensure_ascii=ensure_ascii, indent=indent)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def format_jsonl(payloads: list[Any], ensure_ascii: bool = False) -> str:
|
|
18
|
+
lines = []
|
|
19
|
+
for item in payloads:
|
|
20
|
+
if isinstance(item, BaseModel):
|
|
21
|
+
item = item.model_dump(mode="json")
|
|
22
|
+
lines.append(json.dumps(item, ensure_ascii=ensure_ascii))
|
|
23
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from io import StringIO
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _normalize_rows(data: Any) -> list[dict]:
|
|
11
|
+
if isinstance(data, list):
|
|
12
|
+
rows = []
|
|
13
|
+
for item in data:
|
|
14
|
+
if isinstance(item, dict):
|
|
15
|
+
rows.append(item)
|
|
16
|
+
else:
|
|
17
|
+
rows.append({"value": item})
|
|
18
|
+
return rows
|
|
19
|
+
|
|
20
|
+
if isinstance(data, dict):
|
|
21
|
+
return [data]
|
|
22
|
+
|
|
23
|
+
return [{"value": data}]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def format_table(data: Any) -> str:
|
|
27
|
+
rows = _normalize_rows(data)
|
|
28
|
+
columns = sorted({key for row in rows for key in row.keys()})
|
|
29
|
+
table = Table(show_header=True, header_style="bold cyan")
|
|
30
|
+
for col in columns:
|
|
31
|
+
table.add_column(col, overflow="fold")
|
|
32
|
+
|
|
33
|
+
for row in rows:
|
|
34
|
+
table.add_row(*[str(row.get(col, "")) for col in columns])
|
|
35
|
+
|
|
36
|
+
buffer = StringIO()
|
|
37
|
+
console = Console(file=buffer, force_terminal=False, color_system=None)
|
|
38
|
+
console.print(table)
|
|
39
|
+
return buffer.getvalue().rstrip()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def write_output(content: str, out_file: str | None = None) -> None:
|
|
8
|
+
print(content)
|
|
9
|
+
if out_file:
|
|
10
|
+
path = Path(out_file).expanduser().resolve()
|
|
11
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
12
|
+
path.write_text(content + "\n", encoding="utf-8")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def write_error(content: str) -> None:
|
|
16
|
+
sys.stderr.write(content + "\n")
|
|
17
|
+
sys.stderr.flush()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Persistence adapters."""
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from rednote_cli._runtime.core.database.manager import DatabaseManager
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FileAccountRepository:
|
|
9
|
+
@staticmethod
|
|
10
|
+
def _resolve_account(platform: str, account_uid: str) -> dict[str, Any] | None:
|
|
11
|
+
target = str(account_uid or "").strip()
|
|
12
|
+
if not target:
|
|
13
|
+
return None
|
|
14
|
+
accounts = DatabaseManager.get_all_accounts(only_active=False)
|
|
15
|
+
for item in accounts:
|
|
16
|
+
if item.get("platform") != platform:
|
|
17
|
+
continue
|
|
18
|
+
if str(item.get("user_no") or "").strip() == target:
|
|
19
|
+
return item
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def _to_cli_account(item: dict[str, Any]) -> dict[str, Any]:
|
|
24
|
+
output = dict(item)
|
|
25
|
+
output.pop("storage_state", None)
|
|
26
|
+
output["account_uid"] = output.get("user_no")
|
|
27
|
+
return output
|
|
28
|
+
|
|
29
|
+
def list_accounts(self, platform: str = "Rednote", only_active: bool = True) -> list[dict[str, Any]]:
|
|
30
|
+
accounts = DatabaseManager.get_all_accounts(only_active=only_active)
|
|
31
|
+
filtered = [acc for acc in accounts if acc.get("platform") == platform]
|
|
32
|
+
return [self._to_cli_account(item) for item in filtered]
|
|
33
|
+
|
|
34
|
+
def delete(self, platform: str, account_uid: str) -> bool:
|
|
35
|
+
record = self._resolve_account(platform=platform, account_uid=account_uid)
|
|
36
|
+
if not record:
|
|
37
|
+
return False
|
|
38
|
+
DatabaseManager.delete_account(platform, record["user_no"])
|
|
39
|
+
return True
|
|
40
|
+
|
|
41
|
+
def activate(self, platform: str, account_uid: str) -> bool:
|
|
42
|
+
record = self._resolve_account(platform=platform, account_uid=account_uid)
|
|
43
|
+
if not record:
|
|
44
|
+
return False
|
|
45
|
+
return DatabaseManager.set_account_login_status(platform=platform, user_no=record["user_no"], is_logged_in=True)
|
|
46
|
+
|
|
47
|
+
def deactivate(self, platform: str, account_uid: str) -> bool:
|
|
48
|
+
record = self._resolve_account(platform=platform, account_uid=account_uid)
|
|
49
|
+
if not record:
|
|
50
|
+
return False
|
|
51
|
+
return DatabaseManager.set_account_login_status(platform=platform, user_no=record["user_no"], is_logged_in=False)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Platform adapters."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Rednote platform adapters."""
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from rednote_cli._runtime.services import scraper_service
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RednoteExtractorAdapter:
|
|
7
|
+
async def search_notes(
|
|
8
|
+
self,
|
|
9
|
+
keyword: str,
|
|
10
|
+
size: int,
|
|
11
|
+
sort_by: str | None = None,
|
|
12
|
+
note_type: str | None = None,
|
|
13
|
+
publish_time: str | None = None,
|
|
14
|
+
search_scope: str | None = None,
|
|
15
|
+
location: str | None = None,
|
|
16
|
+
account_uid: str | None = None,
|
|
17
|
+
) -> list[dict]:
|
|
18
|
+
return await scraper_service.search_notes(
|
|
19
|
+
keyword=keyword,
|
|
20
|
+
size=size,
|
|
21
|
+
sort_by=sort_by,
|
|
22
|
+
note_type=note_type,
|
|
23
|
+
publish_time=publish_time,
|
|
24
|
+
search_scope=search_scope,
|
|
25
|
+
location=location,
|
|
26
|
+
account_uid=account_uid,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
async def search_users(self, keyword: str, size: int, account_uid: str | None = None) -> list[dict]:
|
|
30
|
+
return await scraper_service.search_users(keyword=keyword, size=size, account_uid=account_uid)
|
|
31
|
+
|
|
32
|
+
async def get_user_info(
|
|
33
|
+
self,
|
|
34
|
+
user_id: str,
|
|
35
|
+
xsec_token: str | None = None,
|
|
36
|
+
xsec_source: str | None = "pc_feed",
|
|
37
|
+
account_uid: str | None = None,
|
|
38
|
+
) -> dict:
|
|
39
|
+
return await scraper_service.get_user_info(
|
|
40
|
+
user_id=user_id,
|
|
41
|
+
xsec_token=xsec_token,
|
|
42
|
+
xsec_source=xsec_source,
|
|
43
|
+
account_uid=account_uid,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
async def get_note_info(
|
|
47
|
+
self,
|
|
48
|
+
note_id: str,
|
|
49
|
+
xsec_token: str | None = None,
|
|
50
|
+
xsec_source: str | None = "pc_feed",
|
|
51
|
+
comment_size: int = 10,
|
|
52
|
+
sub_comment_size: int = 5,
|
|
53
|
+
account_uid: str | None = None,
|
|
54
|
+
) -> dict:
|
|
55
|
+
return await scraper_service.get_note_info(
|
|
56
|
+
note_id=note_id,
|
|
57
|
+
xsec_token=xsec_token,
|
|
58
|
+
xsec_source=xsec_source,
|
|
59
|
+
comment_size=comment_size,
|
|
60
|
+
sub_comment_size=sub_comment_size,
|
|
61
|
+
account_uid=account_uid,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
async def get_self_info(self, account_uid: str | None = None) -> dict:
|
|
65
|
+
return await scraper_service.get_self_info(account_uid=account_uid)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from rednote_cli._runtime.services import scraper_service
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RednotePublisherAdapter:
|
|
7
|
+
async def publish_note(
|
|
8
|
+
self,
|
|
9
|
+
*,
|
|
10
|
+
target: str,
|
|
11
|
+
image_list: list[str],
|
|
12
|
+
title: str = "",
|
|
13
|
+
content: str = "",
|
|
14
|
+
tags: list[str] | None = None,
|
|
15
|
+
schedule_at: str | None = None,
|
|
16
|
+
account_uid: str | None = None,
|
|
17
|
+
) -> dict:
|
|
18
|
+
return await scraper_service.publish_note(
|
|
19
|
+
target=target,
|
|
20
|
+
image_list=image_list,
|
|
21
|
+
title=title,
|
|
22
|
+
content=content,
|
|
23
|
+
tags=tags,
|
|
24
|
+
schedule_at=schedule_at,
|
|
25
|
+
account_uid=account_uid,
|
|
26
|
+
)
|