├── .gitignore ├── LICENSE ├── README.md ├── README.rst ├── client_installer.py ├── log └── dummy ├── main.py ├── pygram ├── __init__.py ├── actionform.py ├── app.py ├── boxtitle.py ├── checkbox.py ├── command.py ├── config.py ├── menu.py ├── menuitem.py ├── pager.py ├── pg_threading.py ├── pubkey.pub └── selectone.py ├── requirements └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | .idea/ 6 | # Vim 7 | *.swp 8 | # Vim Session Files 9 | *.vim 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | env/ 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | *_local* 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *,cover 52 | .hypothesis/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | 61 | # Sphinx documentation 62 | docs/_build/ 63 | 64 | # PyBuilder 65 | target/ 66 | 67 | #Ipython Notebook 68 | .ipynb_checkpoints 69 | 70 | # Created by https://www.gitignore.io/api/pycharm 71 | 72 | ### PyCharm ### 73 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 74 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 75 | 76 | # User-specific stuff: 77 | .idea/workspace.xml 78 | .idea/tasks.xml 79 | .idea/dictionaries 80 | .idea/vcs.xml 81 | .idea/jsLibraryMappings.xml 82 | 83 | # Sensitive or high-churn files: 84 | .idea/dataSources.ids 85 | .idea/dataSources.xml 86 | .idea/dataSources.local.xml 87 | .idea/sqlDataSources.xml 88 | .idea/dynamic.xml 89 | .idea/uiDesigner.xml 90 | 91 | # Gradle: 92 | .idea/gradle.xml 93 | .idea/libraries 94 | 95 | # Mongo Explorer plugin: 96 | .idea/mongoSettings.xml 97 | 98 | ## File-based project format: 99 | *.iws 100 | 101 | ## Plugin-specific files: 102 | 103 | # IntelliJ 104 | /out/ 105 | 106 | # mpeltonen/sbt-idea plugin 107 | .idea_modules/ 108 | 109 | # JIRA plugin 110 | atlassian-ide-plugin.xml 111 | 112 | # Crashlytics plugin (for Android Studio and IntelliJ) 113 | com_crashlytics_export_strings.xml 114 | crashlytics.properties 115 | crashlytics-build.properties 116 | fabric.properties 117 | .env 118 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Barbaros Yıldırım 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pygram 2 | Telegram messaging from your terminal. 3 | 4 | ## Setup 5 | #### In hard way 6 | At first tg-cli must be installed. 7 | ```bash 8 | git clone --recursive https://github.com/vysheng/tg.git ~/.tg && cd ~/.tg 9 | ./configure 10 | make 11 | ``` 12 | #### More basic 13 | Just run the following. 14 | ```bash 15 | python client_installer.py 16 | ``` 17 | 18 | After installing client package, must be called ones to save login information on local folder, all explained on client readme file 19 | ```bash 20 | .~/.tg/bin/telegram-cli server.pub 21 | ``` 22 | on coming screen, you should type your phone number then received activation code on your phone. then just type safe_quit or just quit 23 | 24 | python 3.5.1 used. 25 | 26 | ## Installing 27 | there are two way, first; 28 | ```bash 29 | git clone https://github.com/RedXBeard/pygram.git 30 | cd pygram;pip3.5 install -r requirements 31 | ``` 32 | second; 33 | ```bash 34 | pip3.5 install -e /path/to/cloned/repo/ 35 | ``` 36 | 37 | ## Usage 38 | Because of there are two ways to install, there are two ways to use, first; 39 | ```bash 40 | python3.5 main.py 41 | ``` 42 | second, much more easy; 43 | ```bash 44 | pygram 45 | ``` 46 | ## Collaborators 47 | - Barbaros Yıldırım ([RedXBeard](https://github.com/RedXBeard)) 48 | - Barış Güler ([hwclass](https://github.com/hwclass)) 49 | - Dünya Değirmenci ([ddegirmenci](https://github.com/ddegirmenci)) 50 | - Emre Yılmaz ([emre](https://github.com/emre)) 51 | - Gürel Kaynak ([gurelkaynak](https://github.com/gurelkaynak)) 52 | - Hazar İlhan ([batilc1](https://github.com/batilc1)) 53 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pygram 2 | ====== 3 | 4 | Telegram messaging from your terminal. 5 | 6 | Setup (In hard way) 7 | ------------------- 8 | 9 | At first tg-cli must be installed. 10 | 11 | .. code-block:: bash 12 | 13 | git clone --recursive https://github.com/vysheng/tg.git ~/.tg && cd ~/.tg 14 | ./configure 15 | make 16 | 17 | Setup (More basic) 18 | ------------------ 19 | 20 | Just run the following. 21 | 22 | .. code-block:: bash 23 | 24 | python client_installer.py 25 | 26 | Setup (for both installation) 27 | ----------------------------- 28 | 29 | After installing client package, must be called ones to save login information on local folder, all explained on client readme file 30 | 31 | .. code-block:: bash 32 | 33 | .~/.tg/bin/telegram-cli server.pub 34 | 35 | on coming screen, you should type your phone number then received activation code on your phone. then just type ``safe_quit`` or just ``quit`` 36 | 37 | Installing 38 | ---------- 39 | 40 | ``python 3.5.1`` used. 41 | 42 | there are two way, first; 43 | 44 | .. code-block:: bash 45 | 46 | git clone https://github.com/RedXBeard/pygram.git 47 | cd pygram;pip3.5 install -r requirements 48 | 49 | second; 50 | 51 | .. code-block:: bash 52 | 53 | pip3.5 install -e /path/to/cloned/repo/ 54 | 55 | Usage 56 | ----- 57 | 58 | Because of there are two ways to install, there are two ways to use, first; 59 | 60 | .. code-block:: bash 61 | 62 | python3.5 main.py 63 | 64 | second, much more easy; 65 | 66 | .. code-block:: bash 67 | 68 | pygram 69 | 70 | 71 | Collaborators 72 | ----------- 73 | 74 | - Barbaros Yıldırım (`RedXBeard `_) 75 | - Barış Güler (`hwclass `_) 76 | - Dünya Değirmenci (`ddegirmenci `_) 77 | - Emre Yılmaz (`emre `_) 78 | - Gürel Kaynak (`gurelkaynak `_) 79 | - Hazar İlhan (`batilc1 `_) 80 | -------------------------------------------------------------------------------- /client_installer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | 4 | CHANGE_MAKEFILE = ( 5 | """python -c "ff=open('Makefile').read();f=open('Makefile', 'w');f.write(ff.replace(' -Werror',''));f.close()" """) 6 | 7 | PLATFORM = platform.system() 8 | 9 | 10 | def getoutput(cmd): 11 | import subprocess 12 | p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, 13 | stderr=subprocess.PIPE) 14 | p.wait() 15 | if p.returncode: 16 | print('WARNING: A problem occurred while running {0} (code {1})\n' 17 | .format(cmd, p.returncode)) 18 | stderr_content = p.stderr.read() 19 | if stderr_content: 20 | print('{0}\n'.format(stderr_content)) 21 | return "" 22 | return True 23 | 24 | 25 | # Cloning 26 | home_folder = os.path.expanduser('~') 27 | print("Cloning client library...") 28 | getoutput("rm -rf {}".format(os.path.join(home_folder, ".tg"))) 29 | result = getoutput("git clone --recursive https://github.com/vysheng/tg.git ~/.tg") 30 | 31 | os.chdir(os.path.join(home_folder, ".tg")) 32 | 33 | if result and PLATFORM == "Darwin": 34 | print("Required libraries installing...") 35 | result = getoutput("brew install libconfig readline lua libevent jansson") 36 | 37 | if result and PLATFORM == "Darwin": 38 | print("New paths exporting...") 39 | result = getoutput( 40 | 'export CFLAGS="-I/usr/local/include -I/usr/local/Cellar/readline/6.3.8/include";' 41 | 'export CPPFLAGS="-I/usr/local/opt/openssl/include";' 42 | 'export LDFLAGS="-L/usr/local/opt/openssl/lib ' 43 | '-L/usr/local/Cellar/readline/6.3.8/lib -L/usr/local/opt/lua/lib";' 44 | './configure;{};make'.format(CHANGE_MAKEFILE)) 45 | 46 | elif result: 47 | print("New paths exporting...") 48 | result = getoutput( 49 | 'sudo apt-get install libreadline-dev libconfig-dev libssl-dev lua5.2 ' 50 | 'liblua5.2-dev libevent-dev libjansson-dev libpython-dev make;./configure;make' 51 | ) 52 | 53 | os.chdir(home_folder) 54 | -------------------------------------------------------------------------------- /log/dummy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedXBeard/pygram/b02dd20dd74d406a08ca08ca5873f2e6e2c3ae59/log/dummy -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | import logging 4 | from datetime import datetime 5 | 6 | from pygram.app import PyGramApp 7 | 8 | logger = logging.getLogger("main") 9 | 10 | if __name__ == "__main__": 11 | # from run import main 12 | logging.basicConfig(filename="./log/pygram-{}.log".format(datetime.now().date())) 13 | # sys.exit(main()) 14 | PyGramApp().run() 15 | -------------------------------------------------------------------------------- /pygram/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from urllib.error import URLError 3 | from urllib.request import urlopen 4 | 5 | __version__ = "1.0.0" 6 | VERSION = __version__.split('.') 7 | 8 | 9 | def printed(obj): 10 | if hasattr(obj, 'first_name'): 11 | return "{} {}".format(obj.first_name, obj.last_name) 12 | elif hasattr(obj, 'title'): 13 | return obj.title 14 | return '' 15 | 16 | 17 | @asyncio.coroutine 18 | def check_version(): 19 | try: 20 | resp = urlopen("https://github.com/RedXBeard/pygram/releases/latest") 21 | current_version = int("".join(resp.url.split("/")[-1].split("."))) 22 | if current_version > int("".join(list(map(str, VERSION)))): 23 | return False 24 | except (URLError, ValueError): 25 | pass 26 | return True 27 | -------------------------------------------------------------------------------- /pygram/actionform.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import curses 3 | import weakref 4 | from datetime import datetime 5 | 6 | import npyscreen 7 | from DictObject import DictObject 8 | from npyscreen import ActionFormV2WithMenus, wgwidget as widget 9 | from pytg.exceptions import NoResponse, IllegalResponseException 10 | from pytg.utils import coroutine 11 | 12 | from pygram import printed, check_version 13 | from pygram.boxtitle import DialogBox, HistoryBox, ChatBox 14 | from pygram.menu import CustomMenu 15 | from pygram.pg_threading import PGThread 16 | 17 | 18 | class PyGramForm(ActionFormV2WithMenus): 19 | BLANK_LINES_BASE = 1 20 | OK_BUTTON_BR_OFFSET = (1, 6) 21 | CANCEL_BUTTON_BR_OFFSET = (5, 12) 22 | OK_BUTTON_TEXT = "QUIT" 23 | CANCEL_BUTTON_TEXT = "SEND" 24 | MENUS = [] 25 | 26 | def __init__(self, *args, **kwargs): 27 | self.TG = kwargs.pop('TG', None) 28 | self.form_width = 30 29 | self.receiver_thread = None 30 | self.full_name = printed(self.TG.sender.get_self()) 31 | self.dialog_list = None 32 | self.chat_history = None 33 | self.chat_box = None 34 | self.contacts_list = [] 35 | self.editw = 0 36 | super().__init__(*args, **kwargs) 37 | self.current_peer = None 38 | self.version_checked = False 39 | 40 | def display_menu_advert_at(self): 41 | return 2, self.lines - 1 42 | 43 | def draw_form(self): 44 | super().draw_form() 45 | menu_advert = " " + self.__class__.MENU_KEY + ": Menu " 46 | x, y = self.display_menu_advert_at() 47 | if isinstance(menu_advert, bytes): 48 | menu_advert = menu_advert.decode('utf-8', 'replace') 49 | self.add_line( 50 | y, x, 51 | menu_advert, 52 | self.make_attributes_list(menu_advert, curses.A_NORMAL), 53 | self.columns - x 54 | ) 55 | 56 | def set_up_exit_condition_handlers(self): 57 | super().set_up_exit_condition_handlers() 58 | self.how_exited_handers.update({ 59 | widget.EXITED_ESCAPE: self.find_quit_button 60 | }) 61 | 62 | def find_quit_button(self): 63 | self.editw = len(self._widgets__) - 1 64 | 65 | def on_ok(self, direct=False): 66 | if direct: 67 | self.parentApp.switchForm(None) 68 | ans = npyscreen.notify_yes_no('Are you sure, you want to quit?') 69 | if ans: 70 | self.TG.receiver.stop() 71 | self.parentApp.switchForm(None) 72 | 73 | def check_version(self, **keywords): 74 | if not self.version_checked: 75 | loop = keywords.get('loop') 76 | result = loop.run_until_complete(check_version()) 77 | if not result: 78 | npyscreen.notify("New version released please check.") 79 | self.version_checked = True 80 | 81 | def on_screen(self): 82 | if not hasattr(self, 'checker_thread'): 83 | loop = asyncio.get_event_loop() 84 | self.checker_thread = PGThread(target=self.check_version, args=(), kwargs={'loop': loop}) 85 | self.checker_thread.daemon = True 86 | self.checker_thread.start() 87 | 88 | def on_cancel(self): 89 | """ Message will be send """ 90 | if self.current_peer: 91 | text = self.chat_box.entry_widget.value.strip() 92 | if text: 93 | send_status = self.TG.sender.send_msg(self.current_peer.print_name, text) 94 | if send_status: 95 | self.chat_box.entry_widget.value = "" 96 | self.load_history(current_dialog=self.current_peer) 97 | self.editw = self._widgets__.index(self.chat_box) 98 | else: 99 | npyscreen.notify_ok_cancel('Please select receiver first.') 100 | 101 | def add_menu(self, name=None, *args, **keywords): 102 | if not hasattr(self, '_NMenuList'): 103 | self._NMenuList = [] 104 | _mnu = CustomMenu(name=name, *args, **keywords) 105 | self._NMenuList.append(_mnu) 106 | return weakref.proxy(_mnu) 107 | 108 | def create(self): 109 | self.dialog_list = self.add(DialogBox, name="Dialog List", scroll_exit=True, 110 | editable=True, max_width=self.form_width, 111 | max_height=self._max_physical()[0] - 10) 112 | self.load_dialogs() 113 | 114 | self.chat_history = self.add(HistoryBox, name="", scroll_exit=True, 115 | editable=True, relx=self.form_width + 2, rely=2, 116 | max_height=self._max_physical()[0] - 10, exit_left=True, exit_right=True) 117 | 118 | self.chat_box = self.add(ChatBox, name='{}'.format(self.full_name), scroll_exit=True, 119 | editable=True, max_height=5) 120 | 121 | self.contacts_list_menu = self.add_menu(name="Contact List") 122 | 123 | self.contacts_list_menu.addItemsFromList( 124 | list(map(lambda x: (x, self.start_dialog, None, None, None, {'contact': x}), self.load_contacts_list())) 125 | ) 126 | 127 | self.start_receiver() 128 | 129 | def load_contacts_list(self): 130 | self.contacts_list = self.TG.sender.contacts_list() 131 | return self.contacts_list 132 | 133 | def start_dialog(self, contact): 134 | # start a chat with selected one, what about dialog_list? 135 | contact.printed = printed(contact) 136 | self.current_peer = contact 137 | self.load_history(current_dialog=self.current_peer) 138 | 139 | def start_receiver(self): 140 | self.receiver_thread = PGThread(target=self.trigger_receiver) 141 | self.receiver_thread.daemon = True 142 | self.receiver_thread.start() 143 | 144 | @coroutine 145 | def message_loop(self): 146 | try: 147 | while True: 148 | msg = (yield) 149 | if msg.event != "message" or msg.own: 150 | continue 151 | else: 152 | self.load_dialogs() 153 | if self.current_peer: 154 | if ((self.current_peer.peer_type == 'user' and 155 | self.current_peer.peer_id == msg.sender.peer_id) or 156 | (self.current_peer.peer_type == 'chat' and 157 | self.current_peer.peer_id == msg.receiver.peer_id)): 158 | self.load_history(trigger_movement=False, current_dialog=self.current_peer) 159 | except (GeneratorExit, KeyboardInterrupt, TypeError, NoResponse) as err: 160 | print(err) 161 | pass 162 | 163 | def trigger_receiver(self): 164 | try: 165 | self.TG.receiver.start() 166 | self.TG.receiver.message(self.message_loop()) 167 | except TypeError: 168 | npyscreen.notify("Sorry, An error occurred please restart the app :(") 169 | self.on_ok(direct=True) 170 | 171 | def load_dialogs(self): 172 | dialog_list = list(reversed(self.TG.sender.dialog_list(retry_connect=True))) 173 | 174 | # Formating display for dialogs 175 | peer_index = None 176 | for dial in dialog_list: 177 | dial.printed = printed(dial) 178 | if hasattr(self, 'current_peer') and dial == self.current_peer: 179 | peer_index = dialog_list.index(dial) 180 | try: 181 | history = self.TG.sender.history(dial.print_name, 2, 0, retry_connect=True) 182 | unread = len(list(filter(lambda x: x.unread, history))) 183 | dial.unread = unread 184 | except (IllegalResponseException, NoResponse): 185 | dial.unread = 0 186 | self.parentApp.dialog_list = dialog_list 187 | self.dialog_list.values = dialog_list 188 | self.dialog_list.entry_widget.value = peer_index 189 | self.dialog_list.update() 190 | self.find_next_editable() 191 | self.editw -= 1 192 | 193 | def load_history(self, **keywords): 194 | current_dialog = keywords.get('current_dialog', None) 195 | if current_dialog: 196 | self.current_peer = current_dialog 197 | self.chat_history.entry_widget.lines_placed = False 198 | self.chat_history.name = (getattr(current_dialog, 'title', '') or 199 | getattr(current_dialog, 'printed', '') or 'Unknown') 200 | 201 | while True: 202 | try: 203 | history = self.TG.sender.history(current_dialog.print_name, 100, 0, retry_connect=True) 204 | break 205 | except NoResponse: 206 | continue 207 | 208 | unread = list(filter(lambda x: x.unread, history)) 209 | if unread: 210 | unread_index = history.index(unread[0]) 211 | history = history[:unread_index] + ["--New Messages--"] + history[unread_index:] 212 | self.chat_history.values = list( 213 | filter(lambda x: x, 214 | map(lambda x: ( 215 | isinstance(x, str) and x or '{} ({})\n\t{}'.format( 216 | printed(getattr(x, 'from')), 217 | datetime.fromtimestamp(getattr(x, 'date', '')), 218 | (getattr(x, 'text', '') or 219 | getattr(getattr(x, 'media', DictObject()), 'address', '')))), 220 | history))) 221 | self.parentApp.fill_history() 222 | self.find_next_editable() 223 | self.editw -= 1 224 | self.chat_history.entry_widget.lines_placed = True 225 | self.chat_history.update() 226 | self.chat_history.entry_widget.h_show_end(None) 227 | self.find_next_editable() 228 | self.editw -= 1 229 | 230 | if keywords.get('trigger_movement', True): 231 | # Force movement to chat box 232 | for wid in self._widgets__: 233 | if wid == self.chat_box: 234 | self.editw = self._widgets__.index(wid) 235 | self._widgets__[self.editw].editing = True 236 | self._widgets__[self.editw].edit() 237 | self._widgets__[self.editw].display() 238 | break 239 | wid.editing = False 240 | wid.how_exited = widget.EXITED_DOWN 241 | self.handle_exiting_widgets(wid.how_exited) 242 | self.load_dialogs() 243 | self.dialog_list.update() 244 | -------------------------------------------------------------------------------- /pygram/app.py: -------------------------------------------------------------------------------- 1 | import npyscreen 2 | from npyscreen import NPSAppManaged 3 | from pytg import Telegram 4 | 5 | from pygram import __version__ 6 | from pygram.actionform import PyGramForm 7 | from pygram.config import TELEGRAM_CLI_PATH, PUBKEY_FILE 8 | 9 | TG = Telegram(telegram=TELEGRAM_CLI_PATH, 10 | pubkey_file=PUBKEY_FILE) 11 | 12 | 13 | class PyGramApp(NPSAppManaged): 14 | dialog_list = [] 15 | contacts_list = [] 16 | 17 | def onStart(self): 18 | npyscreen.setTheme(npyscreen.Themes.ElegantTheme) 19 | self.dialog_list = TG.sender.dialog_list(retry_connect=True) 20 | self.contacts_list = TG.sender.contacts_list() 21 | self.addForm('MAIN', PyGramForm, name='Welcome PyGram ({})'.format(__version__), TG=TG) 22 | 23 | def fill_history(self): 24 | self.resetHistory() 25 | -------------------------------------------------------------------------------- /pygram/boxtitle.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | from npyscreen import BoxTitle 4 | from npyscreen import Textfield 5 | from pygram.pager import CustomPager 6 | from pygram.selectone import CustomSelectOne 7 | 8 | 9 | class DialogBox(BoxTitle): 10 | _contained_widget = CustomSelectOne 11 | 12 | 13 | class HistoryBox(BoxTitle): 14 | _contained_widget = CustomPager 15 | 16 | 17 | class ChatBox(BoxTitle): 18 | _contained_widget = Textfield 19 | -------------------------------------------------------------------------------- /pygram/checkbox.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | from npyscreen import RoundCheckBox 4 | from npyscreen import Textfield 5 | 6 | 7 | class CustomRoundCheckBox(RoundCheckBox): 8 | def __init__(self, screen, value=False, **keywords): 9 | super().__init__(screen, value=value, **keywords) 10 | self.unread = None 11 | 12 | def when_toggle(self, current_dialog): 13 | root = self.find_parent_app() 14 | form = root.getForm('MAIN') 15 | form.load_history(current_dialog=current_dialog) 16 | 17 | def _create_label_area(self, screen): 18 | l_a_width = self.width - 5 19 | 20 | if l_a_width < 1: 21 | raise ValueError("Width of checkbox + label must be at least 6") 22 | self.label_area = Textfield(screen, rely=self.rely, relx=self.relx + 5, 23 | width=self.width - 5, value=self.name) 24 | 25 | def update(self, clear=True): 26 | super().update(clear=clear) 27 | if self.hide: 28 | return True 29 | cb_display = "({})".format(str(self.unread).zfill(2)) 30 | if self.do_colors(): 31 | self.parent.curses_pad.addstr(self.rely, self.relx, cb_display, 32 | self.parent.theme_manager.findPair( 33 | self, 'DANGER' if self.unread else 'CONTROL')) 34 | else: 35 | self.parent.curses_pad.addstr(self.rely, self.relx, cb_display) 36 | 37 | self._update_label_area() 38 | 39 | def _update_label_row_attributes(self, row, clear=True): 40 | super()._update_label_row_attributes(row, clear=clear) 41 | if self.unread: 42 | row.color = 'DANGER' 43 | else: 44 | row.color = 'DEFAULT' 45 | 46 | row.update(clear=clear) 47 | 48 | def calculate_area_needed(self): 49 | return 0, 0 50 | -------------------------------------------------------------------------------- /pygram/command.py: -------------------------------------------------------------------------------- 1 | from pygram.app import PyGramApp 2 | 3 | 4 | def main(): 5 | PyGramApp().run() 6 | -------------------------------------------------------------------------------- /pygram/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | PACKAGE_ROOT = os.path.dirname(os.path.abspath(__file__)) 4 | 5 | TELEGRAM_CLI_PATH = os.path.join(os.path.expanduser('~'),".tg/bin/telegram-cli") 6 | PUBKEY_FILE = os.path.join(PACKAGE_ROOT, 'pubkey.pub') 7 | try: 8 | from pygram.config_local import * 9 | except ImportError: 10 | pass 11 | -------------------------------------------------------------------------------- /pygram/menu.py: -------------------------------------------------------------------------------- 1 | from npyscreen.muNewMenu import NewMenu 2 | 3 | from pygram.menuitem import CustomMenuItem 4 | 5 | 6 | class CustomMenu(NewMenu): 7 | def addItemsFromList(self, item_list): 8 | for l in item_list: 9 | if isinstance(l, CustomMenuItem): 10 | self.addNewSubmenu(*l) 11 | else: 12 | self.addItem(*l) 13 | 14 | def addItem(self, *args, **keywords): 15 | _itm = CustomMenuItem(*args, **keywords) 16 | self._menuList.append(_itm) 17 | -------------------------------------------------------------------------------- /pygram/menuitem.py: -------------------------------------------------------------------------------- 1 | from npyscreen.muNewMenu import MenuItem 2 | 3 | from pygram import printed 4 | 5 | 6 | class CustomMenuItem(MenuItem): 7 | def __init__(self, obj=None, onSelect=None, shortcut=None, document=None, arguments=None, keywords=None): 8 | text = obj and printed(obj) or '' 9 | super().__init__(text, onSelect, shortcut, document, arguments, keywords) 10 | self.obj = obj 11 | -------------------------------------------------------------------------------- /pygram/pager.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import curses 3 | import textwrap 4 | 5 | from npyscreen import Pager, BufferPager 6 | from npyscreen import wgwidget 7 | 8 | 9 | class CustomPager(BufferPager): 10 | def __init__(self, screen, autowrap=True, center=False, **keywords): 11 | super().__init__(screen, **keywords) 12 | self.how_exited = None 13 | self.autowrap = autowrap 14 | self.center = center 15 | self._values_cache_for_wrapping = [] 16 | self.widgets_inherit_color = True 17 | self.color = 'DEFAULT' 18 | self.lines_placed = False 19 | 20 | def _wrap_message_lines(self, message_lines, line_length): 21 | lines = [] 22 | if not self.lines_placed: 23 | is_user_message = False 24 | for line in message_lines: 25 | if line.rstrip() == '': 26 | lines.append('') 27 | else: 28 | root = self.find_parent_app() 29 | user = root.getForm('MAIN').full_name 30 | if line.find('\n\t') != -1: 31 | is_user_message = False 32 | user_info, message_text = line.rsplit("\n\t", 1) 33 | space = line_length - 1 - len(user_info) 34 | name, timestamp = user_info.split('(') 35 | if name.strip() == user.strip(): 36 | message_header = "({}{}{}".format(timestamp.strip(), 37 | '.' * space, name.strip()) 38 | is_user_message = True 39 | else: 40 | message_header = "{}{}({}".format(name.strip(), '.' * space, 41 | timestamp.strip()) 42 | lines.append("->{}".format(message_header)) 43 | else: 44 | message_text = line 45 | this_line_set = list(map( 46 | lambda x: (is_user_message and 47 | "{}{}".format(' ' * (line_length - 1 - len(x)), x) or 48 | (x == "--New Messages--" and 49 | "{}{}{}".format(' ' * int((line_length - 1 - len(x)) / 2), 50 | x, 51 | ' ' * int((line_length - 1 - len(x)) / 2)) or 52 | "{}{}".format(' ' * 4, x))), 53 | textwrap.wrap(message_text.rstrip(), line_length - 5))) 54 | if this_line_set: 55 | lines.extend(this_line_set + ['']) 56 | else: 57 | lines.append('') 58 | else: 59 | lines = message_lines 60 | return lines 61 | 62 | def _set_line_values(self, line, value_indexer): 63 | try: 64 | _vl = self.values[value_indexer] 65 | except IndexError: 66 | self._set_line_blank(line) 67 | return False 68 | except TypeError: 69 | self._set_line_blank(line) 70 | return False 71 | line.value = self.display_value(_vl) 72 | color = 'DEFAULT' 73 | bold = False 74 | if _vl.startswith('->('): 75 | color = 'GOOD' 76 | bold = True 77 | elif _vl.startswith('->'): 78 | color = 'CONTROL' 79 | bold = True 80 | elif _vl.find('--New Messages--') != -1: 81 | line.value = line.display_value(_vl.replace('-', ' ')) 82 | color = 'STANDOUT' 83 | line.color = color 84 | line.hidden = False 85 | if bold: 86 | line.show_bold = True 87 | 88 | def h_scroll_line_down(self, ch): 89 | self.start_display_at += 1 90 | if self.scroll_exit and self.height > len(self.values) - self.start_display_at: 91 | self.editing = False 92 | self.how_exited = wgwidget.EXITED_DOWN 93 | 94 | def h_scroll_line_up(self, ch): 95 | if ch == curses.KEY_LEFT:# and self.cursor_line: 96 | self.h_show_beginning(ch) 97 | super().h_scroll_line_up(ch) 98 | else: 99 | super().h_scroll_line_up(ch) 100 | -------------------------------------------------------------------------------- /pygram/pg_threading.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import threading 3 | 4 | from pytg.exceptions import NoResponse, IllegalResponseException, ConnectionError 5 | 6 | 7 | class PGThread(threading.Thread): 8 | def run(self): 9 | try: 10 | if self._target: 11 | self._target(*self._args, **self._kwargs) 12 | except (asyncio.TimeoutError, GeneratorExit, KeyboardInterrupt, 13 | TypeError, RuntimeError, NoResponse, IllegalResponseException, 14 | ConnectionError): 15 | pass 16 | finally: 17 | del self._target, self._args, self._kwargs 18 | -------------------------------------------------------------------------------- /pygram/pubkey.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PUBLIC KEY----- 2 | MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6 3 | lyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS 4 | an9tqw3bfUV/nqgbhGX81v/+7RFAEd+RwFnK7a+XYl9sluzHRyVVaTTveB2GazTw 5 | Efzk2DWgkBluml8OREmvfraX3bkHZJTKX4EQSjBbbdJ2ZXIsRrYOXfaA+xayEGB+ 6 | 8hdlLmAjbCVfaigxX0CDqWeR1yFL9kwd9P0NsZRPsmoqVwMbMu7mStFai6aIhc3n 7 | Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB 8 | -----END RSA PUBLIC KEY----- -------------------------------------------------------------------------------- /pygram/selectone.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | from DictObject import DictObject 4 | from npyscreen import SelectOne 5 | 6 | from pygram.checkbox import CustomRoundCheckBox 7 | 8 | 9 | class CustomSelectOne(SelectOne): 10 | _contained_widgets = CustomRoundCheckBox 11 | 12 | def h_select(self, ch): 13 | self.value = [self.cursor_line, ] 14 | line = self._my_widgets[self.value[0]] 15 | line.when_toggle(self.values[self.value[0]]) 16 | 17 | def _print_line(self, line, value_indexer): 18 | try: 19 | unread, display_this = self.display_value(self.values[value_indexer]) 20 | if unread > 1: 21 | unread = '1+' 22 | line.value = display_this 23 | line.hide = False 24 | if hasattr(line, 'selected'): 25 | line.selected = bool(value_indexer in self.value and (self.value is not None)) 26 | # Most classes in the standard library use this 27 | else: 28 | line.show_bold = False 29 | line.value = False 30 | if value_indexer in self.value and (self.value is not None): 31 | line.show_bold = True 32 | line.value = True 33 | line.name = display_this 34 | line.unread = unread 35 | line.important = bool(value_indexer in self._filtered_values_cache) 36 | except IndexError: 37 | line.name = None 38 | line.hide = True 39 | 40 | line.highlight = False 41 | 42 | def display_value(self, vl): 43 | try: 44 | if isinstance(vl, DictObject): 45 | return vl.unread, self.safe_string(vl.printed) 46 | return self.safe_string(str(vl)) 47 | except ReferenceError: 48 | return "**REFERENCE ERROR**" 49 | 50 | def update(self, clear=True): 51 | super().update(clear=clear) 52 | for widget in self._my_widgets: 53 | widget.update() 54 | -------------------------------------------------------------------------------- /requirements: -------------------------------------------------------------------------------- 1 | pytg 2 | DictObject 3 | luckydonald-utils 4 | ipython 5 | npyscreen -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import setup 4 | 5 | from pygram import __version__ 6 | 7 | with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme: 8 | LONG_DESCRIPTION = readme.read() 9 | 10 | setup( 11 | name='pygram', 12 | version=__version__, 13 | packages=['pygram'], 14 | package_data={ 15 | '.': [ 16 | '*.pub', 17 | ]}, 18 | include_package_data=True, 19 | url='https://github.com/RedXBeard/pygram', 20 | license='MIT', 21 | author='Barbaros Yildirim', 22 | author_email='barbarosaliyildirim@gmail.com', 23 | description='Telegram messaging from your terminal.', 24 | long_description=LONG_DESCRIPTION, 25 | entry_points={ 26 | 'console_scripts': [ 27 | 'pygram = pygram.command:main', 28 | ], 29 | }, 30 | classifiers=[ 31 | 'Development Status :: 4 - Beta', 32 | 'Environment :: Console', 33 | 'Intended Audience :: End Users/Desktop', 34 | 'Intended Audience :: Telecommunications Industry', 35 | 'License :: OSI Approved :: MIT License', 36 | 'Operating System :: Unix', 37 | 'Programming Language :: Python :: 3 :: Only', 38 | 'Topic :: Communications :: Chat' 39 | ], 40 | install_requires=[ 41 | 'npyscreen', 42 | 'DictObject', 43 | 'pytg' 44 | ] 45 | ) 46 | --------------------------------------------------------------------------------