Skip to content

Commit 6586a4c

Browse files
Add notification when received message
1 parent 979d217 commit 6586a4c

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
@@ -80,6 +81,48 @@ def __init__(self, config):
8081
unhandled_input=self.unhandled_input
8182
)
8283
self.configure_screen(self.urwid_loop.screen)
84+
self.mentioned_patterns = None
85+
86+
def get_mentioned_patterns(self):
87+
slack_mentions = [
88+
'<!everyone>',
89+
'<!here>',
90+
'<!channel>',
91+
'<@{}>'.format(self.store.state.auth['user_id']),
92+
]
93+
94+
patterns = []
95+
96+
for mention in slack_mentions:
97+
patterns.append('^{}[ ]+'.format(mention))
98+
patterns.append('^{}$'.format(mention))
99+
patterns.append('[ ]+{}'.format(mention))
100+
101+
return re.compile('|'.join(patterns))
102+
103+
def should_notify_me(self, message_obj):
104+
"""
105+
Checking whether notify to user
106+
:param message_obj:
107+
:return:
108+
"""
109+
# You send message, don't need notification
110+
if self.config['features']['notification'] in ['', 'none'] or message_obj['user'] == self.store.state.auth['user_id']:
111+
return False
112+
113+
if self.config['features']['notification'] == 'all':
114+
return True
115+
116+
# Private message
117+
if message_obj.get('channel') is not None and message_obj.get('channel')[0] == 'D':
118+
return True
119+
120+
regex = self.mentioned_patterns
121+
if regex is None:
122+
regex = self.get_mentioned_patterns()
123+
self.mentioned_patterns = regex
124+
125+
return len(re.findall(regex, message_obj['text'])) > 0
83126

84127
def start(self):
85128
self._loading = True
@@ -134,6 +177,8 @@ def mount_sidebar(self, executor):
134177
loop.run_in_executor(executor, self.store.load_groups),
135178
loop.run_in_executor(executor, self.store.load_users)
136179
)
180+
self.mentioned_patterns = self.get_mentioned_patterns()
181+
137182
profile = Profile(name=self.store.state.auth['user'])
138183
channels = [
139184
Channel(
@@ -152,7 +197,7 @@ def mount_sidebar(self, executor):
152197
if user:
153198
dms.append(Dm(
154199
dm['id'],
155-
name=user.get('display_name') or user.get('real_name') or user['name'],
200+
name=self.store.get_user_display_name(user),
156201
user=dm['user'],
157202
you=user['id'] == self.store.state.auth['user_id']
158203
))
@@ -238,7 +283,7 @@ def go_to_profile(self, user_id):
238283
return
239284
self.store.state.profile_user_id = user_id
240285
profile = ProfileSideBar(
241-
user.get('display_name') or user.get('real_name') or user['name'],
286+
self.store.get_user_display_name(user),
242287
user['profile'].get('status_text', None),
243288
user['profile'].get('tz_label', None),
244289
user['profile'].get('phone', None),
@@ -254,7 +299,7 @@ def render_chatbox_header(self):
254299
if self.store.state.channel['id'][0] == 'D':
255300
user = self.store.find_user_by_id(self.store.state.channel['user'])
256301
header = ChannelHeader(
257-
name=user.get('display_name') or user.get('real_name') or user['name'],
302+
name=self.store.get_user_display_name(user),
258303
topic=user['profile']['status_text'],
259304
is_starred=self.store.state.channel.get('is_starred', False),
260305
is_dm_workaround_please_remove_me=True
@@ -276,6 +321,16 @@ def on_change_topic(self, text):
276321
self.store.set_topic(self.store.state.channel['id'], text)
277322
self.go_to_sidebar()
278323

324+
def notification_messages(self, messages):
325+
"""
326+
Check and send notifications
327+
:param messages:
328+
:return:
329+
"""
330+
for message in messages:
331+
if self.should_notify_me(message):
332+
self.send_notification(message, MarkdownText(message['text']))
333+
279334
def render_message(self, message):
280335
is_app = False
281336
subtype = message.get('subtype')
@@ -325,6 +380,7 @@ def render_message(self, message):
325380
return None
326381

327382
user_id = user['id']
383+
# TODO
328384
user_name = user['profile']['display_name'] or user.get('name')
329385
color = user.get('color')
330386
if message.get('file'):
@@ -337,6 +393,7 @@ def render_message(self, message):
337393
return None
338394

339395
user_id = user['id']
396+
# TODO
340397
user_name = user['profile']['display_name'] or user.get('name')
341398
color = user.get('color')
342399

@@ -349,6 +406,7 @@ def render_message(self, message):
349406
]
350407

351408
attachments = []
409+
352410
for attachment in message.get('attachments', []):
353411
attachment_widget = Attachment(
354412
service_name=attachment.get('service_name'),
@@ -422,8 +480,9 @@ def render_messages(self, messages):
422480
previous_date = self.store.state.last_date
423481
last_read_datetime = datetime.fromtimestamp(float(self.store.state.channel.get('last_read', '0')))
424482
today = datetime.today().date()
425-
for message in messages:
426-
message_datetime = datetime.fromtimestamp(float(message['ts']))
483+
484+
for raw_message in messages:
485+
message_datetime = datetime.fromtimestamp(float(raw_message['ts']))
427486
message_date = message_datetime.date()
428487
date_text = None
429488
unread_text = None
@@ -443,11 +502,50 @@ def render_messages(self, messages):
443502
_messages.append(NewMessagesDivider(unread_text, date=date_text))
444503
elif date_text is not None:
445504
_messages.append(TextDivider(('history_date', date_text), 'center'))
446-
message = self.render_message(message)
505+
506+
message = self.render_message(raw_message)
507+
447508
if message is not None:
448509
_messages.append(message)
510+
449511
return _messages
450512

513+
def send_notification(self, raw_message, markdown_text):
514+
"""
515+
Only MacOS
516+
@TODO Linux libnotify and Windows
517+
:param raw_message:
518+
:param markdown_text:
519+
:return:
520+
"""
521+
user = self.store.find_user_by_id(raw_message.get('user'))
522+
sender_name = self.store.get_user_display_name(user)
523+
524+
# TODO Checking bot
525+
if raw_message.get('channel')[0] == 'D':
526+
notification_title = 'New message in {}'.format(
527+
self.store.state.auth['team']
528+
)
529+
else:
530+
notification_title = 'New message in {} #{}'.format(
531+
self.store.state.auth['team'],
532+
self.store.get_channel_name(raw_message.get('channel')),
533+
)
534+
535+
sub_title = sender_name
536+
537+
if platform.system() == 'Darwin':
538+
# Macos
539+
import pync
540+
pync.notify(
541+
markdown_text.render_text(),
542+
title=notification_title,
543+
subtitle=sub_title,
544+
appIcon='./resources/slack_icon.png'
545+
)
546+
else:
547+
pass
548+
451549
@asyncio.coroutine
452550
def _go_to_channel(self, channel_id):
453551
with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
@@ -527,6 +625,7 @@ def start_real_time(self):
527625
def stop_typing(*args):
528626
self.chatbox.message_box.typing = None
529627
alarm = None
628+
530629
while self.store.slack.server.connected is True:
531630
events = self.store.slack.rtm_read()
532631
for event in events:
@@ -537,9 +636,8 @@ def stop_typing(*args):
537636
for channel in self.sidebar.channels:
538637
if channel.id == event['channel']:
539638
channel.set_unread(unread)
540-
elif event.get('channel') == self.store.state.channel['id']:
541-
if event['type'] == 'message':
542-
# Delete message
639+
elif event['type'] == 'message':
640+
if event.get('channel') == self.store.state.channel['id']:
543641
if event.get('subtype') == 'message_deleted':
544642
for widget in self.chatbox.body.body:
545643
if hasattr(widget, 'ts') and getattr(widget, 'ts') == event['deleted_ts']:
@@ -553,16 +651,23 @@ def stop_typing(*args):
553651
else:
554652
self.chatbox.body.body.extend(self.render_messages([event]))
555653
self.chatbox.body.scroll_to_bottom()
556-
elif event['type'] == 'user_typing':
654+
else:
655+
pass
656+
657+
if event.get('subtype') != 'message_deleted' and event.get('subtype') != 'message_changed':
658+
# Notification
659+
self.notification_messages([event])
660+
elif event['type'] == 'user_typing':
661+
if event.get('channel') == self.store.state.channel['id']:
557662
user = self.store.find_user_by_id(event['user'])
558-
name = user.get('display_name') or user.get('real_name') or user['name']
663+
name = self.store.get_user_display_name(user)
664+
559665
if alarm is not None:
560666
self.urwid_loop.remove_alarm(alarm)
561667
self.chatbox.message_box.typing = name
562668
self.urwid_loop.set_alarm_in(3, stop_typing)
563669
else:
564670
pass
565-
# print(json.dumps(event, indent=2))
566671
elif event.get('ok', False):
567672
# Message was sent, Slack confirmed it.
568673
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)