+++ date = "2024-10-21" tags = ["cloudflare", "bluesky"] title = "blueskyのoauthとbbsをlivestream serviceに実装する" +++ youtubeのlivestreamはchatのような書き込みができます。それを再現します。 まずはblueskyのoauthが動作するかの確認です。これは[bluesky-social/cookbook](https://github.com/bluesky-social/cookbook/tree/main/python-oauth-web-app)を使います。 ```sh # https://github.com/bluesky-social/cookbook/tree/main/python-oauth-web-app $ cd ./repos/cookbook/python-oauth-web-app $ rye sync $ rye run python3 -c 'import secrets; print(secrets.token_hex())'|xargs echo FLASK_SECRET_KEY|tr -d ' ' >> .env $ rye run python3 generate_jwk.py |xargs echo FLASK_CLIENT_SECRET_JWK|tr -d ' ' >> .env $ cat .env $ rye run flask run ``` oauthはlocalhostでは動作しません。したがって、`ngrok`, `tailscale`, `cloudflare`などを使用します。個人的にはcloudflareがおすすめです。 ```sh $ cloudflared tunnel --url http://localhost:5000 ``` 表示されるurlにアクセスするとoauthでloginすることができました。oauthの情報はserverに保存されており以後は承認なしにlogin可能となります。 ## bbsと連携する 今回はloginしている場合にbbsの書き込みシステムを表示します。 ```html:template/home.html {% block content %} {% if g.user %}

@{{ g.user['handle'] }}

{% endif %} ``` bbsを作ります。 ```toml:Cargo.toml [package] name = "rust-bbs" version = "0.1.0" edition = "2021" [dependencies] actix-web = "4.0" rusqlite = { version = "0.28", features = ["bundled"] } serde = { version = "1.0", features = ["derive"] } askama = "0.11" ``` ```rust:src/main.rs use actix_web::{web, App, HttpServer, HttpResponse, Responder}; use rusqlite::{Connection, Result as SqliteResult}; use serde::{Deserialize, Serialize}; use askama::Template; #[derive(Serialize, Deserialize)] struct Post { id: i32, content: String, } #[derive(Template)] #[template(path = "index.html")] struct IndexTemplate { posts: Vec, } #[derive(Template)] #[template(path = "post.html")] struct PostTemplate {} fn init_db() -> SqliteResult<()> { let conn = Connection::open("sqlite.db")?; conn.execute( "CREATE TABLE IF NOT EXISTS posts ( id INTEGER PRIMARY KEY, content TEXT NOT NULL )", [], )?; Ok(()) } async fn index() -> impl Responder { let conn = Connection::open("sqlite.db").unwrap(); let mut stmt = conn.prepare("SELECT id, content FROM posts ORDER BY id DESC").unwrap(); let posts = stmt.query_map([], |row| { Ok(Post { id: row.get(0)?, content: row.get(1)?, }) }).unwrap().filter_map(Result::ok).collect::>(); let template = IndexTemplate { posts }; HttpResponse::Ok().body(template.render().unwrap()) } async fn post_form() -> impl Responder { let template = PostTemplate {}; HttpResponse::Ok().body(template.render().unwrap()) } #[derive(Deserialize)] struct FormData { content: String, } async fn submit_post(form: web::Form) -> impl Responder { let conn = Connection::open("sqlite.db").unwrap(); conn.execute( "INSERT INTO posts (content) VALUES (?1)", [&form.content], ).unwrap(); web::Redirect::to("/").see_other() } #[actix_web::main] async fn main() -> std::io::Result<()> { init_db().unwrap(); HttpServer::new(|| { App::new() .route("/", web::get().to(index)) .route("/post", web::get().to(post_form)) .route("/submit", web::post().to(submit_post)) }) .bind("0.0.0.0:8080")? .run() .await } ``` ```html:template/index.html Simple BBS

Simple BBS

New Post
    {% for post in posts %}
  • {{ post.content }}
  • {% endfor %}
``` ```html:template/post.html New Post

New Post


``` ```yml:compose.yml services: web: build: . ports: - "8080:8080" volumes: - ./sqlite.db:/sqlite.db ``` ```sh:Dockerfile.txt FROM syui/aios WORKDIR /usr/src/app COPY . . RUN cargo build --release COPY ./templates /templates CMD ["/usr/src/app/target/release/rust-bbs"] ``` ```sh $ cargo build $ ./target/debug/rust-bbs $ docker compose up $ curl -sL localhost:8080 ``` あとは、iframeからparamでhandleを取得するので、それを使用するようにしたり、cssで見栄えを整えたら完成です。 要点だけまとめたので適時修正してください。redirectしたときにurlを変更しないとiframeで呼び出したとき2回目からはhandleが使えません。 ```rust:src/main.rs async fn submit_post( req: HttpRequest, form: web::Form ) -> Result { let query = web::Query::::from_query(req.query_string()) .unwrap_or_else(|_| web::Query(QueryParams { handle: None })); //let handle = query.handle.clone().filter(|h| !h.is_empty()); //println!("Debug: Extracted handle: {:?}", handle); let handle = if !form.handle.is_empty() { form.handle.clone() } else { query.handle.clone().unwrap_or_default() }; println!("Debug: Using handle: {:?}", handle); let conn = Connection::open("sqlite.db") .map_err(|_| ErrorInternalServerError("Database connection failed"))?; let result = conn.execute( "INSERT INTO posts (handle, content) VALUES (?1, ?2)", &[&form.handle, &form.content], ); match result { Ok(_) => { let redirect_url = if !handle.is_empty() { format!("/?handle={}", handle) } else { "/".to_string() }; Ok(HttpResponse::SeeOther() .append_header(("Location", redirect_url)) .finish()) }, //Ok(_) => Ok(web::Redirect::to("/").see_other()), Err(_) => Err(ErrorInternalServerError("Failed to insert post")), } } ``` ```html:templates/index.html
``` ```html:templates/home.htmll {% if g.user %} @{{ g.user['handle'] }} {% endif %} ```