Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ rdt user-posts spez # User's submitted posts
rdt user-comments spez # User's comments
rdt saved # Your saved posts/items
rdt upvoted # Your upvoted posts
rdt custom-feeds # List your custom feeds (multis)
rdt custom-feed dev -s hot # Browse posts from custom feed

# Short index works after list commands (feed/popular/sub/search)
rdt sub python
Expand Down Expand Up @@ -120,6 +122,14 @@ rdt save 3 --undo # Unsave
rdt subscribe python # Subscribe to r/python
rdt subscribe python --undo # Unsubscribe
rdt comment 3 "Great post!" # Comment on result #3

# ─── Custom Feeds (require login) ─────────────
rdt custom-feeds # List your custom feeds
rdt custom-feed dev -s hot # Browse posts in custom feed
rdt custom-feed-add dev golang # Add r/golang to custom feed
rdt custom-feed-remove dev golang # Remove r/golang from custom feed
rdt custom-feed-create tech -s rust # Create custom feed
rdt custom-feed-delete tech -y # Delete custom feed
```

## Authentication
Expand Down
13 changes: 12 additions & 1 deletion SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ Payloads live under `.data`.
| `rdt user-comments <name>` | View user's comments | `rdt user-comments spez -n 5 --json` |
| `rdt saved` | View your saved items | `rdt saved -n 10 --json` |
| `rdt upvoted` | View your upvoted posts | `rdt upvoted -n 10 --json` |
| `rdt custom-feeds` | List your custom feeds (multis) | `rdt custom-feeds --json` |
| `rdt custom-feed <name>` | Browse posts from custom feed | `rdt custom-feed dev -s hot -n 10` |
| `rdt open <id_or_index>` | Open post in browser | `rdt open 3` |

### Reading
Expand Down Expand Up @@ -124,7 +126,16 @@ Payloads live under `.data`.
| `rdt save <id> --undo` | Unsave | `rdt save 3 --undo` |
| `rdt subscribe <sub>` | Subscribe | `rdt subscribe python` |
| `rdt subscribe <sub> --undo` | Unsubscribe | `rdt subscribe python --undo` |
| `rdt comment <id> <text>` | Post a comment | `rdt comment 3 "Great post!"` |
### Custom Feeds (require auth)

| Command | Description | Example |
|---------|-------------|---------|
| `rdt custom-feeds` | List your custom feeds (multis) | `rdt custom-feeds --json` |
| `rdt custom-feed <name>` | Browse posts from custom feed | `rdt custom-feed dev -s hot -n 10` |
| `rdt custom-feed-add <name> <sub>` | Add subreddit to custom feed | `rdt custom-feed-add dev golang` |
| `rdt custom-feed-remove <name> <sub>` | Remove subreddit from custom feed | `rdt custom-feed-remove dev golang` |
| `rdt custom-feed-create <name>` | Create a new custom feed | `rdt custom-feed-create tech -s python,rust` |
| `rdt custom-feed-delete <name>` | Delete a custom feed | `rdt custom-feed-delete tech -y` |

### Account

Expand Down
6 changes: 6 additions & 0 deletions rdt_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ def cli(ctx: click.Context, verbose: bool) -> None:
cli.add_command(browse.user_comments)
cli.add_command(browse.saved)
cli.add_command(browse.upvoted)
cli.add_command(browse.custom_feeds)
cli.add_command(browse.custom_feed)
cli.add_command(browse.custom_feed_add)
cli.add_command(browse.custom_feed_remove)
cli.add_command(browse.custom_feed_create)
cli.add_command(browse.custom_feed_delete)
cli.add_command(browse.open_post)

# ─── Post commands ───────────────────────────────────────────────────
Expand Down
86 changes: 86 additions & 0 deletions rdt_cli/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import json
import logging
from typing import Any

Expand Down Expand Up @@ -133,6 +134,14 @@ def _post(self, url: str, data: dict[str, Any] | None = None) -> Any:
"""POST request."""
return self._write_request("POST", url, data=data)

def _put(self, url: str, data: dict[str, Any] | None = None) -> Any:
"""PUT request."""
return self._write_request("PUT", url, data=data)

def _delete(self, url: str, data: dict[str, Any] | None = None, params: dict[str, Any] | None = None) -> Any:
"""DELETE request."""
return self._write_request("DELETE", url, data=data, params=params)

# ── Listing helpers ─────────────────────────────────────────────

@staticmethod
Expand Down Expand Up @@ -417,3 +426,80 @@ def get_subs_only_feed(
)

return {"data": {"children": all_posts, "after": None}}

# ── Custom feeds (multireddits) ─────────────────────────────────

def get_custom_feeds(self) -> list[dict]:
"""Get user's custom feeds (multireddits)."""
data = self._get("/api/multi/mine.json", params={"raw_json": 1})
if isinstance(data, list):
return data
return []

def get_custom_feed(
self,
feed_name: str,
username: str | None = None,
sort: str = "hot",
limit: int = DEFAULT_LIMIT,
after: str | None = None,
time_filter: str | None = None,
) -> dict:
"""Get listing for a custom feed (multireddit)."""
user = username or self.session.username or "me"
url = (
f"/user/{user}/m/{feed_name}.json"
if sort == "hot"
else f"/user/{user}/m/{feed_name}/{sort}.json"
)
params: dict[str, Any] = {"limit": limit, "raw_json": 1}
if after:
params["after"] = after
if time_filter and sort in ("top", "controversial"):
params["t"] = time_filter
return self._get(url, params=params)

def add_to_custom_feed(
self, feed_name: str, subreddit: str, username: str | None = None,
) -> dict:
"""Add a subreddit to a custom feed (multireddit)."""
user = username or self.session.username or "me"
url = f"/api/multi/user/{user}/m/{feed_name}/r/{subreddit}"
return self._put(url, data={"model": json.dumps({"name": subreddit})})

def remove_from_custom_feed(
self, feed_name: str, subreddit: str, username: str | None = None,
) -> dict:
"""Remove a subreddit from a custom feed (multireddit)."""
user = username or self.session.username or "me"
url = f"/api/multi/user/{user}/m/{feed_name}/r/{subreddit}"
return self._delete(url)

def create_custom_feed(
self,
feed_name: str,
display_name: str | None = None,
description: str = "",
visibility: str = "private",
subreddits: list[str] | None = None,
username: str | None = None,
) -> dict:
"""Create or update a custom feed (multireddit)."""
user = username or self.session.username or "me"
url = f"/api/multi/user/{user}/m/{feed_name}"
sub_list = [{"name": s} for s in (subreddits or [])]
model = {
"display_name": display_name or feed_name,
"description_md": description,
"visibility": visibility,
"subreddits": sub_list,
}
return self._post(url, data={"model": json.dumps(model)})

def delete_custom_feed(
self, feed_name: str, username: str | None = None,
) -> dict:
"""Delete an entire custom feed (multireddit)."""
user = username or self.session.username or "me"
url = f"/api/multi/user/{user}/m/{feed_name}"
return self._delete(url)
167 changes: 167 additions & 0 deletions rdt_cli/commands/browse.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,173 @@ def upvoted(
)


# ── custom-feeds / custom-feed ──────────────────────────────────────


@click.command(name="custom-feeds")
@structured_output_options
def custom_feeds(as_json: bool, as_yaml: bool) -> None:
"""List your custom feeds (multireddits)"""
from ._common import exit_for_error, run_client_action

cred = require_auth()

def _render(raw_data: list) -> None:
table = Table(title="πŸ“‘ Custom Feeds", show_lines=True)
table.add_column("Name", style="bold cyan")
table.add_column("Display Name", style="green")
table.add_column("Subreddits", style="dim")
table.add_column("Path", style="dim")

for item in raw_data:
d = item.get("data", {})
name = d.get("name", "")
display_name = d.get("display_name", "")
subs = [s.get("name", "") for s in d.get("subreddits", [])]
sub_str = f"{len(subs)} subs ({', '.join(subs[:5])}{'...' if len(subs) > 5 else ''})"
path = d.get("path", "")
table.add_row(name, display_name, sub_str, path)

console.print(table)
console.print("\n [dim]πŸ’‘ Use [bold]rdt custom-feed <name>[/bold] to browse a custom feed[/dim]")

try:
raw_list = run_client_action(cred, lambda c: c.get_custom_feeds())
clean_data = [
{
"name": it.get("data", {}).get("name"),
"display_name": it.get("data", {}).get("display_name"),
"path": it.get("data", {}).get("path"),
"subreddits": [s.get("name") for s in it.get("data", {}).get("subreddits", [])],
}
for it in raw_list
]
if maybe_print_structured(clean_data, as_json=as_json, as_yaml=as_yaml):
return
_render(raw_list)
except Exception as exc:
exit_for_error(exc, as_json=as_json, as_yaml=as_yaml)


@click.command(name="custom-feed")
@click.argument("name")
@click.option("-s", "--sort", default="hot", type=click.Choice(SORT_OPTIONS), help="Sort order")
@click.option("-t", "--time", "time_filter", default=None, type=click.Choice(TIME_FILTERS), help="Time filter")
@click.option("-n", "--limit", default=25, type=int, help="Number of posts (max 100)")
@click.option("--after", default=None, help="Pagination cursor")
@listing_options
def custom_feed(
name: str,
sort: str, time_filter: str | None, limit: int, after: str | None,
as_json: bool, as_yaml: bool,
output_file: str | None, full_text: bool, compact: bool,
) -> None:
"""Browse posts from a custom feed (multireddit)"""
cred = require_auth()
_handle_listing(
cred,
action=lambda c: c.get_custom_feed(
feed_name=name,
sort=sort,
limit=limit,
after=after,
time_filter=time_filter,
),
data_title=f"πŸ“‘ Custom Feed: {name} ({sort})",
next_cmd=f"rdt custom-feed {name} -s {sort}",
as_json=as_json, as_yaml=as_yaml,
output_file=output_file, full_text=full_text, compact=compact,
)


@click.command(name="custom-feed-add")
@click.argument("name")
@click.argument("subreddit")
def custom_feed_add(name: str, subreddit: str) -> None:
"""Add a subreddit to a custom feed (multireddit)"""
from ._common import exit_for_error, write_delay

cred = require_auth()
try:
with RedditClient(cred) as client:
client.validate_session()
client.add_to_custom_feed(name, subreddit)
write_delay()
console.print(f"[green]βœ… Added[/green] r/{subreddit} to custom feed [bold cyan]{name}[/bold cyan]")
except Exception as exc:
exit_for_error(exc, prefix="Failed to add to custom feed")


@click.command(name="custom-feed-remove")
@click.argument("name")
@click.argument("subreddit")
def custom_feed_remove(name: str, subreddit: str) -> None:
"""Remove a subreddit from a custom feed (multireddit)"""
from ._common import exit_for_error, write_delay

cred = require_auth()
try:
with RedditClient(cred) as client:
client.validate_session()
client.remove_from_custom_feed(name, subreddit)
write_delay()
console.print(f"[green]βœ… Removed[/green] r/{subreddit} from custom feed [bold cyan]{name}[/bold cyan]")
except Exception as exc:
exit_for_error(exc, prefix="Failed to remove from custom feed")


@click.command(name="custom-feed-create")
@click.argument("name")
@click.option("--display-name", default=None, help="Display name of custom feed")
@click.option("--description", default="", help="Description text")
@click.option("--public", "visibility", flag_value="public", help="Make feed public")
@click.option("--private", "visibility", flag_value="private", default=True, help="Make feed private (default)")
@click.option("-s", "--subreddits", default="", help="Comma-separated list of initial subreddits")
def custom_feed_create(
name: str, display_name: str | None, description: str, visibility: str, subreddits: str
) -> None:
"""Create a new custom feed (multireddit)"""
from ._common import exit_for_error, write_delay

cred = require_auth()
sub_list = [s.strip() for s in subreddits.split(",") if s.strip()]
try:
with RedditClient(cred) as client:
client.validate_session()
client.create_custom_feed(
feed_name=name,
display_name=display_name,
description=description,
visibility=visibility,
subreddits=sub_list,
)
write_delay()
console.print(f"[green]βœ… Created custom feed[/green] [bold cyan]{name}[/bold cyan] ({visibility})")
except Exception as exc:
exit_for_error(exc, prefix="Failed to create custom feed")


@click.command(name="custom-feed-delete")
@click.argument("name")
@click.option("-y", "--yes", is_flag=True, help="Skip confirmation prompt")
def custom_feed_delete(name: str, yes: bool) -> None:
"""Delete a custom feed (multireddit)"""
from ._common import exit_for_error, write_delay

cred = require_auth()
if not yes and not click.confirm(f"Are you sure you want to delete custom feed '{name}'?"):
console.print("[dim]Cancelled[/dim]")
return
try:
with RedditClient(cred) as client:
client.validate_session()
client.delete_custom_feed(name)
write_delay()
console.print(f"[green]βœ… Deleted custom feed[/green] [bold cyan]{name}[/bold cyan]")
except Exception as exc:
exit_for_error(exc, prefix="Failed to delete custom feed")


# ── open ────────────────────────────────────────────────────────────


Expand Down
Loading