diff --git a/.github/workflows/build_release.yaml b/.github/workflows/build_release.yaml new file mode 100644 index 0000000..935b483 --- /dev/null +++ b/.github/workflows/build_release.yaml @@ -0,0 +1,54 @@ +name: Buid Windows and Linux Executables + +on: + push: + branches: + - main + +jobs: + Windows Build: + runs-on: windows-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.10.6 + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Build Windows Executable + run: pyinstaller windows.spec + + - name: Generate Pre Release + uses: marvinpinto/action-automatic-releases@latest + with: + repo_token: ${{ secrets.TOKEN }} + prerelease: true + files: | + dist/MR.DM.exe + + Linux Build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.10.6 + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Build Linux Executable + run: pyinstaller linux.spec + + - name: Generate Pre Release + uses: marvinpinto/action-automatic-releases@latest + with: + repo_token: ${{ secrets.TOKEN }} + prerelease: true + files: | + dist/MR.DM + diff --git a/README.md b/README.md index ecfb2cb..3d7caa1 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,9 @@ MR.DM will take care of the rest. It will send the message to all the accounts y MR.DM is a desktop application. It is available for Windows, Linux, and Mac. You can download the latest executable version from [github releases](https://github.com/oxlac/mr.dm/releases). Or you can build it from the source with the following instructions. +> [!WARNING] +> You must disable your antivirus before running the executable on windows. For more information, please read [this](https://github.com/pyinstaller/pyinstaller/issues?q=label%3Aantivirus-false-positives+is%3Aclosed) + ### Installation @@ -150,12 +153,12 @@ You can view the status of the messages being sent on the progress screen. MR.DM ## Roadmap -- [ ] Add Accounts that have posted with a specific hashtag. (In Progress) -- [ ] Add Accounts that have interacted with certain hashtags. (In Progress) +- [ ] Add Accounts that have posted with a specific hashtag. +- [ ] Add Accounts that have interacted with certain hashtags. - [ ] Add an account from their Instagram profile link. - [ ] Add accounts that have interacted with a specific post. -- [ ] Ability to send pictures and videos. (Need Contributors) -- [ ] Cleaning up Documentation and Adding Sphinx Autodoc. ( Under Progress) +- [ ] Ability to send pictures and videos. +- [ ] Cleaning up Documentation and Adding Sphinx Autodoc. ( Under Progress by admins) See the [open issues](https://github.com/oxlac/mr.dm/issues) for a full list of proposed features (and known issues). If you have any other ideas, please open an issue and let us know. diff --git a/backend/session.py b/backend/session.py index ddd1fa2..c8c7c16 100644 --- a/backend/session.py +++ b/backend/session.py @@ -1,5 +1,3 @@ -from typing import Self - from instaloader import Instaloader, Profile from selenium import webdriver @@ -16,7 +14,7 @@ class Session: driver: webdriver.Chrome = None username: str = None - def __new__(cls) -> Self: + def __new__(cls): if cls._instance is None: cls._instance = super(Session, cls).__new__(cls) cls._instance.loader = Instaloader() diff --git a/main.py b/main.py index b6bb6dd..e1704d7 100644 --- a/main.py +++ b/main.py @@ -3,9 +3,22 @@ from kivy.uix.screenmanager import ScreenManager from kivymd.app import MDApp - from backend import DatabaseManager, Session from ui.welcomescreen import WelcomeScreen +import sys +import os + + +# Define the path for the icon +if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): + # Running as a bundled executable (PyInstaller) + ICON = os.path.abspath( + os.path.join(os.path.dirname(__file__), "ui/assets/icon.png") + ) + +else: + # Inside a normal Python environment" + ICON = "ui/assets/icon.png" class MyApp(MDApp): @@ -13,7 +26,8 @@ class MyApp(MDApp): :param MDApp: KivyMD App Class :type MDApp: class `MDApp` from `kivymd.app` module - """ + """ + backend_sess = Session() """ Backend Session @@ -24,7 +38,7 @@ class MyApp(MDApp): Title of the application """ - icon = "ui/assets/icon.png" + icon = ICON """ Icon of the application """ @@ -44,6 +58,13 @@ def build(self): # Create Database inspector.create_inspector(Window, sm) DatabaseManager().create_tables() + # dismiss the splash screen + try: + import pyi_splash + + pyi_splash.close() + except Exception: + pass return sm diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..2941d92 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,6 @@ +instaloader==4.10.2 +Kivy==2.2.1 +selenium==4.16.0 +plyer==2.1.0 +kivymd @ git+https://github.com/kivymd/KivyMD.git@master#egg=kivymd +pyinstaller==5.6.2 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index fb39a26..6886772 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ instaloader==4.10.2 Kivy==2.2.1 selenium==4.16.0 plyer==2.1.0 --e git+https://github.com/kivymd/KivyMD.git@master#egg=kivymd +kivymd @ git+https://github.com/kivymd/KivyMD.git@master#egg=kivymd diff --git a/ui/accountselectscreen/accountselectscreen.py b/ui/accountselectscreen/accountselectscreen.py index 410e582..36e9bc9 100644 --- a/ui/accountselectscreen/accountselectscreen.py +++ b/ui/accountselectscreen/accountselectscreen.py @@ -25,6 +25,7 @@ ManualDialog, ) from ui.messagescreen import MessageScreen +from kivy.clock import Clock class AccountSelectScreen(Screen): @@ -66,7 +67,17 @@ class AccountSelectScreen(Screen): """ def __init__(self, **kw): - Builder.load_file("ui/accountselectscreen/accountselectscreen.kv") + if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): + # Running as a bundled executable (PyInstaller) + Builder.load_file( + os.path.abspath( + os.path.join(os.path.dirname(__file__), "accountselectscreen.kv") + ) + ) + else: + # Inside a normal Python environment + Builder.load_file("ui/accountselectscreen/accountselectscreen.kv") + super().__init__(**kw) # add the MDDataTable to the layout self.ids.tablecontainer.add_widget(self.create_datatable(), 1) @@ -104,10 +115,8 @@ def edit_selection(self, *args): """ self.selection = self.table.get_row_checks() if len(self.selection) > 0: - self.ids.next.disabled = False self.ids.Delete.disabled = False else: - self.ids.next.disabled = True self.ids.Delete.disabled = True def verify_delete(self): @@ -129,11 +138,17 @@ def delete_selected(self, widget): """ Removes the selected accounts from the list """ + + def deselect_rows(*args): + self.table.table_data.select_all("normal") + widget.parent.parent.parent.parent.dismiss() for selected in self.selection: for stuff in self.data: if stuff[1] == selected[1]: self.data.remove(stuff) + # remove the check from the table + Clock.schedule_once(deselect_rows) self._update_table() def open_filepicker(self): diff --git a/ui/assets/Splash Screen.png b/ui/assets/Splash Screen.png new file mode 100644 index 0000000..16d51eb Binary files /dev/null and b/ui/assets/Splash Screen.png differ diff --git a/ui/assets/icon.ico b/ui/assets/icon.ico new file mode 100644 index 0000000..8a1b09f Binary files /dev/null and b/ui/assets/icon.ico differ diff --git a/ui/components/confirmmessagedialog.py b/ui/components/confirmmessagedialog.py index 791c400..b1bfca7 100644 --- a/ui/components/confirmmessagedialog.py +++ b/ui/components/confirmmessagedialog.py @@ -57,6 +57,7 @@ class MessageConfirmDialog(MDDialog): def __init__(self, callback, **kwargs): self.callback = callback + self.auto_dismiss = False self.content_cls = MessageConfirmDialogContent() self.ok_button = MDRaisedButton(text="Send Messages") self.cancel_button = MDFlatButton(text="Cancel") diff --git a/ui/messagescreen/messagescreen.py b/ui/messagescreen/messagescreen.py index fcf8237..e34aeb5 100644 --- a/ui/messagescreen/messagescreen.py +++ b/ui/messagescreen/messagescreen.py @@ -1,4 +1,5 @@ -from time import sleep +import os +import sys from typing import Any from kivy.clock import mainthread @@ -8,7 +9,6 @@ from kivymd.uix.button import MDFlatButton from kivymd.uix.dialog import MDDialog from selenium import webdriver -from selenium.webdriver.common.keys import Keys from ui.components import MessageMenu from ui.components.message_components import LinkMessage, PostMessage, TextMessage @@ -39,7 +39,16 @@ class MessageScreen(Screen): """ def __init__(self, data, *args: Any, **kwds: Any) -> Any: - Builder.load_file("ui/messagescreen/messagescreen.kv") + if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): + # Running as a bundled executable (PyInstaller) + Builder.load_file( + os.path.abspath( + os.path.join(os.path.dirname(__file__), "messagescreen.kv") + ) + ) + else: + # Inside a normal Python environment + Builder.load_file("ui/messagescreen/messagescreen.kv") self.data = data return super().__init__(*args, **kwds) @@ -109,6 +118,7 @@ def confirm_send(self): ) self.confirm_send_dialogue.open() + @mainthread def navigate_to_progress(self, *args): """ Navigates to the progress screen @@ -125,68 +135,7 @@ def navigate_to_progress(self, *args): } ) self.manager.add_widget( - ProgressScreen( - name="progress_screen", messages=self.messages, accounts=self.data - ) + ProgressScreen(name="progress", messages=self.messages, accounts=self.data) ) - self.manager.current = "progress_screen" - - def message_loop(self): - """ - This is the main function that deals with sending messages - to each user in the list - """ - for user in self.data: - # compose a new message for the user - self.find_element( - "/html/body/div[2]/div/div/div[2]/div/div/div[1]/div[1]/div[2]/section/div/div/div/div[1]/div/div[1]/div/div[1]/div[2]/div/div" - ).click() - sleep(3) - actions = webdriver.ActionChains(self.session.driver) - actions.send_keys(user[1]) - actions.perform() - sleep(2) - actions = webdriver.ActionChains(self.session.driver) - actions.send_keys(Keys.TAB) - actions.send_keys(Keys.ENTER) - actions.perform() - sleep(2) - actions = webdriver.ActionChains(self.session.driver) - actions.send_keys(Keys.TAB) - actions.send_keys(Keys.ENTER) - actions.perform() - sleep(3) - # start typing the message - actions = webdriver.ActionChains(self.session.driver) - actions.send_keys(self.reel_link) - actions.send_keys(Keys.ENTER) - actions.perform() - sleep(2) - # send the message - actions = webdriver.ActionChains(self.session.driver) - actions.send_keys(self.message) - actions.send_keys(Keys.ENTER) - actions.perform() - sleep(10) - # add the user to the processed list - self.processed_accounts.append(user[0]) - # update the progress bar - self.update_progress() - - # finish the process - self.finish() - - @mainthread - def update_progress(self): - self.ids.progress.value = len(self.processed_accounts) - self.ids.progress_label.text = ( - str(len(self.processed_accounts)) + "/" + str(len(self.data)) - ) - - def finish(self): - self.ids.progress_label.text = "Finished" - self.ids.progress.label.bg_color = (0, 1, 0, 1) - # save the processed accounts to a file - with open("processed_accounts.txt", "w") as f: - for account in self.processed_accounts: - f.write(account + "\n") + self.manager.current = "progress" + self.manager.remove_widget(self) diff --git a/ui/progressscreen/progressscreen.py b/ui/progressscreen/progressscreen.py index e47bdf1..23bc662 100644 --- a/ui/progressscreen/progressscreen.py +++ b/ui/progressscreen/progressscreen.py @@ -1,3 +1,5 @@ +import os +import sys from threading import Thread from time import sleep @@ -40,7 +42,16 @@ class ProgressScreen(Screen): """ def __init__(self, messages, accounts, **kw): - Builder.load_file("ui/progressscreen/progressscreen.kv") + if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): + # Running as a bundled executable (PyInstaller) + Builder.load_file( + os.path.abspath( + os.path.join(os.path.dirname(__file__), "progressscreen.kv") + ) + ) + else: + # Inside a normal Python environment + Builder.load_file("ui/progressscreen/progressscreen.kv") super().__init__(**kw) self.messages = messages self.accounts = accounts @@ -62,7 +73,6 @@ def create_datatable(self): table = MDDataTable( size_hint=(1, 1), use_pagination=True, - rows_num=10, column_data=[ ("S.No", dp(30)), ("Status", dp(30)), @@ -70,6 +80,7 @@ def create_datatable(self): ("Account Source", dp(40)), ], check=True, + rows_num=10, ) self.table = table self.table.bind(on_check_press=self.edit_selection) @@ -81,8 +92,9 @@ def set_table_data(self): Sets the initial data of the MDTable Adds a red clock icon to the status column """ + temp = [] for account in self.accounts: - self.table.add_row( + temp.append( [ account[0], ("clock-outline", [0.8, 0.3, 0.3, 1], "Pending"), @@ -90,6 +102,7 @@ def set_table_data(self): account[2], ] ) + self.table.row_data = temp def edit_selection(self, instance_table, current_row): """ @@ -233,9 +246,6 @@ def start_message_loop(self, *args): """ count = 0 for user in self.accounts: - # reset by reloading to the DM page - self.session.driver.get("https://www.instagram.com/direct/inbox/") - sleep(5) # set the account to processing self.set_account_to_processing(count) # open the user chat @@ -243,7 +253,7 @@ def start_message_loop(self, *args): sleep(3) # Find the user search field and enter the keys self.find_element(SELECTORS["dm_type_username"]).send_keys(user[1]) - # # We press teh bacl key and enter the last key again to fix a bug on insta that causes the account names to not show up + # # We press teh back key and enter the last key again to fix a bug on insta that causes the account names to not show up # self.find_element(SELECTORS["dm_type_username"]).send_keys(Keys.BACKSPACE) # sleep(2) # self.find_element(SELECTORS["dm_type_username"]).send_keys(user[1][-1]) @@ -258,14 +268,26 @@ def start_message_loop(self, *args): sleep(1) for message in self.messages: # start typing the message + self.simulate_human(message["content"]) + # send the message actions = webdriver.ActionChains(self.session.driver) - actions.send_keys(message["content"]) actions.send_keys(Keys.ENTER) actions.perform() sleep(5) # set the account to completed self.set_account_to_completed(count) count += 1 + sleep(30) + + def simulate_human(self, text): + """ + Simulates human typing + """ + for char in text: + sleep(0.2) + actions = webdriver.ActionChains(self.session.driver) + actions.send_keys(char) + actions.perform() @mainthread def wrong_password(self): diff --git a/ui/welcomescreen/welcomescreen.py b/ui/welcomescreen/welcomescreen.py index c94abdf..0a9035e 100644 --- a/ui/welcomescreen/welcomescreen.py +++ b/ui/welcomescreen/welcomescreen.py @@ -1,4 +1,6 @@ import json +import os +import sys from functools import partial from threading import Thread @@ -27,8 +29,16 @@ class WelcomeScreen(Screen): processing: BooleanProperty = BooleanProperty(False) def __init__(self, **kw): - # check if the file is already loaded - Builder.load_file("ui/welcomescreen/welcomescreen.kv") + if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): + # Running as a bundled executable (PyInstaller) + Builder.load_file( + os.path.abspath( + os.path.join(os.path.dirname(__file__), "welcomescreen.kv") + ) + ) + else: + # Inside a normal Python environment + Builder.load_file("ui/welcomescreen/welcomescreen.kv") super().__init__(**kw) def on_pre_enter(self, *args):