Last active
November 14, 2024 10:09
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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