cli-web-reddit 0.1.0__tar.gz
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.
- cli_web_reddit-0.1.0/PKG-INFO +15 -0
- cli_web_reddit-0.1.0/cli_web/reddit/README.md +68 -0
- cli_web_reddit-0.1.0/cli_web/reddit/__init__.py +3 -0
- cli_web_reddit-0.1.0/cli_web/reddit/__main__.py +6 -0
- cli_web_reddit-0.1.0/cli_web/reddit/commands/__init__.py +0 -0
- cli_web_reddit-0.1.0/cli_web/reddit/commands/actions.py +268 -0
- cli_web_reddit-0.1.0/cli_web/reddit/commands/auth_cmd.py +73 -0
- cli_web_reddit-0.1.0/cli_web/reddit/commands/feed.py +115 -0
- cli_web_reddit-0.1.0/cli_web/reddit/commands/me.py +139 -0
- cli_web_reddit-0.1.0/cli_web/reddit/commands/post.py +93 -0
- cli_web_reddit-0.1.0/cli_web/reddit/commands/search.py +66 -0
- cli_web_reddit-0.1.0/cli_web/reddit/commands/subreddit.py +184 -0
- cli_web_reddit-0.1.0/cli_web/reddit/commands/user.py +90 -0
- cli_web_reddit-0.1.0/cli_web/reddit/core/__init__.py +0 -0
- cli_web_reddit-0.1.0/cli_web/reddit/core/auth.py +204 -0
- cli_web_reddit-0.1.0/cli_web/reddit/core/client.py +475 -0
- cli_web_reddit-0.1.0/cli_web/reddit/core/exceptions.py +63 -0
- cli_web_reddit-0.1.0/cli_web/reddit/core/models.py +253 -0
- cli_web_reddit-0.1.0/cli_web/reddit/reddit_cli.py +174 -0
- cli_web_reddit-0.1.0/cli_web/reddit/skills/SKILL.md +143 -0
- cli_web_reddit-0.1.0/cli_web/reddit/tests/TEST.md +109 -0
- cli_web_reddit-0.1.0/cli_web/reddit/tests/__init__.py +0 -0
- cli_web_reddit-0.1.0/cli_web/reddit/tests/conftest.py +9 -0
- cli_web_reddit-0.1.0/cli_web/reddit/tests/test_core.py +568 -0
- cli_web_reddit-0.1.0/cli_web/reddit/tests/test_e2e.py +312 -0
- cli_web_reddit-0.1.0/cli_web/reddit/utils/__init__.py +0 -0
- cli_web_reddit-0.1.0/cli_web/reddit/utils/doctor.py +188 -0
- cli_web_reddit-0.1.0/cli_web/reddit/utils/helpers.py +91 -0
- cli_web_reddit-0.1.0/cli_web/reddit/utils/mcp_server.py +290 -0
- cli_web_reddit-0.1.0/cli_web/reddit/utils/output.py +133 -0
- cli_web_reddit-0.1.0/cli_web/reddit/utils/repl_skin.py +486 -0
- cli_web_reddit-0.1.0/cli_web_reddit.egg-info/PKG-INFO +15 -0
- cli_web_reddit-0.1.0/cli_web_reddit.egg-info/SOURCES.txt +37 -0
- cli_web_reddit-0.1.0/cli_web_reddit.egg-info/dependency_links.txt +1 -0
- cli_web_reddit-0.1.0/cli_web_reddit.egg-info/entry_points.txt +2 -0
- cli_web_reddit-0.1.0/cli_web_reddit.egg-info/requires.txt +7 -0
- cli_web_reddit-0.1.0/cli_web_reddit.egg-info/top_level.txt +1 -0
- cli_web_reddit-0.1.0/setup.cfg +4 -0
- cli_web_reddit-0.1.0/setup.py +28 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cli-web-reddit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI for Reddit browsing, search, and interaction
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: click>=8.0
|
|
7
|
+
Requires-Dist: curl_cffi
|
|
8
|
+
Requires-Dist: rich>=13.0
|
|
9
|
+
Requires-Dist: prompt_toolkit>=3.0
|
|
10
|
+
Provides-Extra: browser
|
|
11
|
+
Requires-Dist: playwright>=1.40.0; extra == "browser"
|
|
12
|
+
Dynamic: provides-extra
|
|
13
|
+
Dynamic: requires-dist
|
|
14
|
+
Dynamic: requires-python
|
|
15
|
+
Dynamic: summary
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# cli-web-reddit
|
|
2
|
+
|
|
3
|
+
CLI for Reddit browsing and search. Browse feeds, subreddits, search posts, view user profiles, and read comments — all from the command line.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd reddit/agent-harness
|
|
9
|
+
pip install -e .
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Quick Start
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# Hot posts on the front page
|
|
16
|
+
cli-web-reddit feed hot --limit 10
|
|
17
|
+
|
|
18
|
+
# Top posts this week
|
|
19
|
+
cli-web-reddit feed top --time week --json
|
|
20
|
+
|
|
21
|
+
# Browse a subreddit
|
|
22
|
+
cli-web-reddit sub hot python --limit 10
|
|
23
|
+
|
|
24
|
+
# Subreddit info
|
|
25
|
+
cli-web-reddit sub info programming --json
|
|
26
|
+
|
|
27
|
+
# Search posts
|
|
28
|
+
cli-web-reddit search posts "machine learning" --sort top --json
|
|
29
|
+
|
|
30
|
+
# Search subreddits
|
|
31
|
+
cli-web-reddit search subs "python" --json
|
|
32
|
+
|
|
33
|
+
# User profile
|
|
34
|
+
cli-web-reddit user info spez --json
|
|
35
|
+
|
|
36
|
+
# Post detail with comments (auto-expands deeply nested threads)
|
|
37
|
+
cli-web-reddit post get https://www.reddit.com/r/python/comments/abc123/my_post/ --json
|
|
38
|
+
|
|
39
|
+
# Interactive REPL mode
|
|
40
|
+
cli-web-reddit
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Commands
|
|
44
|
+
|
|
45
|
+
| Group | Command | Description |
|
|
46
|
+
|-------|---------|-------------|
|
|
47
|
+
| `feed` | `hot`, `new`, `top`, `rising`, `popular` | Global Reddit feeds |
|
|
48
|
+
| `sub` | `hot`, `new`, `top`, `info`, `rules`, `search` | Subreddit operations |
|
|
49
|
+
| `search` | `posts`, `subs` | Search posts and subreddits |
|
|
50
|
+
| `user` | `info`, `posts`, `comments` | User profiles and activity |
|
|
51
|
+
| `post` | `get` | Post detail with comments (auto-expands deep threads) |
|
|
52
|
+
| `vote` | `up`, `down`, `unvote` | Vote on posts/comments (auth required) |
|
|
53
|
+
| `submit` | `text`, `link` | Submit new posts (auth required) |
|
|
54
|
+
| `comment` | `add`, `edit`, `delete` | Comment on posts (auth required) |
|
|
55
|
+
| `saved` | `save`, `unsave` | Save/unsave items (auth required) |
|
|
56
|
+
| `me` | `profile`, `saved`, `upvoted`, `subscriptions`, `inbox` | Authenticated user data (auth required) |
|
|
57
|
+
| `auth` | `login`, `logout`, `status` | OAuth authentication management |
|
|
58
|
+
|
|
59
|
+
All commands support `--json` for structured output and `--limit N` for pagination.
|
|
60
|
+
|
|
61
|
+
## Notes
|
|
62
|
+
|
|
63
|
+
- **Reading is public** — feeds, subreddits, search, and user profiles work without auth
|
|
64
|
+
- **Writing requires auth** — vote, comment, submit, save, and inbox commands need OAuth login (`cli-web-reddit auth login`)
|
|
65
|
+
- **Token auto-refresh** — the `token_v2` session cookie expires every ~15-30 min, but the CLI silently refreshes it using a headless browser with the saved profile. No manual re-login needed.
|
|
66
|
+
- Uses `curl_cffi` with Chrome impersonation (Reddit blocks plain HTTP clients)
|
|
67
|
+
- Pagination via `--after` cursor (shown in output)
|
|
68
|
+
- Time filters: `hour`, `day`, `week`, `month`, `year`, `all`
|
|
File without changes
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""Action commands for cli-web-reddit — vote, save, hide, comment, submit, edit, delete."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from ..core.client import RedditClient
|
|
8
|
+
from ..core.exceptions import SubmitError
|
|
9
|
+
from ..utils.helpers import handle_errors, print_json, resolve_json_mode
|
|
10
|
+
|
|
11
|
+
# ── Vote ──────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.group("vote")
|
|
15
|
+
def vote():
|
|
16
|
+
"""Vote on posts and comments (requires login)."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@vote.command("up")
|
|
20
|
+
@click.argument("thing_id")
|
|
21
|
+
@click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
|
|
22
|
+
def up(thing_id, use_json):
|
|
23
|
+
"""Upvote a post or comment. Pass the fullname (t3_xxx or t1_xxx)."""
|
|
24
|
+
use_json = resolve_json_mode(use_json)
|
|
25
|
+
with handle_errors(json_mode=use_json):
|
|
26
|
+
client = RedditClient()
|
|
27
|
+
client.vote(thing_id, 1)
|
|
28
|
+
if use_json:
|
|
29
|
+
print_json({"success": True, "action": "upvote", "id": thing_id})
|
|
30
|
+
else:
|
|
31
|
+
click.echo(f" Upvoted {thing_id}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@vote.command("down")
|
|
35
|
+
@click.argument("thing_id")
|
|
36
|
+
@click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
|
|
37
|
+
def down(thing_id, use_json):
|
|
38
|
+
"""Downvote a post or comment."""
|
|
39
|
+
use_json = resolve_json_mode(use_json)
|
|
40
|
+
with handle_errors(json_mode=use_json):
|
|
41
|
+
client = RedditClient()
|
|
42
|
+
client.vote(thing_id, -1)
|
|
43
|
+
if use_json:
|
|
44
|
+
print_json({"success": True, "action": "downvote", "id": thing_id})
|
|
45
|
+
else:
|
|
46
|
+
click.echo(f" Downvoted {thing_id}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@vote.command("unvote")
|
|
50
|
+
@click.argument("thing_id")
|
|
51
|
+
@click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
|
|
52
|
+
def unvote(thing_id, use_json):
|
|
53
|
+
"""Remove vote from a post or comment."""
|
|
54
|
+
use_json = resolve_json_mode(use_json)
|
|
55
|
+
with handle_errors(json_mode=use_json):
|
|
56
|
+
client = RedditClient()
|
|
57
|
+
client.vote(thing_id, 0)
|
|
58
|
+
if use_json:
|
|
59
|
+
print_json({"success": True, "action": "unvote", "id": thing_id})
|
|
60
|
+
else:
|
|
61
|
+
click.echo(f" Removed vote on {thing_id}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ── Submit ────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@click.group("submit")
|
|
68
|
+
def submit():
|
|
69
|
+
"""Submit posts to subreddits (requires login)."""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@submit.command("flairs")
|
|
73
|
+
@click.argument("subreddit")
|
|
74
|
+
@click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
|
|
75
|
+
def submit_flairs(subreddit, use_json):
|
|
76
|
+
"""List available post flairs for a subreddit.
|
|
77
|
+
|
|
78
|
+
Example: submit flairs ClaudeCode
|
|
79
|
+
"""
|
|
80
|
+
use_json = resolve_json_mode(use_json)
|
|
81
|
+
with handle_errors(json_mode=use_json):
|
|
82
|
+
client = RedditClient()
|
|
83
|
+
flairs = client.get_subreddit_flairs(subreddit)
|
|
84
|
+
if use_json:
|
|
85
|
+
print_json({"success": True, "subreddit": subreddit, "flairs": flairs})
|
|
86
|
+
else:
|
|
87
|
+
if not flairs:
|
|
88
|
+
click.echo(f" No flairs available for r/{subreddit}")
|
|
89
|
+
else:
|
|
90
|
+
click.echo(f" Flairs for r/{subreddit}:")
|
|
91
|
+
for f in flairs:
|
|
92
|
+
click.echo(f" {f['id']} {f['text']}")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@submit.command("text")
|
|
96
|
+
@click.argument("subreddit")
|
|
97
|
+
@click.argument("title")
|
|
98
|
+
@click.argument("body")
|
|
99
|
+
@click.option(
|
|
100
|
+
"--flair", "flair_id", default=None, help="Flair ID (use 'submit flairs <sub>' to list)."
|
|
101
|
+
)
|
|
102
|
+
@click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
|
|
103
|
+
def submit_text(subreddit, title, body, flair_id, use_json):
|
|
104
|
+
"""Submit a text (self) post.
|
|
105
|
+
|
|
106
|
+
Example: submit text python "My Title" "Post body here"
|
|
107
|
+
Example with flair: submit text ClaudeCode "Title" "Body" --flair abc123
|
|
108
|
+
"""
|
|
109
|
+
use_json = resolve_json_mode(use_json)
|
|
110
|
+
# Decode escape sequences so \n from CLI becomes actual newlines
|
|
111
|
+
body = body.encode("utf-8").decode("unicode_escape") if "\\n" in body else body
|
|
112
|
+
with handle_errors(json_mode=use_json):
|
|
113
|
+
client = RedditClient()
|
|
114
|
+
result = client.submit_text(subreddit, title, body, flair_id=flair_id)
|
|
115
|
+
data = result.get("json", {}).get("data", {})
|
|
116
|
+
errors = result.get("json", {}).get("errors", [])
|
|
117
|
+
if errors:
|
|
118
|
+
msg = "; ".join(str(e) for e in errors)
|
|
119
|
+
raise SubmitError(f"Submit failed: {msg}")
|
|
120
|
+
if use_json:
|
|
121
|
+
print_json(
|
|
122
|
+
{
|
|
123
|
+
"success": True,
|
|
124
|
+
"id": data.get("name", ""),
|
|
125
|
+
"url": data.get("url", ""),
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
else:
|
|
129
|
+
click.echo(f" Posted to r/{subreddit}: {data.get('url', '')}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@submit.command("link")
|
|
133
|
+
@click.argument("subreddit")
|
|
134
|
+
@click.argument("title")
|
|
135
|
+
@click.argument("url")
|
|
136
|
+
@click.option(
|
|
137
|
+
"--flair", "flair_id", default=None, help="Flair ID (use 'submit flairs <sub>' to list)."
|
|
138
|
+
)
|
|
139
|
+
@click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
|
|
140
|
+
def submit_link(subreddit, title, url, flair_id, use_json):
|
|
141
|
+
"""Submit a link post.
|
|
142
|
+
|
|
143
|
+
Example: submit link python "Check this out" "https://example.com"
|
|
144
|
+
Example with flair: submit link ClaudeCode "Title" "https://..." --flair abc123
|
|
145
|
+
"""
|
|
146
|
+
use_json = resolve_json_mode(use_json)
|
|
147
|
+
with handle_errors(json_mode=use_json):
|
|
148
|
+
client = RedditClient()
|
|
149
|
+
result = client.submit_link(subreddit, title, url, flair_id=flair_id)
|
|
150
|
+
data = result.get("json", {}).get("data", {})
|
|
151
|
+
errors = result.get("json", {}).get("errors", [])
|
|
152
|
+
if errors:
|
|
153
|
+
msg = "; ".join(str(e) for e in errors)
|
|
154
|
+
raise SubmitError(f"Submit failed: {msg}")
|
|
155
|
+
if use_json:
|
|
156
|
+
print_json(
|
|
157
|
+
{
|
|
158
|
+
"success": True,
|
|
159
|
+
"id": data.get("name", ""),
|
|
160
|
+
"url": data.get("url", ""),
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
else:
|
|
164
|
+
click.echo(f" Posted to r/{subreddit}: {data.get('url', '')}")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ── Comment ───────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@click.group("comment")
|
|
171
|
+
def comment_group():
|
|
172
|
+
"""Comment on posts and reply to comments (requires login)."""
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@comment_group.command("add")
|
|
176
|
+
@click.argument("thing_id")
|
|
177
|
+
@click.argument("text")
|
|
178
|
+
@click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
|
|
179
|
+
def add_comment(thing_id, text, use_json):
|
|
180
|
+
"""Add a comment to a post or reply to a comment.
|
|
181
|
+
|
|
182
|
+
thing_id: fullname of the post (t3_xxx) or comment (t1_xxx) to reply to.
|
|
183
|
+
"""
|
|
184
|
+
use_json = resolve_json_mode(use_json)
|
|
185
|
+
# Decode escape sequences so \n from CLI becomes actual newlines
|
|
186
|
+
text = text.encode("utf-8").decode("unicode_escape") if "\\n" in text else text
|
|
187
|
+
with handle_errors(json_mode=use_json):
|
|
188
|
+
client = RedditClient()
|
|
189
|
+
result = client.comment(thing_id, text)
|
|
190
|
+
things = result.get("json", {}).get("data", {}).get("things", [])
|
|
191
|
+
comment_id = things[0].get("data", {}).get("name", "") if things else ""
|
|
192
|
+
errors = result.get("json", {}).get("errors", [])
|
|
193
|
+
if errors:
|
|
194
|
+
msg = "; ".join(str(e) for e in errors)
|
|
195
|
+
raise SubmitError(f"Comment failed: {msg}")
|
|
196
|
+
if use_json:
|
|
197
|
+
print_json({"success": True, "comment_id": comment_id})
|
|
198
|
+
else:
|
|
199
|
+
click.echo(f" Comment posted: {comment_id}")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@comment_group.command("edit")
|
|
203
|
+
@click.argument("thing_id")
|
|
204
|
+
@click.argument("text")
|
|
205
|
+
@click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
|
|
206
|
+
def edit_comment(thing_id, text, use_json):
|
|
207
|
+
"""Edit your own post or comment text."""
|
|
208
|
+
use_json = resolve_json_mode(use_json)
|
|
209
|
+
with handle_errors(json_mode=use_json):
|
|
210
|
+
client = RedditClient()
|
|
211
|
+
client.edit(thing_id, text)
|
|
212
|
+
if use_json:
|
|
213
|
+
print_json({"success": True, "action": "edit", "id": thing_id})
|
|
214
|
+
else:
|
|
215
|
+
click.echo(f" Edited {thing_id}")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@comment_group.command("delete")
|
|
219
|
+
@click.argument("thing_id")
|
|
220
|
+
@click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
|
|
221
|
+
def delete_thing(thing_id, use_json):
|
|
222
|
+
"""Delete your own post or comment."""
|
|
223
|
+
use_json = resolve_json_mode(use_json)
|
|
224
|
+
with handle_errors(json_mode=use_json):
|
|
225
|
+
client = RedditClient()
|
|
226
|
+
client.delete(thing_id)
|
|
227
|
+
if use_json:
|
|
228
|
+
print_json({"success": True, "action": "delete", "id": thing_id})
|
|
229
|
+
else:
|
|
230
|
+
click.echo(f" Deleted {thing_id}")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# ── Save ──────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@click.group("saved")
|
|
237
|
+
def saved_group():
|
|
238
|
+
"""Save and unsave posts (requires login)."""
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@saved_group.command("save")
|
|
242
|
+
@click.argument("thing_id")
|
|
243
|
+
@click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
|
|
244
|
+
def save_thing(thing_id, use_json):
|
|
245
|
+
"""Save a post or comment."""
|
|
246
|
+
use_json = resolve_json_mode(use_json)
|
|
247
|
+
with handle_errors(json_mode=use_json):
|
|
248
|
+
client = RedditClient()
|
|
249
|
+
client.save(thing_id)
|
|
250
|
+
if use_json:
|
|
251
|
+
print_json({"success": True, "action": "save", "id": thing_id})
|
|
252
|
+
else:
|
|
253
|
+
click.echo(f" Saved {thing_id}")
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@saved_group.command("unsave")
|
|
257
|
+
@click.argument("thing_id")
|
|
258
|
+
@click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
|
|
259
|
+
def unsave_thing(thing_id, use_json):
|
|
260
|
+
"""Unsave a post or comment."""
|
|
261
|
+
use_json = resolve_json_mode(use_json)
|
|
262
|
+
with handle_errors(json_mode=use_json):
|
|
263
|
+
client = RedditClient()
|
|
264
|
+
client.unsave(thing_id)
|
|
265
|
+
if use_json:
|
|
266
|
+
print_json({"success": True, "action": "unsave", "id": thing_id})
|
|
267
|
+
else:
|
|
268
|
+
click.echo(f" Unsaved {thing_id}")
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Auth commands for cli-web-reddit."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from ..core.auth import clear_auth, load_auth, login_browser
|
|
8
|
+
from ..core.client import RedditClient
|
|
9
|
+
from ..utils.helpers import handle_errors, print_json, resolve_json_mode
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.group("auth")
|
|
13
|
+
def auth():
|
|
14
|
+
"""Login, logout, and check authentication status."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@auth.command("login")
|
|
18
|
+
@click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
|
|
19
|
+
def login(use_json):
|
|
20
|
+
"""Login to Reddit via browser (opens Playwright)."""
|
|
21
|
+
use_json = resolve_json_mode(use_json)
|
|
22
|
+
with handle_errors(json_mode=use_json):
|
|
23
|
+
login_browser()
|
|
24
|
+
if use_json:
|
|
25
|
+
print_json({"success": True, "message": "Logged in successfully"})
|
|
26
|
+
else:
|
|
27
|
+
click.echo(" Logged in successfully. Token saved.")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@auth.command("logout")
|
|
31
|
+
@click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
|
|
32
|
+
def logout(use_json):
|
|
33
|
+
"""Remove saved authentication."""
|
|
34
|
+
use_json = resolve_json_mode(use_json)
|
|
35
|
+
with handle_errors(json_mode=use_json):
|
|
36
|
+
clear_auth()
|
|
37
|
+
if use_json:
|
|
38
|
+
print_json({"success": True, "message": "Logged out"})
|
|
39
|
+
else:
|
|
40
|
+
click.echo(" Logged out. Auth data removed.")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@auth.command("status")
|
|
44
|
+
@click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
|
|
45
|
+
def status(use_json):
|
|
46
|
+
"""Check current authentication status."""
|
|
47
|
+
use_json = resolve_json_mode(use_json)
|
|
48
|
+
with handle_errors(json_mode=use_json):
|
|
49
|
+
auth_data = load_auth()
|
|
50
|
+
if not auth_data or not auth_data.get("token"):
|
|
51
|
+
if use_json:
|
|
52
|
+
print_json({"authenticated": False, "message": "Not logged in"})
|
|
53
|
+
else:
|
|
54
|
+
click.echo(" Not logged in. Run: cli-web-reddit auth login")
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
# Verify token works by calling /api/v1/me
|
|
58
|
+
client = RedditClient()
|
|
59
|
+
me = client.me()
|
|
60
|
+
username = me.get("name", "unknown")
|
|
61
|
+
karma = me.get("total_karma", 0)
|
|
62
|
+
|
|
63
|
+
if use_json:
|
|
64
|
+
print_json(
|
|
65
|
+
{
|
|
66
|
+
"authenticated": True,
|
|
67
|
+
"username": username,
|
|
68
|
+
"total_karma": karma,
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
else:
|
|
72
|
+
click.echo(f" Logged in as: u/{username}")
|
|
73
|
+
click.echo(f" Total karma: {karma:,}")
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Feed commands for cli-web-reddit — global feeds (hot, new, top, rising, popular)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from ..core.client import RedditClient
|
|
8
|
+
from ..core.models import extract_listing_posts
|
|
9
|
+
from ..utils.helpers import handle_errors, print_json, resolve_json_mode
|
|
10
|
+
from ..utils.output import post_table
|
|
11
|
+
|
|
12
|
+
TIME_CHOICES = ["hour", "day", "week", "month", "year", "all"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.group("feed")
|
|
16
|
+
def feed():
|
|
17
|
+
"""Browse global Reddit feeds."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@feed.command("hot")
|
|
21
|
+
@click.option("--limit", type=int, default=25, help="Number of posts (max 100).")
|
|
22
|
+
@click.option("--after", default=None, help="Pagination cursor.")
|
|
23
|
+
@click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
|
|
24
|
+
def hot(limit, after, use_json):
|
|
25
|
+
"""Hot posts from the front page."""
|
|
26
|
+
use_json = resolve_json_mode(use_json)
|
|
27
|
+
with handle_errors(json_mode=use_json):
|
|
28
|
+
client = RedditClient()
|
|
29
|
+
data = client.feed_hot(limit=limit, after=after)
|
|
30
|
+
posts, next_after = extract_listing_posts(data)
|
|
31
|
+
if use_json:
|
|
32
|
+
print_json({"posts": posts, "after": next_after})
|
|
33
|
+
else:
|
|
34
|
+
post_table(posts, title="Hot Posts")
|
|
35
|
+
if next_after:
|
|
36
|
+
click.echo(f" Next page: feed hot --after {next_after}")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@feed.command("new")
|
|
40
|
+
@click.option("--limit", type=int, default=25, help="Number of posts (max 100).")
|
|
41
|
+
@click.option("--after", default=None, help="Pagination cursor.")
|
|
42
|
+
@click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
|
|
43
|
+
def new(limit, after, use_json):
|
|
44
|
+
"""Newest posts from the front page."""
|
|
45
|
+
use_json = resolve_json_mode(use_json)
|
|
46
|
+
with handle_errors(json_mode=use_json):
|
|
47
|
+
client = RedditClient()
|
|
48
|
+
data = client.feed_new(limit=limit, after=after)
|
|
49
|
+
posts, next_after = extract_listing_posts(data)
|
|
50
|
+
if use_json:
|
|
51
|
+
print_json({"posts": posts, "after": next_after})
|
|
52
|
+
else:
|
|
53
|
+
post_table(posts, title="New Posts")
|
|
54
|
+
if next_after:
|
|
55
|
+
click.echo(f" Next page: feed new --after {next_after}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@feed.command("top")
|
|
59
|
+
@click.option(
|
|
60
|
+
"--time", "time_filter", type=click.Choice(TIME_CHOICES), default="day", help="Time period."
|
|
61
|
+
)
|
|
62
|
+
@click.option("--limit", type=int, default=25, help="Number of posts (max 100).")
|
|
63
|
+
@click.option("--after", default=None, help="Pagination cursor.")
|
|
64
|
+
@click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
|
|
65
|
+
def top(time_filter, limit, after, use_json):
|
|
66
|
+
"""Top posts by time period."""
|
|
67
|
+
use_json = resolve_json_mode(use_json)
|
|
68
|
+
with handle_errors(json_mode=use_json):
|
|
69
|
+
client = RedditClient()
|
|
70
|
+
data = client.feed_top(limit=limit, after=after, time=time_filter)
|
|
71
|
+
posts, next_after = extract_listing_posts(data)
|
|
72
|
+
if use_json:
|
|
73
|
+
print_json({"posts": posts, "after": next_after})
|
|
74
|
+
else:
|
|
75
|
+
post_table(posts, title=f"Top Posts ({time_filter})")
|
|
76
|
+
if next_after:
|
|
77
|
+
click.echo(f" Next page: feed top --time {time_filter} --after {next_after}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@feed.command("rising")
|
|
81
|
+
@click.option("--limit", type=int, default=25, help="Number of posts (max 100).")
|
|
82
|
+
@click.option("--after", default=None, help="Pagination cursor.")
|
|
83
|
+
@click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
|
|
84
|
+
def rising(limit, after, use_json):
|
|
85
|
+
"""Rising posts."""
|
|
86
|
+
use_json = resolve_json_mode(use_json)
|
|
87
|
+
with handle_errors(json_mode=use_json):
|
|
88
|
+
client = RedditClient()
|
|
89
|
+
data = client.feed_rising(limit=limit, after=after)
|
|
90
|
+
posts, next_after = extract_listing_posts(data)
|
|
91
|
+
if use_json:
|
|
92
|
+
print_json({"posts": posts, "after": next_after})
|
|
93
|
+
else:
|
|
94
|
+
post_table(posts, title="Rising Posts")
|
|
95
|
+
if next_after:
|
|
96
|
+
click.echo(f" Next page: feed rising --after {next_after}")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@feed.command("popular")
|
|
100
|
+
@click.option("--limit", type=int, default=25, help="Number of posts (max 100).")
|
|
101
|
+
@click.option("--after", default=None, help="Pagination cursor.")
|
|
102
|
+
@click.option("--json", "use_json", is_flag=True, help="Output as JSON.")
|
|
103
|
+
def popular(limit, after, use_json):
|
|
104
|
+
"""Popular posts from r/popular."""
|
|
105
|
+
use_json = resolve_json_mode(use_json)
|
|
106
|
+
with handle_errors(json_mode=use_json):
|
|
107
|
+
client = RedditClient()
|
|
108
|
+
data = client.feed_popular(limit=limit, after=after)
|
|
109
|
+
posts, next_after = extract_listing_posts(data)
|
|
110
|
+
if use_json:
|
|
111
|
+
print_json({"posts": posts, "after": next_after})
|
|
112
|
+
else:
|
|
113
|
+
post_table(posts, title="Popular Posts")
|
|
114
|
+
if next_after:
|
|
115
|
+
click.echo(f" Next page: feed popular --after {next_after}")
|