mention
いよいよ、blueskyにpost
するコマンドを作成します。正確にはmention
です。
今度は、新しいファイルを作成し、そのファイルをsrc/main.rs
で読み込む方式で書いてみます。
Cargo.toml
[package]
name = "ai"
version = "0.1.0"
edition = "2021"
[dependencies]
seahorse = "*"
reqwest = { version = "*", features = ["blocking", "json"] }
tokio = { version = "1", features = ["full"] }
serde_derive = "1.0"
serde_json = "1.0"
serde = "*"
config = { git = "https://github.com/mehcode/config-rs", branch = "master" }
shellexpand = "*"
toml = "*"
iso8601-timestamp = "0.2.10"
src/data.rs
use config::{Config, ConfigError, File};
use serde_derive::{Deserialize, Serialize};
#[derive(Debug, Deserialize)]
#[allow(unused)]
pub struct Data {
pub host: String,
pub pass: String,
pub handle: String,
}
#[derive(Serialize, Deserialize)]
#[allow(non_snake_case)]
pub struct Token {
pub did: String,
pub accessJwt: String,
pub refreshJwt: String,
pub handle: String,
}
#[derive(Serialize, Deserialize)]
#[allow(non_snake_case)]
pub struct Tokens {
pub did: String,
pub access: String,
pub refresh: String,
pub handle: String,
}
#[derive(Serialize, Deserialize)]
#[allow(non_snake_case)]
pub struct Labels {
}
#[derive(Serialize, Deserialize)]
#[allow(non_snake_case)]
pub struct Declaration {
pub actorType: String,
pub cid: String,
}
#[derive(Serialize, Deserialize)]
#[allow(non_snake_case)]
pub struct Viewer {
pub muted: bool,
}
#[derive(Serialize, Deserialize)]
#[allow(non_snake_case)]
pub struct Profile {
pub did: String,
pub handle: String,
pub followsCount: Option<i32>,
pub followersCount: Option<i32>,
pub postsCount: i32,
pub indexedAt: Option<String>,
pub avatar: Option<String>,
pub banner: Option<String>,
pub displayName: Option<String>,
pub description: Option<String>,
pub viewer: Viewer,
pub labels: Labels,
}
impl Data {
pub fn new() -> Result<Self, ConfigError> {
let d = shellexpand::tilde("~") + "/.config/ai/config.toml";
let s = Config::builder()
.add_source(File::with_name(&d))
.add_source(config::Environment::with_prefix("APP"))
.build()?;
s.try_deserialize()
}
}
impl Tokens {
pub fn new() -> Result<Self, ConfigError> {
let d = shellexpand::tilde("~") + "/.config/ai/token.toml";
let s = Config::builder()
.add_source(File::with_name(&d))
.add_source(config::Environment::with_prefix("APP"))
.build()?;
s.try_deserialize()
}
}
pub fn token_toml(s: &str) -> String {
let s = String::from(s);
let tokens = Tokens::new().unwrap();
let tokens = Tokens {
did: tokens.did,
access: tokens.access,
refresh: tokens.refresh,
handle: tokens.handle,
};
match &*s {
"did" => tokens.did,
"access" => tokens.access,
"refresh" => tokens.refresh,
"handle" => tokens.handle,
_ => s,
}
}
src/profile.rs
extern crate reqwest;
use crate::token_toml;
pub async fn get_request(handle: String) -> String {
let token = token_toml(&"access");
let url = "https://bsky.social/xrpc/app.bsky.actor.getProfile".to_owned() + &"?actor=" + &handle;
let client = reqwest::Client::new();
let res = client
.get(url)
.header("Authorization", "Bearer ".to_owned() + &token)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
return res
}
src/mention.rs
extern crate reqwest;
use crate::token_toml;
use serde_json::json;
use iso8601_timestamp::Timestamp;
pub async fn post_request(text: String, at: String, udid: String, s: i32, e: i32) -> String {
let token = token_toml(&"access");
let did = token_toml(&"did");
let handle = token_toml(&"handle");
let url = "https://bsky.social/xrpc/com.atproto.repo.createRecord";
let col = "app.bsky.feed.post".to_string();
let d = Timestamp::now_utc();
let d = d.to_string();
let post = Some(json!({
"did": did.to_string(),
"repo": handle.to_string(),
"collection": col.to_string(),
"record": {
"text": at.to_string() + &" ".to_string() + &text.to_string(),
"$type": "app.bsky.feed.post",
"createdAt": d.to_string(),
"facets": [
{
"$type": "app.bsky.richtext.facet",
"index": {
"byteEnd": e,
"byteStart": s
},"features": [
{
"did": udid.to_string(),
"$type": "app.bsky.richtext.facet#mention"
}
]
}
]
},
}));
let client = reqwest::Client::new();
let res = client
.post(url)
.json(&post)
.header("Authorization", "Bearer ".to_owned() + &token)
.send()
.await
.unwrap()
.text()
.await
.unwrap();
return res
}
src/main.rs
pub mod data;
pub mod mention;
pub mod profile;
use seahorse::{App, Command, Context, Flag, FlagType};
use std::env;
use std::fs;
use std::io::Write;
use std::collections::HashMap;
use data::Data as Datas;
use crate::data::Token;
use crate::data::Tokens;
use crate::data::Profile;
use crate::data::token_toml;
fn main() {
let args: Vec<String> = env::args().collect();
let app = App::new(env!("CARGO_PKG_NAME"))
//.action(c_ascii_art)
.command(
Command::new("bluesky")
.alias("b")
.action(c_list_records),
)
.command(
Command::new("login")
.alias("l")
.action(c_access_token),
)
.command(
Command::new("profile")
.alias("p")
.action(c_profile),
)
.command(
Command::new("mention")
.alias("m")
.action(c_mention)
.flag(
Flag::new("post", FlagType::String)
.description("post flag\n\t\t\t$ ai m syui.bsky.social -p text")
.alias("p"),
)
)
;
app.run(args);
}
#[tokio::main]
async fn list_records() -> reqwest::Result<()> {
let client = reqwest::Client::new();
let handle= "support.bsky.team";
let col = "app.bsky.feed.post";
let body = client.get("https://bsky.social/xrpc/com.atproto.repo.listRecords")
.query(&[("repo", &handle),("collection", &col),("limit", &"1"),("revert", &"true")])
.send()
.await?
.text()
.await?;
println!("{}", body);
Ok(())
}
fn c_list_records(_c: &Context) {
list_records().unwrap();
}
#[tokio::main]
async fn access_token() -> reqwest::Result<()> {
let file = "/.config/ai/token.toml";
let mut f = shellexpand::tilde("~").to_string();
f.push_str(&file);
let data = Datas::new().unwrap();
let data = Datas {
host: data.host,
handle: data.handle,
pass: data.pass,
};
let url = "https://".to_owned() + &data.host + &"/xrpc/com.atproto.server.createSession";
let mut map = HashMap::new();
map.insert("identifier", &data.handle);
map.insert("password", &data.pass);
let client = reqwest::Client::new();
let res = client
.post(url)
.json(&map)
.send()
.await?
.text()
.await?;
let json: Token = serde_json::from_str(&res).unwrap();
let tokens = Tokens {
did: json.did.to_string(),
access: json.accessJwt.to_string(),
refresh: json.refreshJwt.to_string(),
handle: json.handle.to_string(),
};
let toml = toml::to_string(&tokens).unwrap();
let mut f = fs::File::create(f.clone()).unwrap();
f.write_all(&toml.as_bytes()).unwrap();
Ok(())
}
fn c_access_token(_c: &Context) {
access_token().unwrap();
}
fn profile(c: &Context) {
let m = c.args[0].to_string();
let h = async {
let str = profile::get_request(m.to_string()).await;
println!("{}",str);
};
let res = tokio::runtime::Runtime::new().unwrap().block_on(h);
return res
}
fn c_profile(c: &Context) {
access_token().unwrap();
profile(c);
}
fn mention(c: &Context) {
let m = c.args[0].to_string();
let h = async {
let str = profile::get_request(m.to_string()).await;
println!("{}",str);
let profile: Profile = serde_json::from_str(&str).unwrap();
let udid = profile.did;
let handle = profile.handle;
let at = "@".to_owned() + &handle;
let e = at.chars().count();
let s = 0;
if let Ok(post) = c.string_flag("post") {
let str = mention::post_request(post.to_string(), at.to_string(), udid.to_string(), s, e.try_into().unwrap()).await;
println!("{}",str);
}
};
let res = tokio::runtime::Runtime::new().unwrap().block_on(h);
return res
}
fn c_mention(c: &Context) {
access_token().unwrap();
mention(c);
}
今回、面倒なのでbsky.social
以外のhostには対応していません。主にprofile.rs
, mention.rs
です。その辺は注意してください。
src/profile.rs
let url = "https://bsky.social/xrpc/app.bsky.actor.getProfile".to_owned() + &"?actor=" + &handle;