week one (project fetching

This commit is contained in:
2026-03-14 17:17:55 +01:00
commit b6cd2aacc5
5 changed files with 2773 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/target
.env

1741
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

9
Cargo.toml Normal file
View File

@@ -0,0 +1,9 @@
[package]
name = "ft_cli"
version = "0.1.0"
edition = "2024"
[dependencies]
dotenv = "0.15.0"
reqwest = { version = "0.12", features = ["json", "blocking"] }
serde = { version = "1", features = ["derive"] }

942
ft-api.json Normal file
View File

@@ -0,0 +1,942 @@
{
"openapi": "3.0.3",
"info": {
"title": "Flavortown API",
"version": "v1",
"description": "You need an API key to use this! Go to your [account settings](https://flavortown.hackclub.com/kitchen?settings=1) to get one."
},
"servers": [
{
"url": "https://flavortown.hackclub.com/api/v1"
}
],
"security": [
{
"bearerAuth": []
}
],
"components": {
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"description": "Include your API key via `Authorization: Bearer YOUR_API_KEY`. You can find or regenerate your API key in your [account settings](https://flavortown.hackclub.com/kitchen?settings=1)."
}
},
"schemas": {
"Pagination": {
"type": "object",
"properties": {
"current_page": {
"type": "integer"
},
"total_pages": {
"type": "integer"
},
"total_count": {
"type": "integer"
},
"next_page": {
"type": "integer",
"nullable": true
}
}
},
"Project": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"repo_url": {
"type": "string"
},
"demo_url": {
"type": "string"
},
"readme_url": {
"type": "string"
},
"ai_declaration": {
"type": "string"
},
"ship_status": {
"type": "string"
},
"devlog_ids": {
"type": "array",
"items": {
"type": "integer"
}
},
"banner_url": {
"type": "string",
"nullable": true
},
"created_at": {
"type": "string",
"format": "date-time"
},
"updated_at": {
"type": "string",
"format": "date-time"
}
}
},
"Comment": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"author": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"display_name": {
"type": "string"
},
"avatar": {
"type": "string"
}
}
},
"body": {
"type": "string"
},
"created_at": {
"type": "string",
"format": "date-time"
},
"updated_at": {
"type": "string",
"format": "date-time"
}
}
},
"Devlog": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"body": {
"type": "string"
},
"comments_count": {
"type": "integer"
},
"duration_seconds": {
"type": "integer"
},
"likes_count": {
"type": "integer"
},
"scrapbook_url": {
"type": "string"
},
"created_at": {
"type": "string",
"format": "date-time"
},
"updated_at": {
"type": "string",
"format": "date-time"
},
"media": {
"type": "array",
"items": {
"type": "object",
"properties": {
"url": {
"type": "string"
},
"content_type": {
"type": "string"
}
}
}
},
"comments": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Comment"
}
}
}
},
"User": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"slack_id": {
"type": "string"
},
"display_name": {
"type": "string"
},
"avatar": {
"type": "string"
},
"project_ids": {
"type": "array",
"items": {
"type": "integer"
}
},
"cookies": {
"type": "integer",
"nullable": true
}
}
},
"UserDetail": {
"allOf": [
{
"$ref": "#/components/schemas/User"
},
{
"type": "object",
"properties": {
"vote_count": {
"type": "integer"
},
"like_count": {
"type": "integer"
},
"devlog_seconds_total": {
"type": "integer"
},
"devlog_seconds_today": {
"type": "integer"
}
}
}
]
},
"RegionEnabled": {
"type": "object",
"properties": {
"enabled_au": {
"type": "boolean"
},
"enabled_ca": {
"type": "boolean"
},
"enabled_eu": {
"type": "boolean"
},
"enabled_in": {
"type": "boolean"
},
"enabled_uk": {
"type": "boolean"
},
"enabled_us": {
"type": "boolean"
},
"enabled_xx": {
"type": "boolean"
}
}
},
"TicketCost": {
"type": "object",
"properties": {
"base_cost": {
"type": "number"
},
"au": {
"type": "number"
},
"ca": {
"type": "number"
},
"eu": {
"type": "number"
},
"in": {
"type": "number"
},
"uk": {
"type": "number"
},
"us": {
"type": "number"
},
"xx": {
"type": "number"
}
}
},
"StoreItem": {
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"description": {
"type": "string"
},
"old_prices": {
"type": "array",
"items": {}
},
"limited": {
"type": "boolean"
},
"stock": {
"type": "integer"
},
"type": {
"type": "string"
},
"show_in_carousel": {
"type": "boolean"
},
"accessory_tag": {
"type": "string"
},
"agh_contents": {
"type": "string"
},
"attached_shop_item_ids": {
"type": "array",
"items": {}
},
"buyable_by_self": {
"type": "boolean"
},
"long_description": {
"type": "string"
},
"max_qty": {
"type": "integer"
},
"one_per_person_ever": {
"type": "boolean"
},
"sale_percentage": {
"type": "integer"
},
"image_url": {
"type": "string"
},
"enabled": {
"$ref": "#/components/schemas/RegionEnabled"
},
"ticket_cost": {
"$ref": "#/components/schemas/TicketCost"
}
}
},
"Error": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
}
},
"ValidationErrors": {
"type": "object",
"properties": {
"errors": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
},
"responses": {
"Unauthorized": {
"description": "Missing or invalid API key",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"example": {
"error": "Invalid API key"
}
}
}
},
"Forbidden": {
"description": "Permission denied",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"NotFound": {
"description": "Resource not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
},
"example": {
"error": "Resource not found"
}
}
}
},
"UnprocessableEntity": {
"description": "Validation failed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationErrors"
}
}
}
}
}
},
"paths": {
"/projects": {
"get": {
"summary": "List projects",
"tags": [
"Projects"
],
"description": "Fetch a list of projects. Ratelimit: 5 reqs/min, 20 reqs/min if searching.",
"operationId": "listProjects",
"parameters": [
{
"name": "page",
"in": "query",
"required": false,
"schema": {
"type": "integer"
},
"description": "Page number for pagination"
},
{
"name": "query",
"in": "query",
"required": false,
"schema": {
"type": "string"
},
"description": "Search projects by title or description"
}
],
"responses": {
"200": {
"description": "A paginated list of projects",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"projects": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Project"
}
},
"pagination": {
"$ref": "#/components/schemas/Pagination"
}
}
}
}
}
},
"401": {
"$ref": "#/components/responses/Unauthorized"
}
}
},
"post": {
"summary": "Create a project",
"tags": [
"Projects"
],
"description": "Create a new project.",
"operationId": "createProject",
"parameters": [],
"responses": {
"201": {
"description": "Project created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Project"
}
}
}
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"422": {
"$ref": "#/components/responses/UnprocessableEntity"
}
},
"requestBody": {
"content": {
"application/x-www-form-urlencoded": {
"schema": {
"type": "object",
"required": [
"title",
"description"
],
"properties": {
"title": {
"type": "string",
"description": "Project title"
},
"description": {
"type": "string",
"description": "Project description"
},
"repo_url": {
"type": "string",
"description": "URL to the source code repository"
},
"demo_url": {
"type": "string",
"description": "URL to the live demo"
},
"readme_url": {
"type": "string",
"description": "URL to the README"
},
"ai_declaration": {
"type": "string",
"description": "Declaration of AI tools used in this project"
}
}
}
}
},
"required": true
}
}
},
"/projects/{id}": {
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer"
}
}
],
"get": {
"summary": "Get a project",
"tags": [
"Projects"
],
"description": "Fetch a specific project by ID. Ratelimit: 30 reqs/min.",
"operationId": "getProject",
"responses": {
"200": {
"description": "A single project",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Project"
}
}
}
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"404": {
"$ref": "#/components/responses/NotFound"
}
}
},
"patch": {
"summary": "Update a project",
"tags": [
"Projects"
],
"description": "Update an existing project.",
"operationId": "updateProject",
"parameters": [],
"responses": {
"200": {
"description": "Project updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Project"
}
}
}
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"403": {
"$ref": "#/components/responses/Forbidden"
},
"404": {
"$ref": "#/components/responses/NotFound"
},
"422": {
"$ref": "#/components/responses/UnprocessableEntity"
}
},
"requestBody": {
"content": {
"application/x-www-form-urlencoded": {
"schema": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Project title"
},
"description": {
"type": "string",
"description": "Project description"
},
"repo_url": {
"type": "string",
"description": "URL to the source code repository"
},
"demo_url": {
"type": "string",
"description": "URL to the live demo"
},
"readme_url": {
"type": "string",
"description": "URL to the README"
},
"ai_declaration": {
"type": "string",
"description": "Declaration of AI tools used in this project"
}
}
}
}
},
"required": true
}
}
},
"/projects/{project_id}/devlogs": {
"get": {
"summary": "List project devlogs",
"tags": [
"Projects"
],
"description": "Fetch all devlogs for a specific project.",
"operationId": "listProjectDevlogs",
"parameters": [
{
"name": "project_id",
"in": "path",
"required": true,
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "A paginated list of devlogs for the project",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"devlogs": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Devlog"
}
},
"pagination": {
"$ref": "#/components/schemas/Pagination"
}
}
}
}
}
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"404": {
"$ref": "#/components/responses/NotFound"
}
}
}
},
"/devlogs": {
"get": {
"summary": "List devlogs",
"tags": [
"Devlogs"
],
"description": "Fetch all devlogs across all projects.",
"operationId": "listDevlogs",
"parameters": [
{
"name": "page",
"in": "query",
"required": false,
"schema": {
"type": "integer"
},
"description": "Page number for pagination"
}
],
"responses": {
"200": {
"description": "A paginated list of devlogs",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"devlogs": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Devlog"
}
},
"pagination": {
"$ref": "#/components/schemas/Pagination"
}
}
}
}
}
},
"401": {
"$ref": "#/components/responses/Unauthorized"
}
}
}
},
"/devlogs/{id}": {
"get": {
"summary": "Get a devlog",
"tags": [
"Devlogs"
],
"description": "Fetch a devlog by ID.",
"operationId": "getDevlog",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "A single devlog",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Devlog"
}
}
}
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"404": {
"$ref": "#/components/responses/NotFound"
}
}
}
},
"/store": {
"get": {
"summary": "List store items",
"tags": [
"Store"
],
"description": "Fetch a list of store items. Ratelimit: 5 reqs/min.",
"operationId": "listStoreItems",
"responses": {
"200": {
"description": "A list of store items",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/StoreItem"
}
}
}
}
},
"401": {
"$ref": "#/components/responses/Unauthorized"
}
}
}
},
"/store/{id}": {
"get": {
"summary": "Get a store item",
"tags": [
"Store"
],
"description": "Fetch a specific store item by ID. Ratelimit: 30 reqs/min.",
"operationId": "getStoreItem",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "integer"
}
}
],
"responses": {
"200": {
"description": "A single store item",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StoreItem"
}
}
}
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"404": {
"$ref": "#/components/responses/NotFound"
}
}
}
},
"/users": {
"get": {
"summary": "List users",
"tags": [
"Users"
],
"description": "Fetch a list of users. Ratelimit: 5 reqs/min.",
"operationId": "listUsers",
"parameters": [
{
"name": "page",
"in": "query",
"required": false,
"schema": {
"type": "integer"
},
"description": "Page number for pagination"
},
{
"name": "query",
"in": "query",
"required": false,
"schema": {
"type": "string"
},
"description": "Search users by display name or slack ID"
}
],
"responses": {
"200": {
"description": "A paginated list of users",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"users": {
"type": "array",
"items": {
"$ref": "#/components/schemas/User"
}
},
"pagination": {
"$ref": "#/components/schemas/Pagination"
}
}
}
}
}
},
"401": {
"$ref": "#/components/responses/Unauthorized"
}
}
}
},
"/users/{id}": {
"get": {
"summary": "Get a user",
"tags": [
"Users"
],
"description": "Fetch a specific user by ID. Use \"me\" as the ID to fetch the authenticated user.",
"operationId": "getUser",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
},
"description": "User ID or \"me\" for the authenticated user"
}
],
"responses": {
"200": {
"description": "A single user with additional stats",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserDetail"
}
}
}
},
"401": {
"$ref": "#/components/responses/Unauthorized"
},
"404": {
"$ref": "#/components/responses/NotFound"
}
}
}
}
}
}

79
src/main.rs Normal file
View File

@@ -0,0 +1,79 @@
use dotenv::dotenv;
use std::env;
use serde::Deserialize;
#[derive(Deserialize)]
pub struct User {
pub id: u64,
pub slack_id: String,
pub display_name: String,
pub avatar: String,
pub project_ids: Vec<u64>,
pub cookies: Option<u64>,
pub vote_count: u64,
pub like_count: u64,
pub devlog_seconds_total: u64,
pub devlog_seconds_today: u64,
}
#[derive(Deserialize)]
pub struct Project {
pub id: u64,
pub title: String,
pub description: String,
pub repo_url: String,
pub demo_url: String,
pub readme_url: String,
pub ai_declaration: Option<String>,
pub ship_status: String,
pub devlog_ids: Vec<u64>,
pub banner_url: Option<String>,
pub created_at: String,
pub updated_at: String,
}
fn main() {
dotenv().ok();
let ft_key = env::var("FT_KEY").expect("FT_KEY must be set");
let client = reqwest::blocking::Client::new();
let user: User = client
.get("https://flavortown.hackclub.com/api/v1/users/me")
.header("Authorization", format!("Bearer {}", ft_key))
.send()
.expect("Failed to send request")
.json()
.expect("Failed to parse JSON");
let mut projects: Vec<Project> = Vec::new();
println!("Logged in as {}", user.display_name);
let cookies_display = user
.cookies
.map_or("(undeclared)".to_string(), |count| count.to_string());
println!(
"You have {} cookies and have voted {} times.",
cookies_display, user.vote_count
);
for pid in user.project_ids {
let project: Project = client
.get(format!("https://flavortown.hackclub.com/api/v1/projects/{}", pid))
.header("Authorization", format!("Bearer {}", ft_key))
.send()
.expect("Failed to send request")
.json()
.expect("Failed to parse JSON");
projects.push(project);
}
println!(
"Your projects are: {}",
projects
.iter()
.map(|p| p.title.as_str())
.collect::<Vec<_>>()
.join(", ")
);
}