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
$ 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がおすすめです。
$ cloudflared tunnel --url http://localhost:5000
表示されるurlにアクセスするとoauthでloginすることができました。oauthの情報はserverに保存されており以後は承認なしにlogin可能となります。
bbsと連携する
今回はloginしている場合にbbsの書き込みシステムを表示します。
{% block content %}
{% if g.user %}
<p>@{{ g.user['handle'] }}</p>
<iframe src="example.com?handle={{ g.user['handle'] }}"></iframe>
{% endif %}
bbsを作ります。
[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"
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<Post>,
}
#[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::<Vec<Post>>();
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<FormData>) -> 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
}
<!DOCTYPE html>
<html>
<head>
<title>Simple BBS</title>
</head>
<body>
<h1>Simple BBS</h1>
<a href="/post">New Post</a>
<ul>
{% for post in posts %}
<li>{{ post.content }}</li>
{% endfor %}
</ul>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>New Post</title>
</head>
<body>
<h1>New Post</h1>
<form action="/submit" method="post">
<textarea name="content" required></textarea>
<br>
<input type="submit" value="Post">
</form>
</body>
</html>
services:
web:
build: .
ports:
- "8080:8080"
volumes:
- ./sqlite.db:/sqlite.db
FROM syui/aios
WORKDIR /usr/src/app
COPY . .
RUN cargo build --release
COPY ./templates /templates
CMD ["/usr/src/app/target/release/rust-bbs"]
$ docker compose up
$ curl -sL localhost:8080
あとは、iframeからparamでhandleを取得するので、それを使用するようにしたり、cssで見栄えを整えたら完成です。