Skip to content

Commit c4d19fc

Browse files
Add notification when received message
1 parent ca6eddb commit c4d19fc

File tree

8 files changed

+153
-11
lines changed

8 files changed

+153
-11
lines changed

README.md

+21-3
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

+1-1
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

+110-6
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@
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 time
1012
import traceback
1113
import tempfile
1214
import urwid
1315
from datetime import datetime
16+
1417
from sclack.components import Attachment, Channel, ChannelHeader, ChatBox, Dm
1518
from sclack.components import Indicators, MarkdownText, Message, MessageBox
1619
from sclack.components import NewMessagesDivider, Profile, ProfileSideBar
@@ -84,6 +87,48 @@ def __init__(self, config):
8487
)
8588
self.configure_screen(self.urwid_loop.screen)
8689
self.last_keypress = (0, None)
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
87132

88133
def start(self):
89134
self._loading = True
@@ -140,6 +185,8 @@ def mount_sidebar(self, executor):
140185
loop.run_in_executor(executor, self.store.load_users),
141186
loop.run_in_executor(executor, self.store.load_user_dnd),
142187
)
188+
self.mentioned_patterns = self.get_mentioned_patterns()
189+
143190
profile = Profile(name=self.store.state.auth['user'], is_snoozed=self.store.state.is_snoozed)
144191
channels = [
145192
Channel(
@@ -158,7 +205,7 @@ def mount_sidebar(self, executor):
158205
if user:
159206
dms.append(Dm(
160207
dm['id'],
161-
name=user.get('display_name') or user.get('real_name') or user['name'],
208+
name=self.store.get_user_display_name(user),
162209
user=dm['user'],
163210
you=user['id'] == self.store.state.auth['user_id']
164211
))
@@ -286,7 +333,7 @@ def go_to_profile(self, user_id):
286333
return
287334
self.store.state.profile_user_id = user_id
288335
profile = ProfileSideBar(
289-
user.get('display_name') or user.get('real_name') or user['name'],
336+
self.store.get_user_display_name(user),
290337
user['profile'].get('status_text', None),
291338
user['profile'].get('tz_label', None),
292339
user['profile'].get('phone', None),
@@ -302,7 +349,7 @@ def render_chatbox_header(self):
302349
if self.store.state.channel['id'][0] == 'D':
303350
user = self.store.find_user_by_id(self.store.state.channel['user'])
304351
header = ChannelHeader(
305-
name=user.get('display_name') or user.get('real_name') or user['name'],
352+
name=self.store.get_user_display_name(user),
306353
topic=user['profile']['status_text'],
307354
is_starred=self.store.state.channel.get('is_starred', False),
308355
is_dm_workaround_please_remove_me=True
@@ -324,6 +371,16 @@ def on_change_topic(self, text):
324371
self.store.set_topic(self.store.state.channel['id'], text)
325372
self.go_to_sidebar()
326373

374+
def notification_messages(self, messages):
375+
"""
376+
Check and send notifications
377+
:param messages:
378+
:return:
379+
"""
380+
for message in messages:
381+
if self.should_notify_me(message):
382+
self.send_notification(message, MarkdownText(message['text']))
383+
327384
def render_message(self, message, channel_id=None):
328385
is_app = False
329386
subtype = message.get('subtype')
@@ -375,6 +432,7 @@ def render_message(self, message, channel_id=None):
375432
return None
376433

377434
user_id = user['id']
435+
# TODO
378436
user_name = user['profile']['display_name'] or user.get('name')
379437
color = user.get('color')
380438
if message.get('file'):
@@ -387,6 +445,7 @@ def render_message(self, message, channel_id=None):
387445
return None
388446

389447
user_id = user['id']
448+
# TODO
390449
user_name = user['profile']['display_name'] or user.get('name')
391450
color = user.get('color')
392451

@@ -399,6 +458,7 @@ def render_message(self, message, channel_id=None):
399458
]
400459

401460
attachments = []
461+
402462
for attachment in message.get('attachments', []):
403463
attachment_widget = Attachment(
404464
service_name=attachment.get('service_name'),
@@ -475,8 +535,9 @@ def render_messages(self, messages, channel_id=None):
475535
previous_date = self.store.state.last_date
476536
last_read_datetime = datetime.fromtimestamp(float(self.store.state.channel.get('last_read', '0')))
477537
today = datetime.today().date()
478-
for message in messages:
479-
message_datetime = datetime.fromtimestamp(float(message['ts']))
538+
539+
for raw_message in messages:
540+
message_datetime = datetime.fromtimestamp(float(raw_message['ts']))
480541
message_date = message_datetime.date()
481542
date_text = None
482543
unread_text = None
@@ -505,6 +566,42 @@ def render_messages(self, messages, channel_id=None):
505566

506567
return _messages
507568

569+
def send_notification(self, raw_message, markdown_text):
570+
"""
571+
Only MacOS
572+
@TODO Linux libnotify and Windows
573+
:param raw_message:
574+
:param markdown_text:
575+
:return:
576+
"""
577+
user = self.store.find_user_by_id(raw_message.get('user'))
578+
sender_name = self.store.get_user_display_name(user)
579+
580+
# TODO Checking bot
581+
if raw_message.get('channel')[0] == 'D':
582+
notification_title = 'New message in {}'.format(
583+
self.store.state.auth['team']
584+
)
585+
else:
586+
notification_title = 'New message in {} #{}'.format(
587+
self.store.state.auth['team'],
588+
self.store.get_channel_name(raw_message.get('channel')),
589+
)
590+
591+
sub_title = sender_name
592+
593+
if platform.system() == 'Darwin':
594+
# Macos
595+
import pync
596+
pync.notify(
597+
markdown_text.render_text(),
598+
title=notification_title,
599+
subtitle=sub_title,
600+
appIcon='./resources/slack_icon.png'
601+
)
602+
else:
603+
pass
604+
508605
def handle_mark_read(self, data):
509606
"""
510607
Mark as read to bottom
@@ -633,6 +730,7 @@ def stop_typing(*args):
633730
self.chatbox.message_box.typing = None
634731

635732
alarm = None
733+
636734
while self.store.slack.server.connected is True:
637735
events = self.store.slack.rtm_read()
638736
for event in events:
@@ -664,10 +762,15 @@ def stop_typing(*args):
664762
self.chatbox.body.scroll_to_bottom()
665763
else:
666764
pass
765+
766+
if event.get('subtype') != 'message_deleted' and event.get('subtype') != 'message_changed':
767+
# Notification
768+
self.notification_messages([event])
667769
elif event['type'] == 'user_typing':
668770
if event.get('channel') == self.store.state.channel['id']:
669771
user = self.store.find_user_by_id(event['user'])
670-
name = user.get('display_name') or user.get('real_name') or user['name']
772+
name = self.store.get_user_display_name(user)
773+
671774
if alarm is not None:
672775
self.urwid_loop.remove_alarm(alarm)
673776
self.chatbox.message_box.typing = name
@@ -799,6 +902,7 @@ def ask_for_token(json_config):
799902
config_file.write(json.dumps(token_config, indent=False))
800903
json_config.update(token_config)
801904

905+
802906
if __name__ == '__main__':
803907
json_config = {}
804908
with open('./config.json', 'r') as config_file:

config.json

+2-1
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

+1
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

+4
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

+14
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 = []
@@ -151,6 +152,19 @@ def load_channels(self):
151152
self.state.channels.sort(key=lambda channel: channel['name'])
152153
self.state.dms.sort(key=lambda dm: dm['created'])
153154

155+
def get_channel_name(self, channel_id):
156+
matched_channel = None
157+
158+
for channel in self.state.channels:
159+
if channel['id'] == channel_id:
160+
matched_channel = channel
161+
break
162+
163+
if matched_channel:
164+
return matched_channel['name']
165+
166+
return channel_id
167+
154168
def load_groups(self):
155169
self.state.groups = self.slack.api_call('mpim.list')['groups']
156170

0 commit comments

Comments
 (0)