Skip to content

Commit 01be5a8

Browse files
Add notification when received message
1 parent edc0342 commit 01be5a8

File tree

8 files changed

+168
-13
lines changed

8 files changed

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

85130
def start(self):
86131
self._loading = True
@@ -137,6 +182,8 @@ def mount_sidebar(self, executor):
137182
loop.run_in_executor(executor, self.store.load_users),
138183
loop.run_in_executor(executor, self.store.load_user_dnd),
139184
)
185+
self.mentioned_patterns = self.get_mentioned_patterns()
186+
140187
profile = Profile(name=self.store.state.auth['user'], is_snoozed=self.store.state.is_snoozed)
141188
channels = [
142189
Channel(
@@ -155,7 +202,7 @@ def mount_sidebar(self, executor):
155202
if user:
156203
dms.append(Dm(
157204
dm['id'],
158-
name=user.get('display_name') or user.get('real_name') or user['name'],
205+
name=self.store.get_user_display_name(user),
159206
user=dm['user'],
160207
you=user['id'] == self.store.state.auth['user_id']
161208
))
@@ -280,7 +327,7 @@ def go_to_profile(self, user_id):
280327
return
281328
self.store.state.profile_user_id = user_id
282329
profile = ProfileSideBar(
283-
user.get('display_name') or user.get('real_name') or user['name'],
330+
self.store.get_user_display_name(user),
284331
user['profile'].get('status_text', None),
285332
user['profile'].get('tz_label', None),
286333
user['profile'].get('phone', None),
@@ -296,7 +343,7 @@ def render_chatbox_header(self):
296343
if self.store.state.channel['id'][0] == 'D':
297344
user = self.store.find_user_by_id(self.store.state.channel['user'])
298345
header = ChannelHeader(
299-
name=user.get('display_name') or user.get('real_name') or user['name'],
346+
name=self.store.get_user_display_name(user),
300347
topic=user['profile']['status_text'],
301348
is_starred=self.store.state.channel.get('is_starred', False),
302349
is_dm_workaround_please_remove_me=True
@@ -318,6 +365,16 @@ def on_change_topic(self, text):
318365
self.store.set_topic(self.store.state.channel['id'], text)
319366
self.go_to_sidebar()
320367

368+
def notification_messages(self, messages):
369+
"""
370+
Check and send notifications
371+
:param messages:
372+
:return:
373+
"""
374+
for message in messages:
375+
if self.should_notify_me(message):
376+
self.send_notification(message, MarkdownText(message['text']))
377+
321378
def render_message(self, message):
322379
is_app = False
323380
subtype = message.get('subtype')
@@ -367,6 +424,7 @@ def render_message(self, message):
367424
return None
368425

369426
user_id = user['id']
427+
# TODO
370428
user_name = user['profile']['display_name'] or user.get('name')
371429
color = user.get('color')
372430
if message.get('file'):
@@ -379,6 +437,7 @@ def render_message(self, message):
379437
return None
380438

381439
user_id = user['id']
440+
# TODO
382441
user_name = user['profile']['display_name'] or user.get('name')
383442
color = user.get('color')
384443

@@ -391,6 +450,7 @@ def render_message(self, message):
391450
]
392451

393452
attachments = []
453+
394454
for attachment in message.get('attachments', []):
395455
attachment_widget = Attachment(
396456
service_name=attachment.get('service_name'),
@@ -463,8 +523,9 @@ def render_messages(self, messages):
463523
previous_date = self.store.state.last_date
464524
last_read_datetime = datetime.fromtimestamp(float(self.store.state.channel.get('last_read', '0')))
465525
today = datetime.today().date()
466-
for message in messages:
467-
message_datetime = datetime.fromtimestamp(float(message['ts']))
526+
527+
for raw_message in messages:
528+
message_datetime = datetime.fromtimestamp(float(raw_message['ts']))
468529
message_date = message_datetime.date()
469530
date_text = None
470531
unread_text = None
@@ -485,11 +546,50 @@ def render_messages(self, messages):
485546
_messages.append(NewMessagesDivider(unread_text, date=date_text))
486547
elif date_text is not None:
487548
_messages.append(TextDivider(('history_date', date_text), 'center'))
488-
message = self.render_message(message)
549+
550+
message = self.render_message(raw_message)
551+
489552
if message is not None:
490553
_messages.append(message)
554+
491555
return _messages
492556

557+
def send_notification(self, raw_message, markdown_text):
558+
"""
559+
Only MacOS
560+
@TODO Linux libnotify and Windows
561+
:param raw_message:
562+
:param markdown_text:
563+
:return:
564+
"""
565+
user = self.store.find_user_by_id(raw_message.get('user'))
566+
sender_name = self.store.get_user_display_name(user)
567+
568+
# TODO Checking bot
569+
if raw_message.get('channel')[0] == 'D':
570+
notification_title = 'New message in {}'.format(
571+
self.store.state.auth['team']
572+
)
573+
else:
574+
notification_title = 'New message in {} #{}'.format(
575+
self.store.state.auth['team'],
576+
self.store.get_channel_name(raw_message.get('channel')),
577+
)
578+
579+
sub_title = sender_name
580+
581+
if platform.system() == 'Darwin':
582+
# Macos
583+
import pync
584+
pync.notify(
585+
markdown_text.render_text(),
586+
title=notification_title,
587+
subtitle=sub_title,
588+
appIcon='./resources/slack_icon.png'
589+
)
590+
else:
591+
pass
592+
493593
@asyncio.coroutine
494594
def _go_to_channel(self, channel_id):
495595
with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
@@ -573,6 +673,7 @@ def stop_typing(*args):
573673
self.chatbox.message_box.typing = None
574674

575675
alarm = None
676+
576677
while self.store.slack.server.connected is True:
577678
events = self.store.slack.rtm_read()
578679
for event in events:
@@ -602,9 +703,17 @@ def stop_typing(*args):
602703
else:
603704
self.chatbox.body.body.extend(self.render_messages([event]))
604705
self.chatbox.body.scroll_to_bottom()
605-
elif event['type'] == 'user_typing':
706+
else:
707+
pass
708+
709+
if event.get('subtype') != 'message_deleted' and event.get('subtype') != 'message_changed':
710+
# Notification
711+
self.notification_messages([event])
712+
elif event['type'] == 'user_typing':
713+
if event.get('channel') == self.store.state.channel['id']:
606714
user = self.store.find_user_by_id(event['user'])
607-
name = user.get('display_name') or user.get('real_name') or user['name']
715+
name = self.store.get_user_display_name(user)
716+
608717
if alarm is not None:
609718
self.urwid_loop.remove_alarm(alarm)
610719
self.chatbox.message_box.typing = name

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: 22 additions & 0 deletions
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 = []
@@ -43,6 +44,14 @@ def switch_to_workspace(self, workspace_number):
4344
def find_user_by_id(self, user_id):
4445
return self._users_dict.get(user_id)
4546

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

@@ -99,6 +108,19 @@ def load_channels(self):
99108
self.state.channels.sort(key=lambda channel: channel['name'])
100109
self.state.dms.sort(key=lambda dm: dm['created'])
101110

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

0 commit comments

Comments
 (0)