Skip to content

Commit 5df3242

Browse files
Add notification when received message
1 parent 4c03fb9 commit 5df3242

File tree

8 files changed

+171
-20
lines changed

8 files changed

+171
-20
lines changed

README.md

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ pip
4242
```bash
4343
git clone https://github.com/haskellcamargo/sclack.git
4444
cd sclack
45-
pip3 install -r requirements.txt
45+
pip3 install --upgrade -r requirements.txt
4646
chmod +x ./app.py
4747
./app.py
4848
```
@@ -76,8 +76,7 @@ Your `~/.sclack` file will look like:
7676

7777
### Multiple workspaces
7878

79-
If you want to, you can use Sclack in multiple workspaces. You can have
80-
at most 9 workspaces defined inside `workspaces`:
79+
If you want to, you can use Sclack in multiple workspaces. You can have at most 9 workspaces defined inside `workspaces`:
8180

8281
```json
8382
{
@@ -98,6 +97,23 @@ You can use the keys from 1 up to 9 to switch workspaces or event right-click th
9897

9998
![Multiple workspaces](./resources/example_7.png)
10099

100+
### Enable features
101+
102+
There are some features available, you can adjust them by change the config file
103+
104+
105+
```json
106+
{
107+
"features": {
108+
"emoji": true,
109+
"markdown": true,
110+
"pictures": true,
111+
"notification": ""
112+
},
113+
}
114+
```
115+
116+
* notification: How we send notification for you (*MacOS* supported now): `none` Disable notification / `mentioned` Direct message or mentioned in channel / `all` Receive all notifications
101117

102118
### Default keybindings
103119
```json
@@ -190,5 +206,7 @@ Contributions are very welcome, and there is a lot of work to do! You can...
190206
![](./resources/example_4.png)
191207
![](./resources/example_5.png)
192208
![](./resources/example_6.png)
209+
![](./resources/example_7.png)
210+
![](./resources/example_8.png)
193211

194212
<p align="center">Made with :rage: by <a href="https://github.com/haskellcamargo">@haskellcamargo</a></p>

TODO.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@
1818
- 'Do not disturb' status
1919
- Integration with reminders
2020
- Handle slash commands
21-
- RTM events (see https://api.slack.com/rtm)
21+
- RTM events (see https://api.slack.com/rtm)

app.py

Lines changed: 119 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@
33
import concurrent.futures
44
import functools
55
import json
6+
import re
67
import os
78
import requests
8-
import subprocess
99
import sys
10+
import platform
1011
import traceback
1112
import tempfile
1213
import urwid
1314
from datetime import datetime
14-
from slackclient import SlackClient
15+
1516
from sclack.components import Attachment, Channel, ChannelHeader, ChatBox, Dm
1617
from sclack.components import Indicators, MarkdownText, Message, MessageBox
1718
from sclack.components import NewMessagesDivider, Profile, ProfileSideBar
@@ -86,6 +87,48 @@ def __init__(self, config):
8687
unhandled_input=self.unhandled_input
8788
)
8889
self.configure_screen(self.urwid_loop.screen)
90+
self.mentioned_patterns = None
91+
92+
def get_mentioned_patterns(self):
93+
slack_mentions = [
94+
'<!everyone>',
95+
'<!here>',
96+
'<!channel>',
97+
'<@{}>'.format(self.store.state.auth['user_id']),
98+
]
99+
100+
patterns = []
101+
102+
for mention in slack_mentions:
103+
patterns.append('^{}[ ]+'.format(mention))
104+
patterns.append('^{}$'.format(mention))
105+
patterns.append('[ ]+{}'.format(mention))
106+
107+
return re.compile('|'.join(patterns))
108+
109+
def should_notify_me(self, message_obj):
110+
"""
111+
Checking whether notify to user
112+
:param message_obj:
113+
:return:
114+
"""
115+
# You send message, don't need notification
116+
if self.config['features']['notification'] in ['', 'none'] or message_obj['user'] == self.store.state.auth['user_id']:
117+
return False
118+
119+
if self.config['features']['notification'] == 'all':
120+
return True
121+
122+
# Private message
123+
if message_obj.get('channel') is not None and message_obj.get('channel')[0] == 'D':
124+
return True
125+
126+
regex = self.mentioned_patterns
127+
if regex is None:
128+
regex = self.get_mentioned_patterns()
129+
self.mentioned_patterns = regex
130+
131+
return len(re.findall(regex, message_obj['text'])) > 0
89132

90133
def start(self):
91134
self._loading = True
@@ -140,6 +183,8 @@ def mount_sidebar(self, executor):
140183
loop.run_in_executor(executor, self.store.load_groups),
141184
loop.run_in_executor(executor, self.store.load_users)
142185
)
186+
self.mentioned_patterns = self.get_mentioned_patterns()
187+
143188
profile = Profile(name=self.store.state.auth['user'])
144189
channels = [
145190
Channel(
@@ -158,7 +203,7 @@ def mount_sidebar(self, executor):
158203
if user:
159204
dms.append(Dm(
160205
dm['id'],
161-
name=user.get('display_name') or user.get('real_name') or user['name'],
206+
name=self.store.get_user_display_name(user),
162207
user=dm['user'],
163208
you=user['id'] == self.store.state.auth['user_id']
164209
))
@@ -244,7 +289,7 @@ def go_to_profile(self, user_id):
244289
return
245290
self.store.state.profile_user_id = user_id
246291
profile = ProfileSideBar(
247-
user.get('display_name') or user.get('real_name') or user['name'],
292+
self.store.get_user_display_name(user),
248293
user['profile'].get('status_text', None),
249294
user['profile'].get('tz_label', None),
250295
user['profile'].get('phone', None),
@@ -260,7 +305,7 @@ def render_chatbox_header(self):
260305
if self.store.state.channel['id'][0] == 'D':
261306
user = self.store.find_user_by_id(self.store.state.channel['user'])
262307
header = ChannelHeader(
263-
name=user.get('display_name') or user.get('real_name') or user['name'],
308+
name=self.store.get_user_display_name(user),
264309
topic=user['profile']['status_text'],
265310
is_starred=self.store.state.channel.get('is_starred', False),
266311
is_dm_workaround_please_remove_me=True
@@ -282,6 +327,16 @@ def on_change_topic(self, text):
282327
self.store.set_topic(self.store.state.channel['id'], text)
283328
self.go_to_sidebar()
284329

330+
def notification_messages(self, messages):
331+
"""
332+
Check and send notifications
333+
:param messages:
334+
:return:
335+
"""
336+
for message in messages:
337+
if self.should_notify_me(message):
338+
self.send_notification(message, MarkdownText(message['text']))
339+
285340
def render_message(self, message):
286341
is_app = False
287342
subtype = message.get('subtype')
@@ -331,6 +386,7 @@ def render_message(self, message):
331386
return None
332387

333388
user_id = user['id']
389+
# TODO
334390
user_name = user['profile']['display_name'] or user.get('name')
335391
color = user.get('color')
336392
if message.get('file'):
@@ -343,6 +399,7 @@ def render_message(self, message):
343399
return None
344400

345401
user_id = user['id']
402+
# TODO
346403
user_name = user['profile']['display_name'] or user.get('name')
347404
color = user.get('color')
348405

@@ -355,6 +412,7 @@ def render_message(self, message):
355412
]
356413

357414
attachments = []
415+
358416
for attachment in message.get('attachments', []):
359417
attachment_widget = Attachment(
360418
service_name=attachment.get('service_name'),
@@ -428,8 +486,9 @@ def render_messages(self, messages):
428486
previous_date = self.store.state.last_date
429487
last_read_datetime = datetime.fromtimestamp(float(self.store.state.channel.get('last_read', '0')))
430488
today = datetime.today().date()
431-
for message in messages:
432-
message_datetime = datetime.fromtimestamp(float(message['ts']))
489+
490+
for raw_message in messages:
491+
message_datetime = datetime.fromtimestamp(float(raw_message['ts']))
433492
message_date = message_datetime.date()
434493
date_text = None
435494
unread_text = None
@@ -449,11 +508,50 @@ def render_messages(self, messages):
449508
_messages.append(NewMessagesDivider(unread_text, date=date_text))
450509
elif date_text is not None:
451510
_messages.append(TextDivider(('history_date', date_text), 'center'))
452-
message = self.render_message(message)
511+
512+
message = self.render_message(raw_message)
513+
453514
if message is not None:
454515
_messages.append(message)
516+
455517
return _messages
456518

519+
def send_notification(self, raw_message, markdown_text):
520+
"""
521+
Only MacOS
522+
@TODO Linux libnotify and Windows
523+
:param raw_message:
524+
:param markdown_text:
525+
:return:
526+
"""
527+
user = self.store.find_user_by_id(raw_message.get('user'))
528+
sender_name = self.store.get_user_display_name(user)
529+
530+
# TODO Checking bot
531+
if raw_message.get('channel')[0] == 'D':
532+
notification_title = 'New message in {}'.format(
533+
self.store.state.auth['team']
534+
)
535+
else:
536+
notification_title = 'New message in {} #{}'.format(
537+
self.store.state.auth['team'],
538+
self.store.get_channel_name(raw_message.get('channel')),
539+
)
540+
541+
sub_title = sender_name
542+
543+
if platform.system() == 'Darwin':
544+
# Macos
545+
import pync
546+
pync.notify(
547+
markdown_text.render_text(),
548+
title=notification_title,
549+
subtitle=sub_title,
550+
appIcon='./resources/slack_icon.png'
551+
)
552+
else:
553+
pass
554+
457555
@asyncio.coroutine
458556
def _go_to_channel(self, channel_id):
459557
with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
@@ -533,6 +631,7 @@ def start_real_time(self):
533631
def stop_typing(*args):
534632
self.chatbox.message_box.typing = None
535633
alarm = None
634+
536635
while self.store.slack.server.connected is True:
537636
events = self.store.slack.rtm_read()
538637
for event in events:
@@ -543,9 +642,8 @@ def stop_typing(*args):
543642
for channel in self.sidebar.channels:
544643
if channel.id == event['channel']:
545644
channel.set_unread(unread)
546-
elif event.get('channel') == self.store.state.channel['id']:
547-
if event['type'] == 'message':
548-
# Delete message
645+
elif event['type'] == 'message':
646+
if event.get('channel') == self.store.state.channel['id']:
549647
if event.get('subtype') == 'message_deleted':
550648
for widget in self.chatbox.body.body:
551649
if hasattr(widget, 'ts') and getattr(widget, 'ts') == event['deleted_ts']:
@@ -559,16 +657,23 @@ def stop_typing(*args):
559657
else:
560658
self.chatbox.body.body.extend(self.render_messages([event]))
561659
self.chatbox.body.scroll_to_bottom()
562-
elif event['type'] == 'user_typing':
660+
else:
661+
pass
662+
663+
if event.get('subtype') != 'message_deleted' and event.get('subtype') != 'message_changed':
664+
# Notification
665+
self.notification_messages([event])
666+
elif event['type'] == 'user_typing':
667+
if event.get('channel') == self.store.state.channel['id']:
563668
user = self.store.find_user_by_id(event['user'])
564-
name = user.get('display_name') or user.get('real_name') or user['name']
669+
name = self.store.get_user_display_name(user)
670+
565671
if alarm is not None:
566672
self.urwid_loop.remove_alarm(alarm)
567673
self.chatbox.message_box.typing = name
568674
self.urwid_loop.set_alarm_in(3, stop_typing)
569675
else:
570676
pass
571-
# print(json.dumps(event, indent=2))
572677
elif event.get('ok', False):
573678
# Message was sent, Slack confirmed it.
574679
self.chatbox.body.body.extend(self.render_messages([{

config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
"emoji": true,
2626
"markdown": true,
2727
"pictures": true,
28-
"browser": ""
28+
"browser": "",
29+
"notification": ""
2930
},
3031
"icons": {
3132
"block": "\u258C",

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ pyperclip==1.6.2
44
requests
55
slackclient==1.2.1
66
urwid_readline
7+
git+git://github.com/duynguyenhoang/pync@994fbf77360a273fac1225558de01c8d0040dc6c#egg=pync

resources/slack_icon.png

57 KB
Loading

sclack/markdown.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def parse_message(self, text):
4040
self._state = 'message'
4141
self._previous_state = 'message'
4242
self._result = []
43+
4344
def render_emoji(result):
4445
return emoji_codemap.get(result.group(1), result.group(0))
4546

@@ -71,3 +72,6 @@ def render_emoji(result):
7172

7273
self._result.append(('message', self.decode_buffer()))
7374
return self._result
75+
76+
def render_text(self):
77+
return urwid.Text(self.markup).text

sclack/store.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from slackclient import SlackClient
22

3+
34
class State:
45
def __init__(self):
56
self.channels = []
@@ -42,6 +43,14 @@ def switch_to_workspace(self, workspace_number):
4243
def find_user_by_id(self, user_id):
4344
return self._users_dict.get(user_id)
4445

46+
def get_user_display_name(self, user_detail):
47+
"""
48+
Get real name of user to display
49+
:param user_detail:
50+
:return:
51+
"""
52+
return user_detail.get('display_name') or user_detail.get('real_name') or user_detail['name']
53+
4554
def load_auth(self):
4655
self.state.auth = self.slack.api_call('auth.test')
4756

@@ -97,6 +106,19 @@ def load_channels(self):
97106
self.state.channels.sort(key=lambda channel: channel['name'])
98107
self.state.dms.sort(key=lambda dm: dm['created'])
99108

109+
def get_channel_name(self, channel_id):
110+
matched_channel = None
111+
112+
for channel in self.state.channels:
113+
if channel['id'] == channel_id:
114+
matched_channel = channel
115+
break
116+
117+
if matched_channel:
118+
return matched_channel['name']
119+
120+
return channel_id
121+
100122
def load_groups(self):
101123
self.state.groups = self.slack.api_call('mpim.list')['groups']
102124

@@ -144,4 +166,4 @@ def get_presence(self, user_id):
144166
self.state.online_users.add(user_id)
145167
else:
146168
self.state.online_users.discard(user_id)
147-
return response
169+
return response

0 commit comments

Comments
 (0)