mention

Now it's time to create a command to post to bluesky. To be precise, it is mention.

Now, let's create a new file and read it in 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);
}

This time, we don't support any hosts other than bsky.social because it is troublesome. Mainly profile.rs and mention.rs. Please be careful about that.

src/profile.rs

let url = "https://bsky.social/xrpc/app.bsky.actor.getProfile".to_owned() + &"?actor=" + &handle;

results matching ""

    No results matching ""