Coder Social home page Coder Social logo

inb's People

Contributors

josephlimtech avatar joshiayush avatar ologbonowiwi avatar panquesito7 avatar schmelto avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

inb's Issues

Could we have keywords as a multiple parameter?

I noticed that keywords could be something we can use with multiple values.

For example, if I want to reach out to recruiters, I could use the following:

--keywords "Tech Recruiter" --keywords "Recruiter" --keywords "Talent Acquisition"

What do you think?

Implement custom `find_child_*` functions to target child elements in the `DOM`

Description

entity_result_item_container = li.find_element_by_tag_name(
"div").find_element_by_css_selector(
"div[class='entity-result__item']")
entity_result_image_container = entity_result_item_container.find_element_by_css_selector(
"div[class='entity-result__image']")
entity_result_anchor_tag = entity_result_image_container.find_element_by_tag_name(
"a")
person_profile_url = entity_result_anchor_tag.get_attribute(
"href")
entity_result_img_tag = entity_result_image_container.find_element_by_xpath(
"//div/a/div[1]/div[1]/img[1]")
person_photo_url = entity_result_img_tag.get_attribute("src")
entity_result_content_container = entity_result_item_container.find_element_by_css_selector(
"div[class^='entity-result__content']")
try:
person_occupation = entity_result_content_container.find_element_by_css_selector(
"div[class^='entity-result__primary-subtitle']").text
except NoSuchElementException:
person_occupation = None
try:
person_location = entity_result_content_container.find_element_by_css_selector(
"div[class^='entity-result__secondary-subtitle']").text
except NoSuchElementException:
person_location = None
try:
person_summary = entity_result_content_container.find_element_by_css_selector(
"p[class^='entity-result__summary']").text
for extr_wrd in person_summary_extra_words:
indx = person_summary.find(extr_wrd)
if indx > -1:
person_summary = person_summary[len(
extr_wrd) + indx + 1::]
person_summary = person_summary.strip()
except NoSuchElementException:
person_summary = None
entity_result_content_anchor_tag = entity_result_content_container.find_element_by_tag_name(
"a")
person_name = entity_result_content_anchor_tag.text
# the above line will give a result something like this:
#
# Ayush Joshi\nView Ayush Joshi’s profile
#
# and we don't want any thing except for the name so we split the result
# at '\n' and take out the first element of the resulting list
person_name = person_name.split('\n')[0]
person_connect_button = entity_result_item_container.find_element_by_css_selector(
"div[class^='entity-result__actions']").find_element_by_tag_name("button")

We need to change the way we target elements in the DOM. Look at the above code it's so messy and probably need some refactoring. If we do not figure out a more systematic way of finding elements in the DOM then this code is bound to be labelled as legacy tommorow. Chained function calls lying around that way makes code less readable and messy. Probably we need to build some kind of abstraction around it that will just take in the selectors for selecting elements in the DOM and will return WebElement if found.

Possible solutions

  • find_child_by_css_selector(parent: Union[str, WebElement], child: str)

    def find_child_by_css_selector(
            parent: Union[str, webdriver.WebElement],
            child: str) -> webdriver.WebElement:
        """Function returns the child of the given parent element.
    
        This function takes in a 'parent' instance whether in a string format means parent's css
        selector or in form of a webdriver.WebElement instance and returns the child located at
        the given css selector i.e., 'child'.
    
        Note: In case the element or the parent is not present anymore in the DOM then this
        function will not prevent the built-in find_element_by_css_selector() function by raising
        NoSuchElementException.
    
        Args:
          parent (Union[str, WebElement]): Either css selector of the parent or the parent's
            webdriver.WebElement instance itself. 
          child (str): CSS selector of the child. 
    
        Returns:
          webdriver.WebElement: The requested child, WebElement instance returned by the built-in 
            find_element_by_css_selector() function.
    
        Raises:
          NoSuchElementException: Default NoSuchElementException behaviour of function 
            find_element_by_css_selector().
    
        >>> child = find_child_by_css_selector(
        ...                "div[class='discover-entity-type-card__info-container']", "a")
        """
        pass
  • find_child_by_tag_name(parent: Union[str, WebElement], child: str)

    def find_child_by_tag_name(
            parent: Union[str, webdriver.WebElement],
            child: str) -> webdriver.WebElement:
        """Function returns the child of the given parent element.
    
        This function takes in a 'parent' instance whether in a string format means parent's tag
        name or in form of a webdriver.WebElement instance and returns the child by the given
        tag name i.e., 'child'.
    
        Note: In case the element or the parent is not present anymore in the DOM then this
        function will not prevent the built-in find_element_by_tag_name() function by raising
        NoSuchElementException.
    
        Args:
          parent (Union[str, WebElement]): Either tag name of the parent or the parent's
            webdriver.WebElement instance itself. 
          child (str): Tag name of the child. 
    
        Returns:
          webdriver.WebElement: The requested child, WebElement instance returned by the built-in 
            find_element_by_tag_name() function.
    
        Raises:
          NoSuchElementException: Default NoSuchElementException behaviour of function 
            find_element_by_tag_name().
    
        >>> child = find_child_by_tag_name("div", "a")
        """
        pass
  • find_child_by_xpath(parent: Union[str, WebElement], child: str)

    def find_child_by_xpath(
            parent: Union[str, webdriver.WebElement],
            child: str) -> webdriver.WebElement:
        """Function returns the child of the given parent element.
    
        This function takes in a 'parent' instance whether in a string format means parent's xpath
        or in form of a webdriver.WebElement instance and returns the child located at the given
        xpath i.e., 'child'.
    
        Note: In case the element or the parent is not present anymore in the DOM then this
        function will not prevent the built-in find_element_by_xpath() function by raising
        NoSuchElementException.
    
        Args:
          parent (Union[str, WebElement]): Either xpath of the parent or the parent's
            webdriver.WebElement instance itself.
          child (str): Xpath of the child. 
    
        Returns:
          webdriver.WebElement: The requested child, WebElement instance returned by the built-in 
            find_element_by_xpath() function.
    
        Raises:
          NoSuchElementException: Default NoSuchElementException behaviour of function 
            find_element_by_xpath().
    
        >>> child = find_child_by_xpath(
        ...                '//*[@id="main"]/div/div/div[2]//a[text()="Try Premium Free For 1 Month"]',
        ...                "//div/a/div[1]/div[1]/img[1]")
        """
        pass

Add a custom message

Is it possible to include a custom message when using the bot? As per the screenshot...

Screenshot_2021-10-08_15-53-50

Add functionality for `show` command

Description

def show(self: Command) -> None:
"""Method `show()` prints user(s) information stored in database.
This feature is not programed yet. Contributions are welcome.
Args:
self: (Command) Self.
Example:
>>> from argparse import Namespace
>>> from inbparser import Command
>>>
>>> namespace = Namespace(which='show', keyword='email', ...)
>>> command = Command(namespace)
>>> command.show()
>>> # -> It should print email(s) stored in database
"""
self.logger.info("facility not present")

As of now command show which is meant to show the information of user(s) stored in the database (database is also not set yet) does not work. It will be a good feature to implement in conjunction with config and delete command to give user's ability to store their email and password in a database so they don't have to type their email and password every time they want to automate a LinkedIn session.

show command will be helpful to read out the database and dump the content of the database on the console or any raw file in case user want that.

Possible solutions

  1. Try to set up a database probably in inb/database, (use SQLite).
  2. Check if database exists or not.
  3. Independent of the platform, check if we have read permissions or not.
  4. Either dump the database or throw PERM error depending on the step 3.

Consider writing design documents for `inb`

Description

Leaving on a long road trip without using a map is going to waste a lot of valuable time even if we get there eventually and I feel this is the same what we are doing. Project inb is currently in its beta state so before proposing any change we must write design document on it.

Possible solutions

Add software design description inside of the docs/project folder.

Things to add:

  1. Title, author and reviewers.
    1. Title: Title of the project with a description.
    2. Author: Author of the document not the code.
    3. Reviewers: Reviewers of the document not the code.
  2. Functional description.
    1. What does the software do?
    2. Start-up procedure.
    3. Exception handling.
    4. Limitations.
  3. Command line interface.
  4. Goals and Milestones.
  5. Prioritization.
  6. Current and proposed solutions.

Resources

  1. What Is A Design Doc In Software Engineering? (Clément Mihailescu)
  2. What is a Design Doc: Software Engineering Best Practice #1 (TechLead)

Add functionality for `config` command

Description

def config(self: Command) -> None:
"""Method `config()` stores user's information in database.
This feature is not programed yet. Contributions are welcome.
Args:
self: (Command) Self.
Example:
>>> from argparse import Namespace
>>> from inbparser import Command
>>>
>>> namespace = Namespace(which='config', EMAIL='[email protected]', PASSWORD='xxx-xxx-xxx')
>>> command = Command(namespace)
>>> command.config()
>>> # -> It should update database with new values
"""
self.logger.info("facility not present")

As of now the config command which is meant to create or update entries in the database does not work. It will be a good feature to implement in conjunction with the delete command and before show command.

Possible solutions

  1. Try to set up a database probably in inb/database, (use SQLite).
  2. Check if we have write permissions or not if not then throw PERM error otherwise follow the below steps.
  3. Check if database exists or not, if it does not then create the following table:
    CREATE TABLE database_name.inb_users(
        Email CHAR(50) PRIMARY KEY NOT NULL,
        Password CHAR(50) NOT NULL,
        CreatedAt DATETIME DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')) 
    );
    Note: Database must be created at the top level directory inside of the db folder.
  4. After the 2 step, insert the fields given by the user.

config command should follow the following command line arguments:

$ inb.py config --email "user_email" --password "user_password"

Replace `MIT` License with `BSD 3-Clause` license

Replace MIT license that is at the top of some python file in the repository and replace it with BSD 3-Clause license like the one here and also update the README and the LICENSE file with BSD 3-Clause license.

# Copyright 2021, joshiayus Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# * Neither the name of joshiayus Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import re
import os
import sys
import pathlib
import logging
import subprocess
from urllib import (request, parse)

ResourceWarning: Enable tracemalloc to get the object allocation traceback

Description

Unclosed input/output buffer while testing custom print functions makes python throw ResourceWarning.

Reproduction

  1. Add the following line to test.py file
    from tests.console.print import TestPrintFunctions
  2. Run test script.

Screenshots

Resource Warning Screenshot

Environment

  • Device: laptop
  • OS: Ubuntu
  • Interpreter: python3.7

Resolution

  • I would be interested in resolving this bug.

force argument has no effect on coloured print functions

Description

Argument force has no effect on the coloured print functions where it was supposed to print the text in the given color and style when the force="+f" it doesn't do that. This force argument is to force the coloured print functions to print the given text in the given color and style this functionality is needed when the user turns the theme to normal where every coloured print function just print the given text in the normal style but still we need to highlight some of the information like errors, waiting infos, etc.

Reproduction

  1. Try turning the theme to normal using command theme --normal.
  2. Type a command or flag that is not recognised.
  3. See the problem when the error string is printed in normal text.

Expectation

As the force argument is enabled and the coloured print functions are coded for this functionality it must highlight the text.

Screenshots

Screenshot from 2021-05-28 16-12-16

See it should print the 'whydoesn'tforceargumentworks' is not a 'linkedin' command! in bright red color but it didn't.

Environment

  • Device: laptop
  • OS: Ubuntu
  • Interpreter: python3.7.10
  • Chrome Driver version: 89.0.4389.23
  • Google Chrome Version: 90.0.4430.212 (Official Build) (64-bit)

Resolution

  • I would be interested in resolving this bug.

"Weak Network" error message

The following command:

python3 ~/inb/inb/inb.py search --email "[email protected]" --password "R" --keyword "Team Building" --location "London" --title "Director" --message ~/message-ndp-1.txt limit 1

Returns the following error:

INFO:Checking network status
inb: Error: InternetNotConnectedException: Weak network found

This is on Debian 11 running in an LXD container with 16GB of RAM?

Automatically file an issue using `inb`

Description

Automatically filing an issue from the user's end is a requirement which we must fulfil. An example of the application of this feature is here #32.

Possible solutions

We should probably make use of the PyGithub API to implement this feature.

Steps to follow before filing an issue:

  1. Generate a issue template.
  2. Attach generated logs in the generated issue template.
  3. Mark Assignees.
  4. Attach Labels.
  5. Submit new issue.

Add more information in `search` command's invitation status

Description

Invitation status for search command should display more information than just,

<status>  <full name>
<occupation>
Success: <count>  Failure: <count>  Elapsed time: <count>

We should scrape as much of data as we can to later store in our logs record.

Possible solutions

For search functionality all we need to do is to transform the transform_to_object function inside get_search_results_elements() function to also scrape profile url, degree, location, summary, and shared connections.

def transform_to_object(
lis: List[webdriver.Chrome]) -> List[Person_Info]:
person_infos: List[Person_Info] = []
person_summary_extra_words = [
'Summary', 'Current', 'Past', 'Skills']
for li in lis:
entity_result_item_container = li.find_element_by_tag_name(
"div").find_element_by_css_selector(
"div[class='entity-result__item']")
entity_result_image_container = entity_result_item_container.find_element_by_css_selector(
"div[class='entity-result__image']")
entity_result_anchor_tag = entity_result_image_container.find_element_by_tag_name(
"a")
person_profile_url = entity_result_anchor_tag.get_attribute(
"href")
entity_result_img_tag = entity_result_image_container.find_element_by_xpath(
"//div/a/div[1]/div[1]/img[1]")
person_photo_url = entity_result_img_tag.get_attribute("src")
entity_result_content_container = entity_result_item_container.find_element_by_css_selector(
"div[class^='entity-result__content']")
try:
person_occupation = entity_result_content_container.find_element_by_css_selector(
"div[class^='entity-result__primary-subtitle']").text
except NoSuchElementException:
person_occupation = None
try:
person_location = entity_result_content_container.find_element_by_css_selector(
"div[class^='entity-result__secondary-subtitle']").text
except NoSuchElementException:
person_location = None
try:
person_summary = entity_result_content_container.find_element_by_css_selector(
"p[class^='entity-result__summary']").text
for extr_wrd in person_summary_extra_words:
indx = person_summary.find(extr_wrd)
if indx > -1:
person_summary = person_summary[len(
extr_wrd) + indx + 1::]
person_summary = person_summary.strip()
except NoSuchElementException:
person_summary = None
entity_result_content_anchor_tag = entity_result_content_container.find_element_by_tag_name(
"a")
person_name = entity_result_content_anchor_tag.text
# the above line will give a result something like this:
#
# Ayush Joshi\nView Ayush Joshi’s profile
#
# and we don't want any thing except for the name so we split the result
# at '\n' and take out the first element of the resulting list
person_name = person_name.split('\n')[0]
person_connect_button = entity_result_item_container.find_element_by_css_selector(
"div[class^='entity-result__actions']").find_element_by_tag_name("button")
person_infos.append(
Person_Info(
name=person_name,
occupation=person_occupation,
photo_url=person_photo_url,
profile_url=person_profile_url,
location=person_location,
summary=person_summary,
connect_button=person_connect_button))
return person_infos
return transform_to_object(get_search_results_person_lis())

Later we can display the collected information on the console like the following:

<status>  <full name>  • <degree>
<occupation>
<location>
<summary>
<shared connections>
Profile Id: <id (from the profile url)>
URL: <profile url>
Success: <count>  Failure: <count>  Elapsed time: <count>

Templates

One such template for that could be the following:

SEARCH_INVITATION_STATUS_TEMPL = """    {{status}}  {{full_name}}  • {{degree}}
    {{person_occupation}}
    {{person_location}}
    {{person_summary}}
    {{shared_connections}}
    ID: {{profile_id}}
    URL: {{profile_url}}
    Success:  {{success}}  Failure: {{failure}}  Elapsed time: {{elapsed_time}}\n"""

Note: You should print the next status after the current status rather than clearing up the current status to make room for next status.

Consider logging the stack trace in case there's a `NoSuchElementException`

Description

Because LinkedIn keeps changing its element's selector every now and then, we must capture the stack trace to find out which element's selector has got changed.

Possible solutions

It would be a good feature if we could just implement a logging call that will log the stack trace in case of NoSuchElementException. This logging call must log in a separate file inside of the logs directory inside of the project's root directory.

We also must provide user the ability to automatically file an issue with the generated logs in case inb breaks because of NoSuchElementException.

Automatically file an issue with generated logs

You'll find more details on this issue here #31.

inb.py not providing any output

i have cloned the repo in my local and followed the instruction when i run this script

inb.py search --email [email protected] --password xxxx --keyword 'Software developer' --refresh-cookies

simply it provides the output in the terminal like this

Warning: 'email' is not in the list of known options, but still passed to Electron/Chromium.
Warning: 'password' is not in the list of known options, but still passed to Electron/Chromium.
Warning: 'keyword' is not in the list of known options, but still passed to Electron/Chromium.
Warning: 'refresh-cookies' is not in the list of known options, but still passed to Electron/Chromium.

kindly reslove the issue

Add more information in `send` command's invitation status

Description

Invitation status for send command should display more information than just,

<status>  <full name>
<occupation>
Success: <count>  Failure: <count>  Elapsed time: <count> 

We should scrape as much of data as we can to later store in our logs record.

Possible solutions

For send functionality all we need to do is to transform the transform_to_object() function inside get_suggestion_box_element() function to also scrape profile url, mutual connections.

def transform_to_object(li: webdriver.Chrome) -> Person_Info:
"""Nested function transform_to_object() gets the person's details from the element
that wraps the person and transform into an Person_Info object.
:Args:
- li: {webdriver.Chrome} element to get the person details from
:Returns:
- {Person_Info}
"""
# first target the html elements that holds the actual details of a person
# that we are interested in
info_container = li.find_element_by_css_selector(
"div[class='discover-entity-type-card__info-container']")
anchor_tag = info_container.find_element_by_tag_name("a")
img_tag = anchor_tag.find_element_by_tag_name("img")
bottom_container = li.find_element_by_css_selector(
"div[class^='discover-entity-type-card__bottom-container']")
footer = bottom_container.find_element_by_tag_name("footer")
# target the actual values from the html elements that we get from the above
# operations and store them to later create a Person_Info object with these
# details
person_name = anchor_tag.find_element_by_css_selector(
"span[class^='discover-person-card__name']").text
person_occupation = anchor_tag.find_element_by_css_selector(
"span[class^='discover-person-card__occupation']").text
person_photo_url = img_tag.get_attribute("src")
person_profile_url = "%(profile_path)s" % {
"profile_path": anchor_tag.get_attribute("href")}
person_connect_button = footer.find_element_by_css_selector(
"button[aria-label^='Invite']")
return Person_Info(
name=person_name, occupation=person_occupation,
profile_url=person_profile_url, photo_url=person_photo_url,
connect_button=person_connect_button)

Later we can display the collected information on the console like the following:

<status>  <full name>
<occupation>
<mutual connections>    
ID: <id (from the profile url)>
URL: <profile url>
Success: <count>  Failure: <count>  Elapsed time: <count> 

Templates

One such template for that could be the following:

SEND_INVITATION_STATUS_TEMPL = """    {{status}}  {{full_name}}
    {{person_occupation}}
    {{mutual_connections}}
    ID: {{profile_id}}
    URL: {{profile_url}}
    Success:  {{success}}  Failure: {{failure}}  Elapsed time: {{elapsed_time}}\n"""

Note: You should print the next status after the current status rather than clearing up the current status to make room for next status.

Typo on README.md

There is a typo on the --refresh-cookies parameter on line 139 (it reads --refersh-cookies)

Install `chromedriver` to a local system path and then extend the environment path

Users can not use the bundled version of inb into a single executable unless the chromedriver is served from the environment PATH. We need to install chromedriver inside a local system path and then extend the environment PATH with it so that the chromedriver will be visible to our executable.

def _InstallGoogleChromeCompatibleChromeDriver() -> None:
"""Installs `Google Chrome` compatible `chromedriver`.
This function installs a `Google Chrome` compatible `chromedriver` version.
Because user's can have different versions of `Google Chrome` installed in
their system so we need to handle the case where the `chromedriver` that
comes with the `inb` repository is not compatible with the `Google Chrome`
version they are using on their system.
To handle the above case we install the compatible version of `chromedriver`
from the `googleapis` by calling the function
`_GetPlatformSpecificChromeDriverCompatibleVersionUrl()` to return the URL
for `chromedriver` and then later using that URL with function
`_RetrieveChromeDriverZip()` to install `chromedriver` from `googleapis`.
Once the `chromedriver` is installed we know that it is in a form of zip so
we need to extract it and we do so by calling the function
`_ExtractChromeDriverZip()` with the zip file path.
"""
_RetrieveChromeDriverZip(
_GetPlatformSpecificChromeDriverCompatibleVersionUrl(
_GetGoogleChromeBinaryVersion()),
True if LOGGING_TO_STREAM_ENABLED else False)
_ExtractChromeDriverZip(
os.path.join(_GetInstalledChromeDriverDirectoryPath(),
_CHROME_DRIVER_ZIP_FILE))

The above function should install chromedriver inside a local system path and then extend the environment PATH with it independent of the platform the user is running.

TimeoutException in _GetElementByXPath while running the search Command

Traceback (most recent call last): File "c:\inb\inb2\inb\linkedin\connect\linkedinsearchconnect.py", line 482, in _GetElementByXPath EC.presence_of_element_located((by.By.XPATH, xpath))) File "C:\Users\Work\anaconda3\envs\inb\lib\site-packages\selenium\webdriver\support\wait.py", line 80, in until raise TimeoutException(message, screen, stacktrace) selenium.common.exceptions.TimeoutException: Message:

When unit testing `version` variable inside function `_GetGoogleChromeBinaryVersion()` changes its type to `mock.Mock` :(

When testing fixture TestProtectedGetGoogleChromeBinaryVersionFunction the following exception appears.

======================================================================
ERROR: test_call_to_subprocess_check_output_in_linux (tests.test_linkedin.test_settings.TestProtectedGetGoogleChromeBinaryVersionFunction)
This should test if the call to `subprocess.check_output()` method are
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Python/inb/inb/lib/utils.py", line 142, in wrapper
    func(*args, **kwargs)
  File "/usr/lib/python3.7/unittest/mock.py", line 1256, in patched
    return func(*args, **keywargs)
  File "/Python/inb/inb/tests/test_linkedin/test_settings.py", line 70, in test_call_to_subprocess_check_output_in_linux
    _ = settings._GetGoogleChromeBinaryVersion()  # pylint: disable=protected-access
  File "/Python/inb/inb/linkedin/settings.py", line 287, in _GetGoogleChromeBinaryVersion
    version = re.search(version_regex, version)
  File "/usr/lib/python3.7/re.py", line 185, in search
    return _compile(pattern, flags).search(string)
TypeError: expected string or bytes-like object

----------------------------------------------------------------------

When debugging, it appears that the following variable version that is supposed to contain a string "Google Chrome 97.11.1111.11" changes its type to mock.Mock object which is weird :( and due to which the above exception appears.

version = subprocess.check_output([path, '--version']).decode('utf-8')
except subprocess.CalledProcessError:
logger.error(_CHROME_BINARY_NOT_FOUND_MSG, path)
continue
else:
version = re.search(version_regex, version)
return version.group(0)

For now the solution could be to just ignore TestProtectedGetGoogleChromeBinaryVersionFunction fixture and continue with the rest of the test fixtures :(.

Building image from Docker

when i'm building the image from the readme file it is giving me error!

[+] Building 0.1s (2/2) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 2B 0.0s
=> CANCELED [internal] load .dockerignore 0.0s
=> => transferring context: 0.0s
failed to solve with frontend dockerfile.v0: failed to read dockerfile: open /var/lib/docker/tmp/buildkit-mount2042251597/Dockerfile: no such file or directory

Breaking Change

Description

Screenshot from 2021-11-08 10-12-45

Element Send Now button has changed to No.

Possible solutions

Add corresponding changes to the following lines, instead of targeting element at "//button[@aria-label='Send now']" target the new No button.

send_now = send_invite_modal.find_element_by_xpath(
"//button[@aria-label='Send now']")
ActionChains(self._driver).move_to_element(
send_now).click().perform()

send_now = send_invite_modal.find_element_by_xpath(
"//button[@aria-label='Send now']")
ActionChains(self._driver).move_to_element(
send_now).click().perform()

This code is a bit redundant so try to refactor the above code in a single function call.

Change primary branch from `master` to `main`

There are known issues that seem to be problematic (resuming the whole history if the branch is master; all others are enslaved branches).

If the name is main, all other branches would be side branches instead of enslaved branches. Can you fix that?

Linux issue

I have also tried this package in Linux but it failed to give me any output
i have tried in ubuntu 20.4 and when i run this script

python3 inb.py search --email [email protected] --xxxxx --keyword 'Software Engineer' --refresh-cookies

Still this script not giving any errors !!!

kindly resolve the issue

Deprecate the support of `language_tool_python`

Description

Deprecate the support of language_tool_python for correcting template syntax; we don't need it anymore.

You can remove the following code:

self._enable_language_tool = grammar_check
if self._enable_language_tool:
import language_tool_python
self._language_tool = language_tool_python.LanguageTool(
language=DEFAULT_LANG)

Also,

if self._enable_language_tool:
message = self._language_tool.correct(message)

Also, change the constructor's argument list by removing the grammar_check argument from here and subsequently update the call to Template constructor:

def __init__(
self: Template, message_template: str,
*, var_template: str, grammar_check: bool = True,
use_template: str = None) -> None:

template = Template(self._message_template,
use_template=self._use_template,
var_template=self._var_template,
grammar_check=self._grammar_check)

Also, remove the following lines of code from _install() function in inb.sh.

inb/inb.sh

Lines 62 to 89 in c0073b1

if [ $verbose -eq 0 ]; then
if [ $EUID -eq 0 ]; then
sudo apt update
sudo apt install openjdk-8-jdk openjdk-8-jre
echo "JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64" >>/etc/environment
echo "JRE_HOME=/usr/lib/jvm/java-8-openjdk-amd64/jre" >>/etc/environment
else
echo "Root privileges required to install java!" >&2
exit 1
fi
else
if [ $EUID -eq 0 ]; then
sudo apt update >/dev/null 2>&1
sudo apt install openjdk-8-jdk openjdk-8-jre >/dev/null 2>&1
echo "JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64" >>/etc/environment 2>&1
echo "JRE_HOME=/usr/lib/jvm/java-8-openjdk-amd64/jre" >>/etc/environment 2>&1
else
echo "Root privileges required to install java!" >&2
exit 1
fi
fi
source bin/activate
if [ $verbose -eq 0 ]; then
python3 -c "import language_tool_python; tool = language_tool_python.LanguageTool('en-US')"
else
python3 -c "import language_tool_python; tool = language_tool_python.LanguageTool('en-US')" >/dev/null
fi
deactivate

Make sure to update the requirements.txt as well.

Information Gathering

To make inb more robust, add following features to gather information on LinkedIn inside of the ig module. Once fetched store these values inside of a SQL database.

  • Add functionality to get the contact number (if possible) of the account holder.
  • Add functionality to get the email address (if possible) of the account holder.
  • Add functionality to scrape the interests of the account holder.
  • Add functionality to get the company details of the account holder.
inb/
└── linkedin/
    ├── __init__.py
    └── ig/    # Information gathering modules will go here
        ├── __init__.py

User given `limit` has not effect on `LinkedInSearchConnect`'s send invitation functionality

Description

It has been observed that user's given limit for the search command over the command line has no actual effect on the LinkedInSearchConnect's send invitation functionality.

$ python3 inb/inb.py search --email "[email protected]" --password "F:(:);GVlk\`" --keyword "Software Developer" limit 20

The above limit value is not going to force the search command to limit the invitations to 20 instead search command will fallback to its default behaviour i.e., 40.

Possible solutions

I guess the only possible solution is to debug the program from command.py file to all the way to linkedinsearchconnect.py file.

@_check_net_stat
@_login
def search(self: Command) -> None:
"""Method `search()` initialises an instance of `LinkedInSearchConnect` API.
This instance is then used to send connection request to people whose profile
aligns to the specified criteria.
Args:
self: (Command) Self.
Example:
>>> from argparse import Namespace
>>> from inbparser import Command
>>>
>>> namespace = Namespace(which='search', email='[email protected]'
... password='xxx-xxx-xxx', keyword='Medical',
... location='India', title='', first_name='Mohika', last_name='Negi',
... school=None, industry='Health Economy:Medical:Health Care Industry',
... current_company=None, profile_language='English', limit=1, headless=True,
... debug=False, cookies=False, start_maximized=True, incognito=True)
>>> command = Command(namespace)
>>> command.search()
"""
self.logger.info("Instantiating connection object")
linkedin_search_connect = LinkedInSearchConnect(
self.linkedin.driver, keyword=self.keyword,
location=self.location, title=self.title,
first_name=self.first_name, last_name=self.last_name,
school=self.school, industry=self.industry,
current_company=self.current_company,
profile_language=self.profile_language,
message_template=self.message,
use_template=self.use_template, var_template=self.var_template,
grammar_check=self.grammar_check, limit=self.limit)
self.logger.info("Sending GET request to search results page")
linkedin_search_connect.run()

@_parse_creds
@_parse_inb_opt_params
def _search(
self: InbArgParser, namespace: argparse.Namespace) -> None:
"""Method `search()` parses the `search` command arguments except for user
credentials and optional paramters.
Args:
self: (InbArgParser) Self.
namespace: (argparse.Namespace) Namespace.
"""
# parse the keyword given, we want to set the keyword to NoneType object
# if the keyword is empty, moreover we only go inside of the clause once
# we have confirmed that the keyword is not a NoneType object
if namespace.keyword:
self.keyword = namespace.keyword
else:
self.keyword = None
# parse the location given, we want to set the location to NoneType object
# if the location is empty, moreover we only go inside of the clause once
# we have confirmed that the location is not a NoneType object
if namespace.location:
# if ':' is present in the location string it means that the user gave us
# more than one location separated by ':' and we want to convert the location
# object to a list object filled with the locations
if ':' in namespace.location:
self.location = namespace.location.split(':')
self.location = filter(
lambda location: location, self.location)
else:
self.location = namespace.location
else:
self.location = None
# parse the title given, we want to set the title to NoneType object
# if the title is empty, moreover we only go inside of the clause once
# we have confirmed that the title is not a NoneType object
if namespace.title:
self.title = namespace.title
else:
self.title = None
# parse the first_name given, we want to set the first_name to NoneType object
# if the first_name is empty, moreover we only go inside of the clause once
# we have confirmed that the first_name is not a NoneType object
if namespace.first_name:
self.first_name = namespace.first_name
else:
self.first_name = None
# parse the last_name given, we want to set the last_name to NoneType object
# if the last_name is empty, moreover we only go inside of the clause once
# we have confirmed that the last_name is not a NoneType object
if namespace.last_name:
self.last_name = namespace.last_name
else:
self.last_name = None
# parse the school given, we want to set the school to NoneType object
# if the school is empty, moreover we only go inside of the clause once
# we have confirmed that the school is not a NoneType object
if namespace.school:
self.school = namespace.school
else:
self.school = None
# parse the industry given, we want to set the industry to NoneType object
# if the industry is empty, moreover we only go inside of the clause once
# we have confirmed that the industry is not a NoneType object
if namespace.industry:
# if ':' is present in the industry string it means that the user gave us
# more than one industry separated by ':' and we want to convert the industry
# object to a list object filled with the industries
if ':' in namespace.industry:
self.industry = namespace.industry.split(':')
self.industry = filter(
lambda industry: industry, self.industry)
else:
self.industry = namespace.industry.strip()
else:
self.industry = None
# parse the current_company given, we want to set the current_company to NoneType object
# if the current_company is empty, moreover we only go inside of the clause once
# we have confirmed that the current_company is not a NoneType object
if namespace.current_company:
self.current_company = namespace.current_company
else:
self.current_company = None
# parse the profile_language given, we want to set the profile_language to NoneType object
# if the profile_language is empty, moreover we only go inside of the clause once
# we have confirmed that the profile_language is not a NoneType object
if namespace.profile_language:
# if ':' is present in the profile language string it means that the user gave us
# more than one profile language separated by ':' and we want to convert the profile language
# object to a list object filled with the profile languages
if ':' in namespace.profile_language:
self.profile_language = namespace.profile_language.split(':')
self.profile_language = filter(
lambda language: language, self.profile_language)
else:
self.profile_language = namespace.profile_language
else:
self.profile_language = None
if namespace.message:
self.message = namespace.message
else:
self.message = None
if namespace.template_business:
self.use_template = 'template_business'
elif namespace.template_sales:
self.use_template = 'template_sales'
elif namespace.template_real_estate:
self.use_template = 'template_real_estate'
elif namespace.template_creative_industry:
self.use_template = 'template_creative_industry'
elif namespace.template_hr:
self.use_template = 'template_hr'
elif namespace.template_include_industry:
self.use_template = 'template_include_industry'
elif namespace.template_ben_franklin:
self.use_template = 'template_ben_franklin'
elif namespace.template_virtual_coffee:
self.use_template = 'template_virtual_coffee'
elif namespace.template_common_connection_request:
self.use_template = 'template_common_connection_request'
else:
self.use_template = None
if namespace.force:
self.grammar_check = False
else:
self.grammar_check = True
if namespace.var:
self.var_template = namespace.var
else:
self.var_template = None
# parse the limit given, we want to set the limit to 20 if the limit is
# a string and cannot be converted into an integer with base 10
if namespace.limit:
self.limit = namespace.limit
else:
self.limit = 20

def __init__(
self: LinkedInSearchConnect, driver: webdriver.Chrome, *,
keyword: str, location: Optional[str] = None,
title: Optional[str] = None, first_name: Optional[str] = None,
last_name: Optional[str] = None, school: Optional[str] = None,
industry: Optional[str] = None,
current_company: Optional[str] = None,
profile_language: Optional[str] = None,
message_template: str = None, use_template: str = None,
var_template: str = None, grammar_check: bool = True, limit: int = 40) -> None:

`test_constructor_method_add_argument_internal_calls` fails unexpectedly

The Driver service's method test_constructor_method_add_argument_internall_calls fails unexpectedly with the following error:

======================================================================
ERROR: test_constructor_method_add_argument_internal_calls (tests.test_linkedin.test_driver.TestDriverClass)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/usr/lib/python3.7/unittest/mock.py", line 1256, in patched
    return func(*args, **keywargs)
  File "/Python/inb/inb/tests/test_linkedin/test_driver.py", line 90, in test_constructor_method_add_argument_internal_calls
    Driver.DISABLE_SETUID_SANDBOX, Driver.IGNORE_CERTIFICATE_ERRORS])
  File "/Python/inb/inb/linkedin/__init__.py", line 65, in __init__
    self.enable_webdriver_chrome()
  File "/Python/inb/inb/linkedin/__init__.py", line 83, in enable_webdriver_chrome
    options=self._options)
  File "/Python/inb/lib/python3.7/site-packages/selenium/webdriver/chrome/webdriver.py", line 81, in __init__
    desired_capabilities=desired_capabilities)
  File "/Python/inb/lib/python3.7/site-packages/selenium/webdriver/remote/webdriver.py", line 157, in __init__
    self.start_session(capabilities, browser_profile)
  File "/Python/inb/lib/python3.7/site-packages/selenium/webdriver/remote/webdriver.py", line 252, in start_session
    response = self.execute(Command.NEW_SESSION, parameters)
  File "/Python/inb/lib/python3.7/site-packages/selenium/webdriver/remote/webdriver.py", line 321, in execute
    self.error_handler.check_response(response)
  File "/Python/inb/lib/python3.7/site-packages/selenium/webdriver/remote/errorhandler.py", line 242, in check_response
    raise exception_class(message, screen, stacktrace)
selenium.common.exceptions.WebDriverException: Message: unknown error: Chrome failed to start: exited abnormally
  (Driver info: chromedriver=2.38.552522 (437e6fbedfa8762dec75e2c5b3ddb86763dc9dcb),platform=Linux 5.11.0-34-generic x86_64)

Moreover, it takes more than 60 seconds to run all the tests after adding this particular test

Ran 41 tests in 61.135s

Add functionality for `delete` command

Description

def delete(self: Command) -> None:
"""Method `delete()` deletes user's information from database.
This feature is not programed yet. Contributions are welcome.
Args:
self: (Command) Self.
Example:
>>> from argparse import Namespace
>>> from inbparser import Command
>>>
>>> namespace = Namespace(which='delete', keyword='email') # Not designed yet
>>> command = Command(namespace)
>>> command.delete()
>>> # -> It should remove values from database
"""
self.logger.info("facility not present")

As of now command delete which is meant to delete the user's information from the database or the entire database, does not work. It will be a good feature to implement in conjunction with config command to give user's the ability to create and remove database entries or to remove the entire database with the help of the delete command.

Possible solutions

  1. Check if we have write permissions.
  2. Check if database exists, if yes then follow step 3.
  3. If requested to delete entries, delete the entries using the Python SQLite API using which the database was created.
  4. If requested to delete the entire database, execute rm -f command on the database.

Validate `chromedriver_path` before passing it to `webdriver.Chrome`

inb/inb/linkedin/driver.py

Lines 137 to 138 in d1290b2

if not chromedriver_path:
chromedriver_path = 'chromedriver'

Current version of enable_webdriver_chrome() doesn't validate the chromedriver_path before passing it to the webdriver.Chrome constructor. This may lead to WebdriverException for false driver path. Moreover, we are not checking if the chromedriver binary is present in the executable path or not. We can do these checks and make sure everything is set up before executing further.

Publish image over Docker Hub

Using the project without cloning the repo would be significant, something like docker pull inb and then docker run inb ....

The experience nowadays is already better than needing to install Python manually. However, users still have to clone the repo, which adds friction to the usage and popularization of this tool.

We could publish a docker image straight away, and the users would pull it without cloning the repo or needing the source code to run the project.

as MVP: I can clone/build/publish the image directly under the inb name and update the README.md, adding new instructions to use the project without cloning the repo.

This adds great value already, as we'd have the value for the users to pull from Docker straightaway. The issue with this is that it adds manual work.

We should add a GitHub action to publish the image at every push. I wouldn't say we need to run to do this, as we don't have releases/changes often.

Docker setup

Any chance of us setting up the project with docker instead of manually download and installing with Python?

LinkedInChallengeException

Hi, i am getting LinkedInChallengeException at run time below is the output any fixes for this?

PS D:\PRJ\inb> docker run -it inb search --email [email protected]>--keyword 'Software developer' --refresh-cookies --nofollow Password: Repeat for confirmation: Traceback (most recent call last): File "inb.py", line 190, in <module> Inb() File "/usr/local/lib/python3.8/site-packages/click/core.py", line 1128, in __call__ return self.main(*args, **kwargs) File "/usr/local/lib/python3.8/site-packages/click/core.py", line 1053, in main rv = self.invoke(ctx) File "/usr/local/lib/python3.8/site-packages/click/core.py", line 1659, in invoke return _process_result(sub_ctx.command.invoke(sub_ctx)) File "/usr/local/lib/python3.8/site-packages/click/core.py", line 1395, in invoke return ctx.invoke(self.callback, **ctx.params) File "/usr/local/lib/python3.8/site-packages/click/core.py", line 754, in invoke return __callback(*args, **kwargs) File "inb.py", line 143, in search linkedin = linkedin_api.LinkedIn(email, File "/app/api/linkedin_api.py", line 123, in __init__ self.client.authenticate(username=username, password=password) File "/app/api/client.py", line 174, in authenticate self._fallback_authentication(username, password) File "/app/api/client.py", line 142, in _fallback_authentication raise linkedin_api_exceptions.LinkedInChallengeException( api.exceptions.LinkedInChallengeException: CHALLENGE

`_InstallGoogleChromeCompatibleChromeDriver()` should have the ability to automatically detect correct `chromedriver` version

In the current version of _InstallGoogleChromeCompatibleChromeDriver() the releases are hard coded in function _GetPlatformSpecificChromeDriverUrlForGoogleChromeMajor(). In case the user is using an updated version of Google Chrome then the function will return None resulting the program to crash.

Solution

Implementing our own version of python-chromedriver-autoinstaller will help.

`discover_entity_list()` doesn't seem to work as expected

discover_entity_list()

Method does not return <ul /> elements as expected, it just goes into that while True loop and then not coming out of it.

def discover_entity_list(self: LinkedInConnectionsAuto, xpath: str = "", wait_time: int = 10) -> object:
    """Method discover_entity_list() returns a WebElement that is located at the given xpath.

    :Args:
        - self: {LinkedInConnectionsAuto} object
        - xpath: {str} xpath to the WebElement
        - wait_time: {int} time to wait until the WebElement loads

    :Returns:
        - {WebElement}
    """
    while True:
        try:
            return WebDriverWait(self.driver, wait_time).until(
                expected_conditions.presence_of_element_located(
                    (By.XPATH, xpath)))
        except TimeoutException:
            continue

I've cross checked the xpath, xpath is fine but the problem is once the function goes into that loop it doesn't come out because WebDriverWait is not able to find that element with the given xpath.

Element with that xpath is present in that page in fact it is the element that is present below the invitations box. I don't know why this method suddenly decided to go weird but before today the problem wasn't there it just worked fine.

Deprecate `levenshtein distance` check for identifying `location` given by the user

Description

def levenshtein(string1: str, string2: str) -> int:
"""Function levenshtein() returns the edit distance between the two given strings.
:Args:
- string1: {str} first string.
- string2: {str} second string.
:Returns:
- {int} edit distance.
"""
if not isinstance(string1, str):
raise TypeError(
"levenshtein: %(type)s type object is a invalid argument, requires string!"
% {"type": _type(string1)})
if not isinstance(string2, str):
raise TypeError(
"levenshtein: %(type)s type object is a invalid argument, requires string!"
% {"type": _type(string2)})
# Our optimal solution matrix that will hold the optimal solution
# of converting one string into another
optimal_solution_matrix: list = [
[-1 for i in range(len(string2))] for j in range(len(string1))]
def levenshteinDistanceTopDown(
string1Index: int, string2Index: int) -> int:
"""Function levenshteiDistanceTopDown() is the actual reccursive program to find the edit
distance between two strings.
:Args:
- string1Index: {int} first string index for this reccursive level.
- string2Index: {int} second string index for this reccursive level.
:Returns:
- {int} edit distance.
"""
# Using the parent function string1 variable
nonlocal string1
# Using the parent function string2 variable
nonlocal string2
# Using the parent function optimal_solution_matrix variable
nonlocal optimal_solution_matrix
if string1Index < 0:
# If string1 is NULL, it is all insertion to get string1 to string2
return string2Index + 1
if string2Index < 0:
# If string2 is NULL, it is all insertion to get string2 to string1
return string1Index + 1
if not optimal_solution_matrix[string1Index][string2Index] == -1:
# Return the globally optimized solution if we already have one
return optimal_solution_matrix[string1Index][string2Index]
if string1[string1Index] == string2[string2Index]:
# Character match; no repair needs to take place no addition to distance
optimal_solution_matrix[string1Index][string2Index] = levenshteinDistanceTopDown(
string1Index - 1, string2Index - 1)
# Return the optimized solution
return optimal_solution_matrix[string1Index][string2Index]
# We have a character mismatch. Remember we want to transform string1 into
# string2 and we hold the i'th character of string1 and the j'th character
# of string2
# Deletion:
# Find levenshtein distance of string1[0...(i - 1)] => string2[0...j] i'th
# character of string1 is deleted
delete_cost: int = levenshteinDistanceTopDown(
string1Index - 1, string2Index)
# Insertion:
# Find levenshtein distance of string1[0...j] => string2[0...(j - 1)] we then
# insert string2[j] into string2 to refain string2[0...j]
insert_cost: int = levenshteinDistanceTopDown(
string1Index, string2Index - 1)
# Replace:
# Find levenshtein distance of string1[0...(i -1)] => string2[0...(j - 1)] we
# then insert string2[j] as i'th character of string1 effectively substituting
# it
replace_cost: int = levenshteinDistanceTopDown(
string1Index - 1, string2Index - 1)
# We want to take the minimum of these three costs to fix the problem (we add 1
# to the minimum to symbolize performing the action)
optimal_solution_matrix[string1Index][string2Index] = min(
delete_cost, min(insert_cost, replace_cost)) + 1
# Return the optimized solution
return optimal_solution_matrix[string1Index][string2Index]
# Commence the recurrence to ascertain the globally optimized solution to convert
# string1 into string2
return levenshteinDistanceTopDown(
len(string1) - 1, len(string2) - 1)

It has been observed that function levenshtein() does not do anything useful except for slowing down the element click process when used inside of the check_for_filter() method.

def check_for_filter(filter: str,
filter_dict: Dict[str, webdriver.Chrome],
threshold: float = 80.0) -> None:
"""Nested function check_for_filter() checks if the filter option is present or not.
This function also does some sort of magic using the levenshtein distance algorithm
to predict the filter option in case the filter given is not present on the filters
page.
:Args:
- filter: {str} Filter option to search for.
- filter_dict: {Dict[str, webdriver.Chrome]} Hash-map containing filter one side and
the element to click on, on the other side.
- threshold: {float} Threshold used by levenshtein distance algorithm to predict for
a match in case the filter option is not directly present on the filters page.
"""
nonlocal self
filters_present: List[str] = filter_dict.keys()
def click_overlapped_element(element: webdriver.Chrome) -> None:
"""Nested function click_overlapped_element() fixes the WebdriverException:
Element is not clickable at point (..., ...).
:Args:
- element: {webdriver.Chrome} Element.
"""
nonlocal self
# @TODO: Validate if the current version of this function is efficient
self._driver.execute_script("arguments[0].click();", element)
if isinstance(filter, str):
if filter in filters_present:
click_overlapped_element(filter_dict[filter])
return
for fltr in filters_present:
levenshtein_dis = levenshtein(filter, fltr)
total_str_len = (len(filter) + len(fltr))
levenshtein_dis_percent = (
(total_str_len - levenshtein_dis) / total_str_len) * 100
if levenshtein_dis_percent >= threshold:
click_overlapped_element(filter_dict[fltr])
return
if isinstance(filter, list):
for fltr in filter:
if fltr in filters_present:
click_overlapped_element(filter_dict[fltr])
continue
for _fltr in filters_present:
levenshtein_dis = levenshtein(fltr, _fltr)
total_str_len = (len(fltr) + len(_fltr))
levenshtein_dis_percent = (
(total_str_len - levenshtein_dis) / total_str_len) * 100
if levenshtein_dis_percent >= threshold:
click_overlapped_element(filter_dict[_fltr])
return

The idea behind using levenshtein distance was to make sure that user gets correct results even after making a typo in the query for location command, but it has been observed that the chances of something like that is quite a few or almost none. In cases where user enters a location that is not present in the filters section of LinkedIn, program has to always compute levenshtein distance for the given location and the locations present in the filters section of LinkedIn which really slows down the process.

Possible solutions

Completely delete file levenshtein.py and refactor the function check_for_filter() to the following:

    def check_for_filter(filter: str,
                         filter_dict: Dict[str, webdriver.Chrome]) -> None:
      """Nested function check_for_filter() checks if the filter option is present or not.

      :Args:
          - filter: {str} Filter option to search for.
          - filter_dict: {Dict[str, webdriver.Chrome]} Hash-map containing filter one side and
              the element to click on, on the other side.
      """
      nonlocal self
      filters_present: List[str] = filter_dict.keys()

      def click_overlapped_element(element: webdriver.Chrome) -> None:
        """Nested function click_overlapped_element() fixes the WebdriverException:
        Element is not clickable at point (..., ...).

        :Args:
            - element: {webdriver.Chrome} Element.
        """
        nonlocal self
        # @TODO: Validate if the current version of this function is efficient
        self._driver.execute_script("arguments[0].click();", element)

      if isinstance(filter, str):
        if filter in filters_present:
          click_overlapped_element(filter_dict[filter])
        else
            raise Exception('Given filter "' + filter + '" is not present.')
        return

      if isinstance(filter, list):
        for fltr in filter:
          if fltr in filters_present:
            click_overlapped_element(filter_dict[fltr])
            continue
          else
            raise Exception('Given filter "' + fltr + '" is not present.')
          return

This way we immediately halt the process in case there is an error in the query given by the user.

Feature

A better feature could be to dump the present locations on the console and allow user to select one or more of them to continue the invitation sending process.

Change `README.md` file

Description

Consider writing a README.md file for inb same as the one's shown here. Currently the README.md file is very unattractive and does not show useful information about project inb.

Note: This modification should be done after addressing issue #28. Make a separate branch with a name same as this issue number, propose your changes and create a pull request.

A short description of what needs to be added is defined below:

  1. About the project.
    1. Consider adding a screen shot or a short video of the working of the project.
    2. Give an outline of the project, why exactly we built it and the features it serves.
  2. Technologies used.
  3. Getting started.
    1. Prerequisites.
    2. Installation.
  4. Usage (Give a link to the wiki).
  5. Contributing.
  6. License.
  7. Developer contact.
  8. Acknowledgments.
  9. Maintainership.

Arm64 compatibility

I got the app working before on Arm64... from memory I just needed to install chromium and copy it to the required location...

sudo apt install python3-pip python3-setuptools git chromium get's the dependencies for debian and then sudo cp /usr/bin/chromedriver ~/inb/driver/chromedriver gets an arm64 compatible chromedriver binary in place...

Store `templates` in `SQLite` database

Description

Add message templates in SQL database. Currently it only supports json files lying around the code base; we need a more efficient way to store and fetch the template messages for customizing invitation request because the way we currently store template data will get ugly when the message template database will increase.

{
"template_business": "Hi {{name}},\n\nI'm looking to expand my network with fellow business owners and professionals. I would love to learn about what you do and see\nif there's any way we can support each other.\n\nCheers!",
"template_sales": "Hi {{name}},\n\nI'm looking to connect with like-minded professionals specifically who are on the revenue generating side of things.\n\nLet's connect!",
"template_real_estate": "Hey {{name}},\n\nCame across your profile and saw your work in real estate. I'm reaching out to connect with other like-minded people. Would be\nhappy to make your acquaintance.\n\nHave a good day!",
"template_creative_industry": "Hi {{name}},\n\nLinkedIn showed me your profile multiple times now, so I checked what you do. I really like your work and as we are both in the\ncreative industy - I thought I'll reach out. It's always great to be connected with like-minded individuals, isn't it?\n\n{{my_name}}",
"template_hr": "Hey {{name}},\n\nI hope your week is off to a great start, I noticed we both work in the HR/Employee Experience field together.\n\nI would love to connect with you.",
"template_include_industry": "Hi {{name}},\n\nI hope you're doing great! I'm on a personal mission to grow my connections on LinkedIn, especially in the field of {{industry}}.\nSo even though we're practically strangers, I'd love to connect with you.\n\nHave a great day!",
"template_ben_franklin": "Hi {{name}},\n\nThe Ben Franklin effect - when we do a person a favor, we tend to like them more as a result. Anything I can do for you?\n\nBest, {{my_name}}",
"template_virtual_coffee": "Hi {{name}},\n\nI hope you're doing well. I'm {{my_name}}, {{my_position}} of {{my_company_name}}. We're looking for {{position}} and it would be\ngreat to connect over a 'virtual' coffee/chat and see what we can do together?",
"template_common_connection_request": [
"Hey {{name}},\n\nI notice we share a mutual connection or two & would love to add you to my network of professionals.\n\nIf you're open to that let's connect!",
"Hi {{name}},\n\nI see we have some mutual connections. I always like networking with new people, and thought this would be an easy way for us to\nintroduce ourselves.",
"Hi {{name}},\n\nLife is both long and short. We have quite a few mutual connections. I would like to invite you to join my network on LinkedIn\nplatform. Hopefully, our paths will cross professionally down the line. Until then, wishing you and yours an incredible {{year}}.\n\n{{my_name}}",
"Hi {{name}},\n\nI was looking at your profile and noticed we had a few shared connections. I thought it would be nice to reach out to connect with\nyou and share out networks.\n\nThank you and hope all is well!",
"Hey {{first_name}},\n\nI saw you're based in {{location}} and work on {{keyword}}, I'd love to connect.\n\nThanks, {{my_name}}"
]
}

Possible solutions

The above data should be moved to a SQLite database named message_templates inside the directory db in the top level directory, to do so follow the following steps:

  1. Create a database with name message_templates.db.
  2. Create the following table or extend it if you want to:
    CREATE TABLE database_name.message_templates(
        Id CHAR(50) PRIMARY KEY NOT NULL,
        Template TEXT NOT NULL,
        CreatedAt DATETIME DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW'))
    );

All the database operations must be performed inside of the directory inb/database.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.