2023-05-13 19:58:48 +02:00
|
|
|
open Pusk.Net
|
|
|
|
open Pusk.Utils
|
|
|
|
|
2023-05-14 20:39:24 +02:00
|
|
|
type credentials =
|
|
|
|
{ username : string
|
|
|
|
; password : string
|
|
|
|
}
|
|
|
|
|
2023-05-14 22:52:36 +02:00
|
|
|
let inject_username session_id creds =
|
2023-05-13 22:13:26 +02:00
|
|
|
(* Find username input *)
|
2023-05-14 22:52:36 +02:00
|
|
|
let strat = CSS "input[name='text']" in
|
2023-05-14 01:44:11 +02:00
|
|
|
let input_username =
|
2023-05-14 22:52:36 +02:00
|
|
|
match find session_id strat with
|
|
|
|
| [] -> raise (Any (fmt "Username input not found"))
|
2023-05-13 22:13:26 +02:00
|
|
|
| _ as l ->
|
|
|
|
if List.length l > 1
|
2023-05-14 22:52:36 +02:00
|
|
|
then raise (Any "Too many elements found as the username input")
|
2023-05-13 22:13:26 +02:00
|
|
|
else List.nth l 0
|
|
|
|
in
|
|
|
|
(* Insert the username *)
|
2023-05-14 20:39:24 +02:00
|
|
|
send_keys session_id input_username creds.username;
|
2023-05-16 02:48:48 +02:00
|
|
|
Unix.sleep 2;
|
2023-05-14 20:39:24 +02:00
|
|
|
send_keys session_id input_username Keys.return;
|
2023-05-16 02:48:48 +02:00
|
|
|
Unix.sleep 6
|
2023-05-14 20:39:24 +02:00
|
|
|
;;
|
|
|
|
|
|
|
|
let rec _inject_password session_id creds try_count =
|
|
|
|
if try_count == 0 then raise (Any "Password input not found");
|
2023-05-14 01:44:11 +02:00
|
|
|
let input_password =
|
2023-05-14 22:52:36 +02:00
|
|
|
match find session_id (CSS "input[name='password']") with
|
2023-05-14 20:39:24 +02:00
|
|
|
| [] ->
|
|
|
|
(* Retry to inject username with the second page *)
|
2023-05-15 11:46:45 +02:00
|
|
|
(* NOTE: I think this is only when a email is first sent, so the second
|
|
|
|
* injection NEED to be a username (or phone number)
|
|
|
|
*
|
|
|
|
* Maybe allow user to provides us with username and email and use email
|
|
|
|
* by default and fallback to username in the second attempt *)
|
2023-05-14 22:52:36 +02:00
|
|
|
inject_username session_id creds;
|
2023-05-14 20:39:24 +02:00
|
|
|
_inject_password session_id creds (try_count - 1);
|
|
|
|
None
|
2023-05-14 01:44:11 +02:00
|
|
|
| _ as l ->
|
|
|
|
if List.length l > 1
|
2023-05-14 22:52:36 +02:00
|
|
|
then raise (Any "Too many elements found as the password input")
|
2023-05-14 20:39:24 +02:00
|
|
|
else Some (List.nth l 0)
|
2023-05-14 01:44:11 +02:00
|
|
|
in
|
2023-05-14 20:39:24 +02:00
|
|
|
match input_password with
|
|
|
|
| Some input ->
|
|
|
|
(* Insert password *)
|
|
|
|
send_keys session_id input creds.password;
|
2023-05-16 02:48:48 +02:00
|
|
|
Unix.sleep 2;
|
2023-05-14 20:39:24 +02:00
|
|
|
send_keys session_id input Keys.return;
|
2023-05-16 02:48:48 +02:00
|
|
|
Unix.sleep 6
|
2023-05-14 20:39:24 +02:00
|
|
|
| None -> ()
|
|
|
|
;;
|
|
|
|
|
|
|
|
let inject_password session_id creds = _inject_password session_id creds 1
|
|
|
|
|
2023-05-14 22:37:00 +02:00
|
|
|
let inject_2fa session_id secret input =
|
|
|
|
let code =
|
2023-05-14 22:16:20 +02:00
|
|
|
match secret with
|
|
|
|
| Some seed -> Twostep.TOTP.code ~secret:seed ()
|
2023-05-14 22:52:36 +02:00
|
|
|
| None -> raise (Any "No TOTP code given, but 2FA code required")
|
2023-05-14 22:16:20 +02:00
|
|
|
in
|
2023-05-14 22:37:00 +02:00
|
|
|
(* Insert 2FA code *)
|
|
|
|
send_keys session_id input code;
|
2023-05-16 02:48:48 +02:00
|
|
|
Unix.sleep 2;
|
2023-05-14 22:37:00 +02:00
|
|
|
send_keys session_id input Keys.return;
|
2023-05-16 02:48:48 +02:00
|
|
|
Unix.sleep 10
|
2023-05-14 22:16:20 +02:00
|
|
|
;;
|
|
|
|
|
|
|
|
let login_twitter ctx username password secret =
|
2023-05-15 16:28:24 +02:00
|
|
|
if ctx.debug then print_endline "Login to twitter...";
|
2023-05-14 20:39:24 +02:00
|
|
|
(* Navigate to login page and wait for page loaded*)
|
|
|
|
ignore (navigate ctx.session_id "https://twitter.com/i/flow/login");
|
2023-05-14 01:57:29 +02:00
|
|
|
Unix.sleep 5;
|
2023-05-14 20:39:24 +02:00
|
|
|
let creds = { username; password } in
|
|
|
|
(* Insert the username *)
|
2023-05-15 16:28:24 +02:00
|
|
|
if ctx.debug then print_endline "Type username...";
|
2023-05-14 22:52:36 +02:00
|
|
|
inject_username ctx.session_id creds;
|
2023-05-14 20:39:24 +02:00
|
|
|
(* Find password input *)
|
2023-05-15 16:28:24 +02:00
|
|
|
if ctx.debug then print_endline "Type password...";
|
2023-05-14 20:39:24 +02:00
|
|
|
inject_password ctx.session_id creds;
|
2023-05-14 22:55:18 +02:00
|
|
|
(* Detection and injection of 2FA code if needed *)
|
2023-05-14 22:52:36 +02:00
|
|
|
match find ctx.session_id (CSS "input[name='text']") with
|
|
|
|
| [] -> print_endline "Doesn't use 2FA as no input found"
|
2023-05-14 22:16:20 +02:00
|
|
|
| _ as l ->
|
|
|
|
if List.length l > 1
|
2023-05-14 22:52:36 +02:00
|
|
|
then raise (Any "Too many elements found as 2FA input")
|
2023-05-15 16:28:24 +02:00
|
|
|
else (
|
|
|
|
if ctx.debug then print_endline "Type 2FA code...";
|
|
|
|
inject_2fa ctx.session_id secret (List.nth l 0))
|
2023-05-13 19:58:48 +02:00
|
|
|
;;
|
2023-05-15 11:44:21 +02:00
|
|
|
|
|
|
|
let go_to_profile ctx =
|
2023-05-15 16:28:24 +02:00
|
|
|
if ctx.debug then print_endline "Locate profile button...";
|
2023-05-15 11:44:21 +02:00
|
|
|
let profile_button =
|
|
|
|
match find ctx.session_id (XPath "//a[@data-testid='AppTabBar_Profile_Link']") with
|
|
|
|
| [] -> raise (Any (fmt "Profile button not found"))
|
|
|
|
| _ as l ->
|
|
|
|
if List.length l > 1
|
|
|
|
then raise (Any "Too many profile button found")
|
|
|
|
else List.nth l 0
|
|
|
|
in
|
2023-05-15 16:28:24 +02:00
|
|
|
if ctx.debug then print_endline "Navigate to user replies...";
|
2023-05-15 12:11:59 +02:00
|
|
|
ignore
|
|
|
|
(navigate
|
|
|
|
ctx.session_id
|
2023-05-15 14:29:29 +02:00
|
|
|
(fmt
|
|
|
|
"https://twitter.com%s/with_replies"
|
|
|
|
(get_attribute ctx.session_id profile_button "href")));
|
2023-05-16 02:48:48 +02:00
|
|
|
Unix.sleep 8
|
2023-05-15 11:44:21 +02:00
|
|
|
;;
|
2023-05-15 12:11:59 +02:00
|
|
|
|
2023-05-15 12:48:38 +02:00
|
|
|
let find_latest_tweet ctx =
|
|
|
|
match find ctx.session_id (XPath "//article[@data-testid='tweet']") with
|
|
|
|
| [] -> None
|
|
|
|
| _ as tweets ->
|
2023-05-15 14:29:29 +02:00
|
|
|
(* Get dates attached to each tweets *)
|
|
|
|
let dates =
|
|
|
|
(* When a tweet is a RT, two dates are attached *)
|
|
|
|
List.flatten
|
|
|
|
(List.map
|
|
|
|
(fun tweet ->
|
|
|
|
match find_in_element ctx.session_id (CSS "time[datetime]") tweet with
|
|
|
|
| [] -> raise (Any (fmt "No dates found for tweet '%s'" tweet))
|
|
|
|
| _ as l -> l)
|
|
|
|
tweets)
|
|
|
|
in
|
|
|
|
(* Turn datetime from ISO 8601 format to epoch int *)
|
|
|
|
let datetimes =
|
|
|
|
List.map
|
|
|
|
(fun date ->
|
2023-05-15 14:31:27 +02:00
|
|
|
let time =
|
2024-01-26 15:31:36 +01:00
|
|
|
Core.Time_float.of_string_with_utc_offset
|
2023-05-15 14:29:29 +02:00
|
|
|
(get_attribute ctx.session_id date "datetime")
|
|
|
|
in
|
2024-01-26 15:31:36 +01:00
|
|
|
Float.to_int
|
|
|
|
(Core.Time_float.Span.to_sec (Core.Time_float.to_span_since_epoch time)))
|
2023-05-15 14:29:29 +02:00
|
|
|
dates
|
|
|
|
in
|
2023-05-15 14:56:41 +02:00
|
|
|
(* Returns the most recent date *)
|
2023-05-15 14:29:29 +02:00
|
|
|
Some (List.fold_left max min_int datetimes)
|
2023-05-15 12:48:38 +02:00
|
|
|
;;
|
2023-05-15 15:05:34 +02:00
|
|
|
|
2023-05-15 15:58:26 +02:00
|
|
|
let tweet ctx msg =
|
2023-05-16 13:32:50 +02:00
|
|
|
ignore (navigate ctx.session_id "https://twitter.com/home");
|
|
|
|
Unix.sleep 4;
|
2023-05-15 15:58:26 +02:00
|
|
|
let tweet_area =
|
2023-05-16 03:11:40 +02:00
|
|
|
match find ctx.session_id (CSS "div[data-testid='tweetTextarea_0']") with
|
|
|
|
| [] -> raise (Any (fmt "Tweet area not found"))
|
2023-05-15 15:58:26 +02:00
|
|
|
| _ as l ->
|
2023-05-16 03:11:40 +02:00
|
|
|
if List.length l > 1 then raise (Any "Too many tweet areas found") else List.nth l 0
|
2023-05-15 15:58:26 +02:00
|
|
|
in
|
|
|
|
send_keys ctx.session_id tweet_area msg;
|
2023-05-16 02:48:48 +02:00
|
|
|
Unix.sleep 2;
|
2023-05-15 15:58:26 +02:00
|
|
|
let send_tweet_button =
|
2023-05-16 13:32:50 +02:00
|
|
|
match find ctx.session_id (XPath "//div[@data-testid='tweetButtonInline']") with
|
2023-05-16 03:11:40 +02:00
|
|
|
| [] -> raise (Any (fmt "Tweet button not found"))
|
2023-05-15 15:58:26 +02:00
|
|
|
| _ as l ->
|
|
|
|
if List.length l > 1
|
2023-05-16 03:11:40 +02:00
|
|
|
then raise (Any "Too many tweet button found")
|
2023-05-15 15:58:26 +02:00
|
|
|
else List.nth l 0
|
|
|
|
in
|
|
|
|
click ctx.session_id send_tweet_button;
|
2023-05-16 02:48:48 +02:00
|
|
|
Unix.sleep 8
|
2023-05-15 15:09:24 +02:00
|
|
|
;;
|