├── .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 |
--------------------------------------------------------------------------------