AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
// 專屬號碼、打電話、X — with the person's own accounts (the original uses its own phone and X
// services; here: a Twilio number and an X developer app).
//
// • 簡訊 (Twilio): the bot answers texts sent to the number (polled, no public address needed) and
// replies from it.
// • 打電話 (Twilio): the bot can call someone and read out a message (asked every time).
// • X: the bot answers @mentions as replies, and can post (posting is asked every time).
//
// Test servers can stand in: ANYBOT_TWILIO_API and ANYBOT_X_API.
use base64 ::Engine ;
use serde_json ::{ json , Value } ;
use std ::sync ::{ Mutex , OnceLock } ;
use std ::time ::Duration ;
pub const SMS_MAX : usize = 1_600 ;
pub const X_MAX : usize = 280 ;
fn twilio ( ) -> String {
std ::env ::var ( " ANYBOT_TWILIO_API " ) . unwrap_or_else ( | _ | " https://api.twilio.com " . into ( ) )
}
fn x_api ( ) -> String {
std ::env ::var ( " ANYBOT_X_API " ) . unwrap_or_else ( | _ | " https://api.x.com " . into ( ) )
}
fn agent ( ) -> ureq ::Agent {
ureq ::Agent ::config_builder ( ) . timeout_global ( Some ( Duration ::from_secs ( 30 ) ) ) . http_status_as_error ( false ) . build ( ) . into ( )
}
/// "AccountSID:AuthToken" → (sid, Basic header).
pub fn twilio_auth ( token : & str ) -> Result < ( String , String ) , String > {
let ( sid , secret ) = token . trim ( ) . split_once ( ':' ) . ok_or ( " Twilio 金鑰要填「Account SID:Auth Token」 " ) ? ;
Ok ( ( sid . trim ( ) . to_string ( ) , format! ( " Basic {} " , base64 ::engine ::general_purpose ::STANDARD . encode ( format! ( " {} : {} " , sid . trim ( ) , secret . trim ( ) ) ) ) ) )
}
fn form ( pairs : & [ ( & str , & str ) ] ) -> String {
Remove what nothing uses and keep one copy of what was written several times
Unused (found by taking away the dead-code allowances; nothing else used them, tests aside):
computer::specs, quiet_button, the never-made Poke::Spin, ten icons and their SVGs, the
theme's chrome/bubble_agent colours, ImportRow.line, voice's multipart/transcribe/speak
wrappers, and the old keychain writers vault::save/remove/index_add with their live test
(the vault store replaced them; reading old keychain entries for the import stays).
One copy each: percent-encoding (httpd::url_encode, was four), tool arguments (tools::args_of,
was three), the WebSocket handshake (httpd::accept_websocket, was three), the read pacing
(wire::Pace, was two: relay and app), docker with input and a time limit (dk_with_input, was
two). Docs: FEATURES §看畫面 (VNC), protocol (v* frames, /screen, tickets), architecture.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 15:21:47 +00:00
let enc = crate ::httpd ::url_encode ;
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
pairs . iter ( ) . map ( | ( k , v ) | format! ( " {} = {} " , enc ( k ) , enc ( v ) ) ) . collect ::< Vec < _ > > ( ) . join ( " & " )
}
fn read ( mut r : ureq ::http ::Response < ureq ::Body > ) -> Result < Value , String > {
let st = r . status ( ) . as_u16 ( ) ;
let text = r . body_mut ( ) . read_to_string ( ) . unwrap_or_default ( ) ;
let v : Value = serde_json ::from_str ( & text ) . unwrap_or ( Value ::Null ) ;
if ! ( 200 .. 300 ) . contains ( & st ) {
let msg = v [ " message " ] . as_str ( ) . or ( v [ " detail " ] . as_str ( ) ) . or ( v [ " title " ] . as_str ( ) ) . map ( String ::from ) . unwrap_or_else ( | | text . chars ( ) . take ( 160 ) . collect ( ) ) ;
return Err ( format! ( " HTTP {st} : {msg} " ) ) ;
}
Ok ( v )
}
fn twilio_post ( token : & str , path : & str , pairs : & [ ( & str , & str ) ] ) -> Result < Value , String > {
let ( sid , auth ) = twilio_auth ( token ) ? ;
let r = agent ( )
. post ( & format! ( " {} /2010-04-01/Accounts/ {sid} / {path} " , twilio ( ) ) )
. header ( " Authorization " , & auth )
. header ( " Content-Type " , " application/x-www-form-urlencoded " )
. send ( form ( pairs ) . as_bytes ( ) )
. map_err ( | e | format! ( " 連不到 Twilio: {e} " ) ) ? ;
read ( r )
}
pub fn twilio_get ( token : & str , path_query : & str ) -> Result < Value , String > {
let ( sid , auth ) = twilio_auth ( token ) ? ;
let r = agent ( ) . get ( & format! ( " {} /2010-04-01/Accounts/ {sid} / {path_query} " , twilio ( ) ) ) . header ( " Authorization " , & auth ) . call ( ) . map_err ( | e | format! ( " 連不到 Twilio: {e} " ) ) ? ;
read ( r )
}
pub fn twilio_test ( token : & str ) -> Result < String , String > {
let v = twilio_get ( token , " .json " ) ? ;
Ok ( format! ( " 已連上 Twilio: {} " , v [ " friendly_name " ] . as_str ( ) . unwrap_or ( " 帳號 " ) ) )
}
pub fn send_sms ( token : & str , from : & str , to : & str , text : & str ) -> Result < ( ) , String > {
for part in crate ::channels ::split ( text , SMS_MAX ) {
twilio_post ( token , " Messages.json " , & [ ( " From " , from ) , ( " To " , to ) , ( " Body " , & part ) ] ) ? ;
}
Ok ( ( ) )
}
/// New texts to `number` (oldest first), skipping ones already seen. (sid, from, body)
pub fn sms_new ( list : & Value , seen : & mut std ::collections ::HashSet < String > ) -> Vec < ( String , String , String ) > {
let mut v : Vec < ( String , String , String ) > = list [ " messages " ]
. as_array ( )
. into_iter ( )
. flatten ( )
. filter ( | m | m [ " direction " ] . as_str ( ) = = Some ( " inbound " ) )
. filter_map ( | m | Some ( ( m [ " sid " ] . as_str ( ) ? . to_string ( ) , m [ " from " ] . as_str ( ) . unwrap_or ( " " ) . to_string ( ) , m [ " body " ] . as_str ( ) . unwrap_or ( " " ) . to_string ( ) ) ) )
. filter ( | ( sid , _ , _ ) | seen . insert ( sid . clone ( ) ) )
. collect ( ) ;
// Twilio lists newest first.
v . reverse ( ) ;
v
}
fn xml_escape ( s : & str ) -> String {
s . replace ( '&' , " & " ) . replace ( '<' , " < " ) . replace ( '>' , " > " ) . replace ( '"' , " " " )
}
/// Call `to` from the Twilio number and read `message` aloud (Traditional Chinese voice).
pub fn call ( token : & str , from : & str , to : & str , message : & str ) -> Result < String , String > {
let twiml = format! ( " <Response><Say language= \" zh-TW \" > {} </Say><Pause length= \" 1 \" /><Say language= \" zh-TW \" > {} </Say></Response> " , xml_escape ( message ) , xml_escape ( message ) ) ;
let v = twilio_post ( token , " Calls.json " , & [ ( " From " , from ) , ( " To " , to ) , ( " Twiml " , & twiml ) ] ) ? ;
Ok ( format! ( " 已撥給 {to} (通話編號 {} ,對方接起後會聽到兩次)。 " , v [ " sid " ] . as_str ( ) . unwrap_or ( " ? " ) ) )
}
// ---------- X ----------
fn x_get ( token : & str , path : & str ) -> Result < Value , String > {
let r = agent ( ) . get ( & format! ( " {} {path} " , x_api ( ) ) ) . header ( " Authorization " , & format! ( " Bearer {} " , token . trim ( ) ) ) . call ( ) . map_err ( | e | format! ( " 連不到 X: {e} " ) ) ? ;
read ( r )
}
pub fn x_me ( token : & str ) -> Result < ( String , String ) , String > {
let v = x_get ( token , " /2/users/me " ) ? ;
Ok ( ( v [ " data " ] [ " id " ] . as_str ( ) . unwrap_or ( " " ) . to_string ( ) , v [ " data " ] [ " username " ] . as_str ( ) . unwrap_or ( " " ) . to_string ( ) ) )
}
/// Post (optionally as a reply). Returns the new post's address.
pub fn x_post ( token : & str , text : & str , reply_to : & str ) -> Result < String , String > {
let mut body = json! ( { " text " : text . chars ( ) . take ( X_MAX ) . collect ::< String > ( ) } ) ;
if ! reply_to . is_empty ( ) {
body [ " reply " ] = json! ( { " in_reply_to_tweet_id " : reply_to } ) ;
}
let r = agent ( )
. post ( & format! ( " {} /2/tweets " , x_api ( ) ) )
. header ( " Authorization " , & format! ( " Bearer {} " , token . trim ( ) ) )
. header ( " Content-Type " , " application/json " )
. send ( body . to_string ( ) . as_bytes ( ) )
. map_err ( | e | format! ( " 連不到 X: {e} " ) ) ? ;
let v = read ( r ) ? ;
Ok ( format! ( " https://x.com/i/status/ {} " , v [ " data " ] [ " id " ] . as_str ( ) . unwrap_or ( " " ) ) )
}
Clippy is clean on macOS and Linux, for the app and the relay
Every warning `cargo clippy --all-targets` gave (50 on macOS, 8 more only on
Linux, 1 in the relay), fixed without changing what anything does:
- long types get a name (Peer, Members, Imported, Joining, Mention, Stream…);
- the two big enum variants are boxed (the vault's login plan, joining an
account);
- a test now waits for the copy it relaunched (it left a zombie behind);
- a hint's format had a dead `if` whose two branches were both "";
- the rest is what clippy suggests: sort_by_key, as_chunks, is_none_or,
is_multiple_of, while let, from_ref, next_back, struct init, no casts to
the same type.
Checked: cargo test (177 macOS, 181 Linux), relay tests (11), remote_e2e
28/28, and the live Linux test (X11; Wayland clicks, keys, Chinese typing,
scroll and screenshots).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha2RVfSYmrhqNnqx9Z4
2026-11-07 02:10:28 +00:00
/// A mention on X: (id, author username, text).
pub type Mention = ( String , String , String ) ;
/// Mentions newer than `since_id`, oldest first. Returns the newest id.
pub fn x_mentions ( token : & str , user_id : & str , since_id : & str ) -> Result < ( String , Vec < Mention > ) , String > {
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
let q = if since_id . is_empty ( ) { String ::new ( ) } else { format! ( " &since_id= {since_id} " ) } ;
let v = x_get ( token , & format! ( " /2/users/ {user_id} /mentions?max_results=20&expansions=author_id&user.fields=username {q} " ) ) ? ;
Ok ( mentions_new ( & v , since_id ) )
}
pub fn mentions_new ( v : & Value , since_id : & str ) -> ( String , Vec < ( String , String , String ) > ) {
let names : std ::collections ::HashMap < String , String > = v [ " includes " ] [ " users " ] . as_array ( ) . into_iter ( ) . flatten ( ) . map ( | u | ( u [ " id " ] . as_str ( ) . unwrap_or ( " " ) . to_string ( ) , u [ " username " ] . as_str ( ) . unwrap_or ( " " ) . to_string ( ) ) ) . collect ( ) ;
let newest = v [ " meta " ] [ " newest_id " ] . as_str ( ) . unwrap_or ( since_id ) . to_string ( ) ;
let mut items : Vec < ( String , String , String ) > = v [ " data " ]
. as_array ( )
. into_iter ( )
. flatten ( )
. map ( | t | ( t [ " id " ] . as_str ( ) . unwrap_or ( " " ) . to_string ( ) , names . get ( t [ " author_id " ] . as_str ( ) . unwrap_or ( " " ) ) . cloned ( ) . unwrap_or_default ( ) , t [ " text " ] . as_str ( ) . unwrap_or ( " " ) . to_string ( ) ) )
. collect ( ) ;
items . reverse ( ) ;
( newest , items )
}
// ---------- what bots can use ----------
/// The configured accounts (set when the channels start): (Twilio token, Twilio number), X token.
#[ derive(Clone, Default) ]
pub struct Accounts {
pub twilio : Option < ( String , String ) > ,
pub x : Option < String > ,
}
pub fn accounts ( ) -> & 'static Mutex < Accounts > {
static A : OnceLock < Mutex < Accounts > > = OnceLock ::new ( ) ;
A . get_or_init ( | | Mutex ::new ( Accounts ::default ( ) ) )
}
pub fn specs ( ) -> Vec < Value > {
let a = accounts ( ) . lock ( ) . unwrap ( ) . clone ( ) ;
let mut v = vec! [ ] ;
if a . twilio . is_some ( ) {
v . push ( json! ( { " name " : " place_call " , " description " : " Phone someone from the person's own number and read a short message aloud (Traditional Chinese voice). The person is asked every time. " ,
" parameters " : { " type " : " object " , " properties " : { " to " : { " type " : " string " , " description " : " Phone number in international format, e.g. +886912345678 " } , " message " : { " type " : " string " } } , " required " : [ " to " , " message " ] } } ) ) ;
v . push ( json! ( { " name " : " send_sms " , " description " : " Send a text message from the person's own number. The person is asked every time. " ,
" parameters " : { " type " : " object " , " properties " : { " to " : { " type " : " string " } , " text " : { " type " : " string " } } , " required " : [ " to " , " text " ] } } ) ) ;
}
if a . x . is_some ( ) {
v . push ( json! ( { " name " : " post_to_x " , " description " : " Post on X (Twitter) as the person, optionally as a reply. At most 280 characters. The person is asked every time. " ,
" parameters " : { " type " : " object " , " properties " : { " text " : { " type " : " string " } , " reply_to " : { " type " : " string " , " description " : " Post id to reply to (optional) " } } , " required " : [ " text " ] } } ) ) ;
}
v
}
pub fn is_phone_tool ( name : & str ) -> bool {
matches! ( name , " place_call " | " send_sms " | " post_to_x " )
}
/// What the person is asked.
pub fn describe ( name : & str , args : & str ) -> String {
let v = crate ::tools ::args_of ( args ) ;
let s = | k : & str | v [ k ] . as_str ( ) . unwrap_or ( " " ) . to_string ( ) ;
match name {
" place_call " = > format! ( " 打電話給 {} ,唸:「 {} 」 " , s ( " to " ) , s ( " message " ) ) ,
" send_sms " = > format! ( " 傳簡訊給 {} :「 {} 」 " , s ( " to " ) , s ( " text " ) ) ,
" post_to_x " = > format! ( " 用你的帳號在 X 發文 {} :「 {} 」 " , if s ( " reply_to " ) . is_empty ( ) { String ::new ( ) } else { " (回覆) " . into ( ) } , s ( " text " ) ) ,
_ = > String ::new ( ) ,
}
}
pub fn run ( name : & str , args : & str ) -> ( String , bool ) {
let v = crate ::tools ::args_of ( args ) ;
let s = | k : & str | v [ k ] . as_str ( ) . unwrap_or ( " " ) . trim ( ) . to_string ( ) ;
let a = accounts ( ) . lock ( ) . unwrap ( ) . clone ( ) ;
let r = match ( name , & a . twilio , & a . x ) {
( " place_call " , Some ( ( t , from ) ) , _ ) = > call ( t , from , & s ( " to " ) , & s ( " message " ) ) ,
( " send_sms " , Some ( ( t , from ) ) , _ ) = > send_sms ( t , from , & s ( " to " ) , & s ( " text " ) ) . map ( | _ | format! ( " 已傳簡訊給 {} 。 " , s ( " to " ) ) ) ,
( " post_to_x " , _ , Some ( t ) ) = > x_post ( t , & s ( " text " ) , & s ( " reply_to " ) ) . map ( | u | format! ( " 已發文: {u} " ) ) ,
_ = > Err ( " 這個帳號還沒設定(設定 › 訊息管道) " . into ( ) ) ,
} ;
match r {
Ok ( t ) = > ( t , true ) ,
Err ( e ) = > ( e , false ) ,
}
}
#[ cfg(test) ]
mod tests {
use super ::* ;
use std ::io ::{ Read , Write } ;
Clippy is clean on macOS and Linux, for the app and the relay
Every warning `cargo clippy --all-targets` gave (50 on macOS, 8 more only on
Linux, 1 in the relay), fixed without changing what anything does:
- long types get a name (Peer, Members, Imported, Joining, Mention, Stream…);
- the two big enum variants are boxed (the vault's login plan, joining an
account);
- a test now waits for the copy it relaunched (it left a zombie behind);
- a hint's format had a dead `if` whose two branches were both "";
- the rest is what clippy suggests: sort_by_key, as_chunks, is_none_or,
is_multiple_of, while let, from_ref, next_back, struct init, no casts to
the same type.
Checked: cargo test (177 macOS, 181 Linux), relay tests (11), remote_e2e
28/28, and the live Linux test (X11; Wayland clicks, keys, Chinese typing,
scroll and screenshots).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha2RVfSYmrhqNnqx9Z4
2026-11-07 02:10:28 +00:00
/// Requests the stand-in got: (request line, Authorization header, body).
type Requests = std ::sync ::Arc < Mutex < Vec < ( String , String , String ) > > > ;
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
/// A stand-in for Twilio and X: records requests, answers like they do.
Clippy is clean on macOS and Linux, for the app and the relay
Every warning `cargo clippy --all-targets` gave (50 on macOS, 8 more only on
Linux, 1 in the relay), fixed without changing what anything does:
- long types get a name (Peer, Members, Imported, Joining, Mention, Stream…);
- the two big enum variants are boxed (the vault's login plan, joining an
account);
- a test now waits for the copy it relaunched (it left a zombie behind);
- a hint's format had a dead `if` whose two branches were both "";
- the rest is what clippy suggests: sort_by_key, as_chunks, is_none_or,
is_multiple_of, while let, from_ref, next_back, struct init, no casts to
the same type.
Checked: cargo test (177 macOS, 181 Linux), relay tests (11), remote_e2e
28/28, and the live Linux test (X11; Wayland clicks, keys, Chinese typing,
scroll and screenshots).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha2RVfSYmrhqNnqx9Z4
2026-11-07 02:10:28 +00:00
fn fake ( ) -> ( u16 , Requests ) {
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
let l = std ::net ::TcpListener ::bind ( " 127.0.0.1:0 " ) . unwrap ( ) ;
let port = l . local_addr ( ) . unwrap ( ) . port ( ) ;
let log = std ::sync ::Arc ::new ( Mutex ::new ( vec! [ ] ) ) ;
let lg = log . clone ( ) ;
std ::thread ::spawn ( move | | {
for c in l . incoming ( ) . flatten ( ) {
let mut c = c ;
let mut got = vec! [ ] ;
let mut buf = [ 0 u8 ; 8192 ] ;
loop {
let n = c . read ( & mut buf ) . unwrap_or ( 0 ) ;
got . extend_from_slice ( & buf [ .. n ] ) ;
let t = String ::from_utf8_lossy ( & got ) . to_string ( ) ;
if let Some ( ( h , b ) ) = t . split_once ( " \r \n \r \n " ) {
let len : usize = h . lines ( ) . find_map ( | l | l . to_lowercase ( ) . strip_prefix ( " content-length: " ) . map ( | v | v . trim ( ) . parse ( ) . unwrap_or ( 0 ) ) ) . unwrap_or ( 0 ) ;
if b . len ( ) > = len | | n = = 0 {
break ;
}
}
if n = = 0 {
break ;
}
}
let t = String ::from_utf8_lossy ( & got ) . to_string ( ) ;
let ( head , body ) = t . split_once ( " \r \n \r \n " ) . unwrap_or ( ( & t , " " ) ) ;
let line = head . lines ( ) . next ( ) . unwrap_or ( " " ) . to_string ( ) ;
let auth = head . lines ( ) . find_map ( | l | l . strip_prefix ( " Authorization: " ) . or ( l . strip_prefix ( " authorization: " ) ) ) . unwrap_or ( " " ) . to_string ( ) ;
lg . lock ( ) . unwrap ( ) . push ( ( line . clone ( ) , auth , body . to_string ( ) ) ) ;
let resp = if line . contains ( " /Messages.json " ) & & line . starts_with ( " GET " ) {
json! ( { " messages " : [ { " sid " : " SM2 " , " direction " : " inbound " , " from " : " +886900000002 " , " body " : " 第二則 " } , { " sid " : " SM9 " , " direction " : " outbound-api " , " from " : " +1555 " , " body " : " 我發的 " } , { " sid " : " SM1 " , " direction " : " inbound " , " from " : " +886900000001 " , " body " : " 第一則 " } ] } )
} else if line . contains ( " /Calls.json " ) {
json! ( { " sid " : " CA123 " } )
} else if line . contains ( " /users/me " ) {
json! ( { " data " : { " id " : " 42 " , " username " : " daniel " } } )
} else if line . contains ( " /tweets " ) {
json! ( { " data " : { " id " : " 777 " , " text " : " x " } } )
} else {
json! ( { " sid " : " SMx " , " friendly_name " : " My account " } )
} ;
let b = resp . to_string ( ) ;
let _ = c . write_all ( format! ( " HTTP/1.1 201 Created \r \n Content-Type: application/json \r \n Content-Length: {} \r \n Connection: close \r \n \r \n {b} " , b . len ( ) ) . as_bytes ( ) ) ;
}
} ) ;
( port , log )
}
#[ test ]
fn twilio_and_x_requests_look_right ( ) {
let ( port , log ) = fake ( ) ;
std ::env ::set_var ( " ANYBOT_TWILIO_API " , format! ( " http://127.0.0.1: {port} " ) ) ;
std ::env ::set_var ( " ANYBOT_X_API " , format! ( " http://127.0.0.1: {port} " ) ) ;
let tok = " AC123:secret " ;
assert! ( twilio_test ( tok ) . unwrap ( ) . contains ( " My account " ) ) ;
send_sms ( tok , " +15550001 " , " +886912345678 " , " 你好 & 再見 " ) . unwrap ( ) ;
let out = call ( tok , " +15550001 " , " +886912345678 " , " 會議改到 <三點> " ) . unwrap ( ) ;
assert! ( out . contains ( " CA123 " ) ) ;
let mut seen = Default ::default ( ) ;
let list = twilio_get ( tok , " Messages.json?To=%2B15550001&PageSize=20 " ) . unwrap ( ) ;
let new = sms_new ( & list , & mut seen ) ;
assert_eq! ( new . iter ( ) . map ( | m | m . 2. as_str ( ) ) . collect ::< Vec < _ > > ( ) , vec! [ " 第一則 " , " 第二則 " ] ) ;
assert! ( sms_new ( & list , & mut seen ) . is_empty ( ) ) ;
assert_eq! ( x_me ( " xtok " ) . unwrap ( ) . 1 , " daniel " ) ;
assert_eq! ( x_post ( " xtok " , " 嗨 " , " 555 " ) . unwrap ( ) , " https://x.com/i/status/777 " ) ;
let log = log . lock ( ) . unwrap ( ) ;
let sms = log . iter ( ) . find ( | r | r . 0. starts_with ( " POST " ) & & r . 0. contains ( " Messages.json " ) ) . unwrap ( ) ;
assert! ( sms . 0. contains ( " /2010-04-01/Accounts/AC123/Messages.json " ) ) ;
assert_eq! ( sms . 1 , format! ( " Basic {} " , base64 ::engine ::general_purpose ::STANDARD . encode ( " AC123:secret " ) ) ) ;
assert! ( sms . 2. contains ( " To=%2B886912345678 " ) & & sms . 2. contains ( " Body=%E4%BD%A0%E5%A5%BD%20%26%20 " ) ) ;
let callr = log . iter ( ) . find ( | r | r . 0. contains ( " Calls.json " ) ) . unwrap ( ) ;
assert! ( callr . 2. contains ( " Twiml= " ) & & callr . 2. contains ( " %26lt%3B " ) , " TwiML must be escaped: {} " , callr . 2 ) ;
let tw = log . iter ( ) . find ( | r | r . 0. contains ( " /2/tweets " ) ) . unwrap ( ) ;
assert_eq! ( tw . 1 , " Bearer xtok " ) ;
let body : Value = serde_json ::from_str ( & tw . 2 ) . unwrap ( ) ;
assert_eq! ( body [ " reply " ] [ " in_reply_to_tweet_id " ] , " 555 " ) ;
}
#[ test ]
fn mentions_come_oldest_first_with_names ( ) {
let v = json! ( { " data " : [ { " id " : " 9 " , " author_id " : " 1 " , " text " : " @daniel 新的 " } , { " id " : " 8 " , " author_id " : " 2 " , " text " : " @daniel 舊的 " } ] , " includes " : { " users " : [ { " id " : " 1 " , " username " : " amy " } , { " id " : " 2 " , " username " : " bob " } ] } , " meta " : { " newest_id " : " 9 " } } ) ;
let ( newest , items ) = mentions_new ( & v , " 7 " ) ;
assert_eq! ( newest , " 9 " ) ;
assert_eq! ( items [ 0 ] , ( " 8 " . into ( ) , " bob " . into ( ) , " @daniel 舊的 " . into ( ) ) ) ;
assert_eq! ( mentions_new ( & json! ( { " meta " : { " result_count " : 0 } } ) , " 9 " ) . 0 , " 9 " ) ;
}
}