diff --git a/src/void_tools/cli.py b/src/void_tools/cli.py index ab1fa7e..f376e09 100644 --- a/src/void_tools/cli.py +++ b/src/void_tools/cli.py @@ -873,6 +873,65 @@ def cmd_monitor(args): output_path = os.path.join(os.getcwd(), 'horizon.yaml') run_monitor_cycle(config_path, output_path) +def cmd_feed(args): + """Fetch the home feed or discover feed.""" + client = get_client() + + try: + print(f"Fetching {args.type} feed...") + + if args.type == "following": + # Chronological timeline from followed accounts + response = client.app.bsky.feed.get_timeline({'limit': args.limit}) + feed_items = response.feed + elif args.type == "discover": + # Algorithmic "What's Hot" feed + # This is the official Bluesky discover feed generator + whats_hot_uri = 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot' + response = client.app.bsky.feed.get_feed({ + 'feed': whats_hot_uri, + 'limit': args.limit + }) + feed_items = response.feed + else: + print(f"Unknown feed type: {args.type}", file=sys.stderr) + sys.exit(1) + + # Format output similar to inbox.yaml + feed_data = [] + for i, item in enumerate(feed_items): + post = item.post + feed_entry = { + "id": f"feed_{i}", + "author": f"@{post.author.handle}", + "text": getattr(post.record, 'text', ''), + "uri": post.uri, + "cid": post.cid, + "indexed_at": post.indexed_at, + } + + # Include reply info if present + if hasattr(post.record, 'reply') and post.record.reply: + feed_entry["is_reply"] = True + + # Include engagement counts + feed_entry["like_count"] = getattr(post, 'like_count', 0) + feed_entry["reply_count"] = getattr(post, 'reply_count', 0) + feed_entry["repost_count"] = getattr(post, 'repost_count', 0) + + feed_data.append(feed_entry) + + # Write to feed.yaml + output_file = args.output + with open(output_file, "w") as f: + yaml.dump(feed_data, f, sort_keys=False, default_flow_style=False) + + print(f"Fetched {len(feed_data)} posts to {output_file}") + + except Exception as e: + print(f"Error fetching feed: {e}", file=sys.stderr) + sys.exit(1) + def main(): parser = argparse.ArgumentParser(description="Void ATProto CLI Tools") subparsers = parser.add_subparsers(dest="command", required=True) @@ -931,6 +990,13 @@ def main(): # Monitor monitor_parser = subparsers.add_parser("monitor", help="Run the Horizon Radar to discover relevant network activity") + # Feed + feed_parser = subparsers.add_parser("feed", help="Fetch the home timeline or discover feed") + feed_parser.add_argument("--type", choices=["following", "discover"], default="following", + help="Feed type: 'following' for chronological timeline, 'discover' for algorithmic feed (default: following)") + feed_parser.add_argument("--limit", type=int, default=50, help="Number of posts to fetch (default: 50)") + feed_parser.add_argument("--output", default="feed.yaml", help="Output file path (default: feed.yaml)") + args = parser.parse_args() if args.command == "notifs": @@ -953,6 +1019,8 @@ def main(): cmd_annotate(args) elif args.command == "monitor": cmd_monitor(args) + elif args.command == "feed": + cmd_feed(args) if __name__ == "__main__": main()