Skip to content

Instantly share code, notes, and snippets.

@ruxo
Last active November 14, 2024 10:09
Show Gist options
  • Save ruxo/fe7c7c0e337da9fe57f7782cfcd7443f to your computer and use it in GitHub Desktop.
Save ruxo/fe7c7c0e337da9fe57f7782cfcd7443f to your computer and use it in GitHub Desktop.
This sample project shows how to use .NET Selenium WebDriver to create a Facebook for submitting a post. It is an adaptation from the Python bot https://github.com/darideveloper/facebook-groups-post-bot/tree/master, but with updated algorithm for pos
open System
open PostBot
open FSharp.UMX
let [<Literal>] ChromeFolder = @"C:\Users\user-name\AppData\Local\Google\Chrome\User Data"
Scraper.killChrome()
let driver = Scraper.startBrowser { Scraper.StartBrowserOptions.Default with ChromeFolder = ValueSome ChromeFolder }
driver.Manage().Timeouts().ImplicitWait <- TimeSpan.FromSeconds 3L
let post: Scraper.GroupPostContext = {
Posts = {
Text = % "⭐⭐ Emoticon is supported!! 😁"
Image = ValueNone
// Image = ValueSome """C:\temp\abc.jpg"""
}
Groups = [ % "49XXXXXXXX67" ] // group number here
}
driver |> Scraper.postInGroups3 Scraper.emulateKeyInput post
printfn "Execution ended.... ENTER to quit"
Console.ReadLine() |> ignore
driver.Quit()
driver.Dispose()
module PostBot.Scraper
open System
open System.Diagnostics
open System.Drawing
open System.Threading
open System.Web
open FSharp.UMX
open OpenQA.Selenium
open OpenQA.Selenium.Chrome
open OpenQA.Selenium.Interactions
open TextCopy
open Units
let private run (program: string) (args: string) = Process.Start(program, args).Dispose()
let killChrome() =
printf "\nTry to kill chrome..."
run "taskkill" "/f /im chrome.exe /t"
printfn "Ok"
type StartBrowserOptions = {
ChromeFolder: string voption
DownloadFolder: string voption
Incognito: bool
/// Set proxy in format "server:port"
ProxyServer: string voption
UserAgent: string voption
WindowSize: Size
}
with
static member Default = {
ChromeFolder = ValueNone
DownloadFolder = ValueNone
Incognito = false
ProxyServer = ValueNone
UserAgent = ValueNone
WindowSize = Size(1280, 720)
}
let startBrowser opts =
Environment.SetEnvironmentVariable("WDM_LOG_LEVEL", "0")
Environment.SetEnvironmentVariable("WDM_PRINT_FIRST_LINE", "False")
let options = ChromeOptions()
options.AddArguments [|
"--disable-notifications"
"--disable-infobars"
"--disable-gpu"
"--disable-extensions"
"--disable-low-res-tiling"
"--disable-dev-shm-usage"
"--disable-renderer-backgrounding"
"--disable-background-timer-throttling"
"--disable-backgrounding-occluded-windows"
"--disable-client-side-phishing-detection"
"--disable-crash-reporter"
"--disable-oopr-debug-crash-dump"
"--log-level=3"
"--mute-audio"
"--no-crash-upload"
"--no-sandbox"
"--output=/dev/null"
"--safebrowsing-disable-download-protection"
"--silent"
"--start-maximized"
|]
// enable experimental options
// options.AddExcludedArguments("enable-logging", "enable-automation")
options.AddArgument $"--window-size={opts.WindowSize.Width},{opts.WindowSize.Height}"
// options.AddArgument "--headless=new"
opts.ChromeFolder |> ValueOption.iter (fun folder -> options.AddArgument $"--user-data-dir={folder}")
opts.UserAgent |> ValueOption.iter (fun ua -> options.AddArgument $"--user-agent={ua}")
if opts.DownloadFolder.IsSome then
let folder = opts.DownloadFolder.Value
options.AddUserProfilePreference("download.default_directory", folder)
options.AddUserProfilePreference("download.prompt_for_download", false)
options.AddUserProfilePreference("download.directory_upgrade", true)
options.AddUserProfilePreference("safebrowsing.enabled", true)
options.AddUserProfilePreference("plugins.always_open_pdf_externally", true)
options.AddUserProfilePreference("download.extensions_to_open", "xml")
if opts.Incognito then options.AddArgument "--incognito"
opts.ProxyServer |> ValueOption.iter (fun proxy -> options.AddArgument $"--proxy-server={proxy}")
// more on proxy -> https://github.com/darideveloper/facebook-groups-post-bot/blob/c4acd4d4feec51cd1460a87b6e1b7c43e88dd419/libs/automate.py#L207
ChromeDriver(options)
let private BaseTime = TimeSpan.FromSeconds 2L
/// Call `click` on element by selector
let clickJs selector (driver: WebDriver) =
driver.FindElement(By.CssSelector(selector)).Click()
driver
let setPage2 timeout (page: Uri) (driver: WebDriver) =
timeout |> ValueOption.iter (fun t ->
assert (t > TimeSpan.Zero)
driver.Manage().Timeouts().PageLoad <- t
)
driver.Url <- page.AbsoluteUri
driver
let setPage page = setPage2 ValueNone page
let openTab (driver: WebDriver) =
driver.ExecuteScript("window.open();") |> ignore
driver
let closeTab (driver: WebDriver) =
driver.Close()
driver
let switchToWindow window (driver: WebDriver) =
driver.SwitchTo().Window(window) |> ignore
driver
let wait (time: TimeSpan) (driver: WebDriver) =
Thread.Sleep time
driver
let refreshSelenium2 time_units back_tab driver =
driver |> openTab
|> switchToWindow (driver.WindowHandles |> Seq.last)
|> wait (BaseTime * time_units)
|> closeTab
|> switchToWindow (driver.WindowHandles |> Seq.item back_tab)
|> wait (BaseTime * time_units)
let refreshSelenium driver = driver |> refreshSelenium2 1. 0
let emulateKeyInput data (el: IWebElement) (driver: WebDriver) =
ClipboardService.SetText data
Actions(driver).KeyDown(el, Keys.Control).SendKeys("v").KeyUp(Keys.Control).Perform()
driver
let inline sendData data (el: IWebElement) (driver: WebDriver) =
el.SendKeys(data)
driver
let goBottom selector (driver: WebDriver) =
driver.FindElement(By.CssSelector(selector)).SendKeys(Keys.Control + Keys.End)
driver
let goPageBottom driver = goBottom "body" driver
// ---------------------------- POST IN GROUP ----------------------------
type PostContext = {
Text: string<message>
Image: string voption
}
type GroupPostContext = {
Posts: PostContext
Groups: string<fbGroup> list
}
module Selectors =
let [<Literal>] DisplayInput = """span + [role="button"]"""
let [<Literal>] Input = """div.notranslate._5rpu[role="textbox"]"""
let [<Literal>] AddImage = """input[type="file"][accept^="image/*"]"""
let [<Literal>] Submit = """[aria-label="Post"][role="button"], [aria-label="โพสต์"][role="button"]"""
let postInGroup messageTo group image driver =
let group_uri = group |> FbGroup.validate
driver |> setPage group_uri
|> match image with
| ValueSome image -> sendData image (driver.FindElement <| By.CssSelector Selectors.AddImage)
| ValueNone -> clickJs Selectors.DisplayInput
|> messageTo (driver.FindElement <| By.CssSelector Selectors.Input)
// |> clickJs Selectors.Submit
|> ignore
let postInGroups3 sendMessage ctx driver =
let message = ctx.Posts.Text |> Message.validate
let image = ctx.Posts.Image
let send group = postInGroup (sendMessage message) group image
assert (ctx.Groups.Length > 0)
driver |> send ctx.Groups.Head
for group in ctx.Groups.Tail do
driver |> wait (TimeSpan.FromSeconds 10L) |> send group
let postInGroups ctx =
postInGroups3 sendData ctx
let getAttributes4 selector attribute_name allow_duplicates allow_empty (driver: WebDriver) =
let elems = driver.FindElements(By.CssSelector selector)
let result = seq {
for elem in elems do
let attr = elem.GetAttribute(attribute_name)
if allow_empty || not (String.IsNullOrWhiteSpace attr) then
yield attr
}
if allow_duplicates then result else result |> Seq.distinct
let getAttributes selector attribute_name = getAttributes4 selector attribute_name true true
// ---------------------------- LIST GROUPS ----------------------------
let [<Literal>] private GroupSelector = """.x1yztbdb div[role="article"] a[aria-label="Visit"]"""
let searchGroups keyword driver =
let search_page = Uri $"https://www.facebook.com/groups/search/groups/?q={HttpUtility.UrlEncode(keyword: string)}"
driver |> setPage search_page
|> ignore
let mutable links_num = 0
let mutable tries_count = 0
while tries_count < 3 do
driver |> goPageBottom
|> ignore
// loop until Facebook finishes loading...
let new_links_num = driver.FindElements(By.CssSelector GroupSelector) |> Seq.length
if new_links_num = links_num then
tries_count <- tries_count + 1
else
links_num <- new_links_num
tries_count <- 0
driver |> refreshSelenium |> ignore
driver |> getAttributes GroupSelector "href" |> Seq.toArray
module Units
open System
open FSharp.UMX
[<Measure>] type message
[<Measure>] type path
[<Measure>] type fbGroup
let inline sideEffect f x = f x; x
type Message =
static member validate (s: string<message>) =
let fs = UMX.untag s
assert (fs.Length > 0)
fs
type FbGroup =
static member validate (s: string<fbGroup>) =
let fs = UMX.untag s
assert (fs.Length > 0)
Uri $"https://www.facebook.com/groups/{fs}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment