Skip to content

feat: add embeds support - #41

Open
Yochyo wants to merge 1 commit into
liamw1:masterfrom
Yochyo:feature/embeds
Open

feat: add embeds support#41
Yochyo wants to merge 1 commit into
liamw1:masterfrom
Yochyo:feature/embeds

Conversation

@Yochyo

@Yochyo Yochyo commented Nov 9, 2025

Copy link
Copy Markdown
Collaborator

Some of this code is probably quite badly written.

This is pretty much only a proof of concept. The following parts might need to be changed:

  1. The AppState Struct and the way it is shared with every route (I had no idea how to share the content of the index.htm file with the /index/post/{post_id} route. The docs recommend using an AppState)

  2. There's lots of beautiful (and totally unsafe) unwrap() calls.

  3. The code needs to check the following things
    3.1) if the index.htm file exists
    3.2) If the user wants embeds enabled (this should probably be a config variable?)
    3.3) if anonymous users have the permission to see posts (this potentially allows leaking of thumbnails otherwise)

  4. The post author currently isn't being displayed

  5. The PostInfo Struct probably shouldn't have public properties?

I used the following docker-compose.yml

# Example Docker Compose configuration
#
# Use this as a template to set up docker compose, or as guide to set up other
# orchestration services
services:
  server:
#    image: oxibooru/server:latest
    build:
      context: server
      args: 
        # Setting this to `native` can give small performance gains for some 
        # operations such as reverse search, but makes binary less portable.
        # Leave blank to let Rust determine the target CPU. 
        TARGET_CPU:
    depends_on:
      - sql
    environment:
      # These should be the names of the dependent containers listed below,
      # or FQDNs/IP addresses if these services are running outside of Docker
      POSTGRES_HOST: sql
      POSTGRES_USER:
      POSTGRES_PASSWORD:
      POSTGRES_DB:
      POSTGRES_PORT:
      PORT:
    volumes:
      - client:/var/www:ro
      - "./data:/data"
      - "./server/config.toml:/opt/app/config.toml"
    stop_signal: SIGINT
    expose:
      - 6666

  client:
#    image: oxibooru/client:latest
    build: client
    depends_on:
      - server
    environment:
      BACKEND_HOST: server
      BASE_URL:
    volumes:
      - client:/var/www
      - "./data:/data:ro"
    expose:
      - 80

  sql:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER:
      POSTGRES_PASSWORD:
      POSTGRES_DB:
    volumes:
      - "${MOUNT_SQL}:/var/lib/postgresql/data"
    expose:
      - ${POSTGRES_PORT}
  web:
    image: nginx
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - client
      - server
    ports:
      - "1800:80"
    environment:
      - NGINX_PORT=80


volumes:
  client:

This is my nginx.conf:

# ideally, use ssl termination + cdn with a provider such as cloudflare.
# modify as needed!

# rate limiting zone
# poor man's ddos protection, essentially


events {
    worker_connections 1024;
}

http {
limit_req_zone $binary_remote_addr zone=throttle:10m rate=25r/s;
resolver 127.0.0.11;
server {
  server_name localhost;
  client_max_body_size 100M;
  client_body_timeout 30s;
  server_tokens off;
  location / {
    proxy_http_version 1.1;
    proxy_pass http://client:80;
    proxy_set_header Host $http_host;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Scheme $scheme;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;
    error_page 500 501 502 504 505 506 507 508 509 510 511 @err;
    error_page 503 @throttle;
  }

  location ~ ^/(post/[0-9]+)(?:/.*)?$ {
      set $query $1;
      proxy_pass http://server:6666/index/$query;
      proxy_set_header Accept "*/*";
  }

  location @err {
    return 500 "server error. please try again later.";
    default_type text/plain;
  }
  location @throttle {
    return 503 "we've detected abuse on your ip. please wait and try again later.";
    default_type text/plain;
  }
  listen 80;
  listen [::]:80;
}

}

These are the only values I changed inside the config.toml

# full url to the homepage of this szurubooru site, with no trailing slash
domain = "http://localhost:1800"
client_dir = "/var/www"
```

@phil-flip

Copy link
Copy Markdown
Collaborator

Resolves #40

@liamw1

liamw1 commented Nov 10, 2025

Copy link
Copy Markdown
Owner

Nice work! I'll try to get around to testing and reviewing this soon. Just know that I might be a little slow to do so because of the holidays.

@Yochyo

Yochyo commented Nov 11, 2025

Copy link
Copy Markdown
Collaborator Author

Take your time, we all have a life beside doing stuff on GitHub ^^

@liamw1 liamw1 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this! A few general notes:

  1. It looks like the submitted code hasn't been formatted, so be sure to run cargo fmt before committing.

  2. I made some comments relaying clippy warning, as I like to keep the codebase free of clippy warnings. These don't appear when compiling the code normally, so just do a quick cargo clippy when finishing up and try to address any feedback it gives. Occasionally there will be specific warnings that are obnoxious, unhelpful, or require contorting the code to silence. In those cases, we can discuss adding them to the list of allowed warnings defined at the top of main.rs.
    As a heads up, I've added a CI job that automatically runs clippy on PRs, but I don't expect it to pass in this case. That's because there were some style warnings in tests that weren't addressed on master that have recently been fixed. These warnings only appear when running cargo clippy --all-targets, so just address the ones that are from cargo clippy.

  1. Most of the remaining work here is figuring out how to remove these unwrap calls. These are great for prototyping, but as you probably know they aren't ideal for production code. The server can technically handle panics just fine, but they produce huge stack traces instead of nice errors with descriptions. Sometimes removing an unwrap can be pretty tricky, but most can be removed with one of a few simple strategies:
    a) If the None or Error variant is impossible in bug-free code, then it can be replaced with expect("msg")
    b) If a reasonable default can be used in place of a None or Error, it can be replaced with unwrap_or(default)
    c) Otherwise, it's best to propagate the error with the ? operator

Comment thread server/src/api/mod.rs
Comment thread server/src/api/embeds_api.rs
Comment thread server/src/api/embeds_api.rs
Comment thread server/src/api/embeds_api.rs
Comment thread server/src/api/embeds_api.rs
Comment thread server/src/config.rs
Comment thread server/src/resource/post.rs
width: config::get().thumbnails.post_width,
height: config::get().thumbnails.post_height,
}
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we only use id and thumbnail_url from post_info, querying for PostInfo is a bit overkill. We can actually get away with just the post_id because we can create a PostHash object from an ID, which has a thumbnail_url method. So maybe the role of get_post_info can be changed to just check if the post exists in the database.

This approach also has the benefit of not having to call unwrap() on post_info fields.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The oembed spec requires a width and height (section 2.3.4.1). So while it pains my heart to waste cpu cycles on querying for a PostInfo object, it's probably the best solution for now.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nevermind, forget what I said. I haven't touched the code for a bit and got confused. I will finish everything after I'm back from the office today.

// todo
author_name: None,
provider_name: config::get().public_info.name.to_string(),
provider_url: config::get().domain.as_deref().unwrap().to_string(),

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default config file doesn't have a domain field, so this unwrap will panic. Consider using unwrap_or with some reasonable default instead. Better yet, you could factor out

let domain = if let Some(domain) = config::get().domain.as_deref() {
domain.to_string()
} else if let Ok(domain) = std::env::var("HTTP_ORIGIN") {
domain
} else if let Ok(domain) = std::env::var("HTTP_REFERER") {
domain
} else if let Ok(port) = std::env::var("PORT") {
format!("http://localhost:{port}")
} else {
String::new()
};
let domain = domain.trim_end_matches('/');
into it's own function and use it here.

Comment thread server/src/api/mod.rs
@Yochyo

Yochyo commented Nov 12, 2025

Copy link
Copy Markdown
Collaborator Author

Holy macaroni, thank you ever so much for taking your time to write comments and suggestions for all the warnings. I'll do some research on best practices and will then push the fixes.

@liamw1

liamw1 commented Nov 12, 2025

Copy link
Copy Markdown
Owner

No problem! I found the Rust book to be a great resource when learning Rust. No need to read through all of it, as there are many concepts that transfer from other languages. I found the section on error handling to be very helpful, especially coming from languages that primarily handle errors via exceptions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants