Skip to content

Add notification #64

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,7 @@ Your `~/.sclack` file will look like:

### Multiple workspaces

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

```json
{
Expand Down Expand Up @@ -154,11 +153,25 @@ The mouse support also has been programmed. You can scroll the chatbox and the s
"emoji": true,
"markdown": true,
"pictures": true,
"browser": ""
"browser": "",
"notification": ""
}
}
```
* `browser`: Config your preferable browser to open the link, when ever you focus on chat box text which contains external link (http/https), press enter key, the link will be opened. Valid [value](https://docs.python.org/2/library/webbrowser.html#webbrowser.get). Example you can config `"browser": "chrome"`
* `browser`: Config your preferable browser to open the link, when ever you focus on chat box text which contains external link (http/https), press enter key, the link will be opened. Valid [value](https://docs.python.org/2/library/webbrowser.html#webbrowser.get). Example you can config `"browser": "chrome"`
* `notification`: How do you want to receive notification. `all` receive all; `none` disable notification, `mentioned` Only mentioned and direct message

#### Notification

Supported:
* Linux
* Macos >= 10.10 use [terminal-notifier](https://github.com/julienXX/terminal-notifier), you can install your custom terminal-notifier or using default binary in pync package

To test your notification availability, trigger below command, if you can see notification you can use this feature

```bash
python sclack/notification.py
```

## Tested Terminals

Expand Down
6 changes: 3 additions & 3 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@
- [x] Show indicator when a message is edited
- [x] Live events
- [x] Post message
- [x] 'Do not disturb' status
- [ ] Header for direct message
- [ ] Load more on up
- [ ] Unread messages indicator
- [ ] Navigate throught users and conversations I don't belong to.
- [ ] Navigate thought users and conversations I don't belong to.
- [ ] Publish on PIP
- [ ] Group messages
- [ ] Update documentation and screenshots

# Good to have

- React to a message
- 'Do not disturb' status
- Integration with reminders
- Handle slash commands
- RTM events (see https://api.slack.com/rtm)
- RTM events (see https://api.slack.com/rtm)
112 changes: 104 additions & 8 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@
import concurrent.futures
import functools
import json
import re
import os
import requests
import sys
import platform
import time
import traceback
import tempfile
import urwid
from datetime import datetime

from sclack.components import Attachment, Channel, ChannelHeader, ChatBox, Dm
from sclack.components import Indicators, MarkdownText, MessageBox
from sclack.component.message import Message
Expand All @@ -22,9 +25,11 @@
from sclack.quick_switcher import QuickSwitcher
from sclack.store import Store
from sclack.themes import themes
from sclack.notification import TerminalNotifier

from sclack.widgets.set_snooze import SetSnoozeWidget
from sclack.utils.channel import is_dm, is_group, is_channel
from sclack.utils.message import get_mentioned_patterns

loop = asyncio.get_event_loop()

Expand Down Expand Up @@ -90,6 +95,38 @@ def __init__(self, config):
)
self.configure_screen(self.urwid_loop.screen)
self.last_keypress = (0, None)
self.mentioned_patterns = None

def get_mentioned_patterns(self):
return get_mentioned_patterns(self.store.state.auth['user_id'])

def should_notify_me(self, message_obj):
"""
Checking whether notify to user
:param message_obj:
:return:
"""
# Snoozzzzzed or disabled
if self.store.state.is_snoozed or self.config['features']['notification'] in ['', 'none']:
return False

# You send message, don't need notification
if message_obj.get('user') == self.store.state.auth['user_id']:
return False

if self.config['features']['notification'] == 'all':
return True

# Private message
if message_obj.get('channel') is not None and message_obj.get('channel')[0] == 'D':
return True

regex = self.mentioned_patterns
if regex is None:
regex = self.get_mentioned_patterns()
self.mentioned_patterns = regex

return len(re.findall(regex, message_obj['text'])) > 0

def start(self):
self._loading = True
Expand Down Expand Up @@ -151,6 +188,8 @@ def mount_sidebar(self, executor):
loop.run_in_executor(executor, self.store.load_users),
loop.run_in_executor(executor, self.store.load_user_dnd),
)
self.mentioned_patterns = self.get_mentioned_patterns()

profile = Profile(name=self.store.state.auth['user'], is_snoozed=self.store.state.is_snoozed)

channels = []
Expand Down Expand Up @@ -345,7 +384,7 @@ def go_to_profile(self, user_id):
return
self.store.state.profile_user_id = user_id
profile = ProfileSideBar(
user.get('display_name') or user.get('real_name') or user['name'],
self.store.get_user_display_name(user),
user['profile'].get('status_text', None),
user['profile'].get('tz_label', None),
user['profile'].get('phone', None),
Expand All @@ -361,7 +400,7 @@ def render_chatbox_header(self):
if self.store.state.channel['id'][0] == 'D':
user = self.store.find_user_by_id(self.store.state.channel['user'])
header = ChannelHeader(
name=user.get('display_name') or user.get('real_name') or user['name'],
name=self.store.get_user_display_name(user),
topic=user['profile']['status_text'],
is_starred=self.store.state.channel.get('is_starred', False),
is_dm_workaround_please_remove_me=True
Expand All @@ -383,6 +422,18 @@ def on_change_topic(self, text):
self.store.set_topic(self.store.state.channel['id'], text)
self.go_to_sidebar()

def notification_messages(self, messages):
"""
Check and send notifications
:param messages:
:return:
"""
for message in messages:
if self.should_notify_me(message):
loop.create_task(
self.send_notification(message, MarkdownText(message['text']))
)

def render_message(self, message, channel_id=None):
is_app = False
subtype = message.get('subtype')
Expand Down Expand Up @@ -458,6 +509,7 @@ def render_message(self, message, channel_id=None):
]

attachments = []

for attachment in message.get('attachments', []):
attachment_widget = Attachment(
service_name=attachment.get('service_name'),
Expand Down Expand Up @@ -538,8 +590,9 @@ def render_messages(self, messages, channel_id=None):
previous_date = self.store.state.last_date
last_read_datetime = datetime.fromtimestamp(float(self.store.state.channel.get('last_read', '0')))
today = datetime.today().date()
for message in messages:
message_datetime = datetime.fromtimestamp(float(message['ts']))

for raw_message in messages:
message_datetime = datetime.fromtimestamp(float(raw_message['ts']))
message_date = message_datetime.date()
date_text = None
unread_text = None
Expand All @@ -561,13 +614,50 @@ def render_messages(self, messages, channel_id=None):
elif date_text is not None:
_messages.append(TextDivider(('history_date', date_text), 'center'))

message = self.render_message(message, channel_id)
message = self.render_message(raw_message, channel_id)

if message is not None:
_messages.append(message)

return _messages

@asyncio.coroutine
def send_notification(self, raw_message, markdown_text):
"""
Only MacOS and Linux
@TODO Windows
:param raw_message:
:param markdown_text:
:return:
"""
user = self.store.find_user_by_id(raw_message.get('user'))
sender_name = self.store.get_user_display_name(user)

if raw_message.get('channel')[0] == 'D':
notification_title = 'New message in {}'.format(
self.store.state.auth['team']
)
else:
notification_title = 'New message in {} #{}'.format(
self.store.state.auth['team'],
self.store.get_channel_name(raw_message.get('channel')),
)


icon_path = os.path.realpath(
os.path.join(
os.path.dirname(__file__),
'resources/slack_icon.png'
)
)
TerminalNotifier().notify(
str(markdown_text),
title=notification_title,
subtitle=sender_name,
appIcon=icon_path,
sound='default'
)

def handle_mark_read(self, data):
"""
Mark as read to bottom
Expand Down Expand Up @@ -760,13 +850,18 @@ def stop_typing(*args):
self.chatbox.body.scroll_to_bottom()
else:
pass

if event.get('subtype') != 'message_deleted' and event.get('subtype') != 'message_changed':
# Notification
self.notification_messages([event])
elif event['type'] == 'user_typing':
if not self.is_chatbox_rendered:
return

if event.get('channel') == self.store.state.channel['id']:
user = self.store.find_user_by_id(event['user'])
name = user.get('display_name') or user.get('real_name') or user['name']
name = self.store.get_user_display_name(user)

if alarm is not None:
self.urwid_loop.remove_alarm(alarm)
self.chatbox.message_box.typing = name
Expand All @@ -775,8 +870,8 @@ def stop_typing(*args):
pass
# print(json.dumps(event, indent=2))
elif event.get('type') == 'dnd_updated' and 'dnd_status' in event:
self.store.is_snoozed = event['dnd_status']['snooze_enabled']
self.sidebar.profile.set_snooze(self.store.is_snoozed)
self.store.state.is_snoozed = event['dnd_status']['snooze_enabled']
self.sidebar.profile.set_snooze(self.store.state.is_snoozed)
elif event.get('ok', False):
if not self.is_chatbox_rendered:
return
Expand Down Expand Up @@ -925,6 +1020,7 @@ def ask_for_token(json_config):
config_file.write(json.dumps(token_config, indent=False))
json_config.update(token_config)


if __name__ == '__main__':
json_config = {}
with open('./config.json', 'r') as config_file:
Expand Down
3 changes: 2 additions & 1 deletion config.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"emoji": true,
"markdown": true,
"pictures": true,
"browser": ""
"browser": "",
"notification": ""
},
"icons": {
"block": "\u258C",
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ pyperclip==1.6.2
requests
slackclient==1.2.1
urwid_readline
git+git://github.com/duynguyenhoang/pync@994fbf77360a273fac1225558de01c8d0040dc6c#egg=pync
Binary file added resources/slack_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions sclack/markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def parse_message(self, text):
self._state = 'message'
self._previous_state = 'message'
self._result = []

def render_emoji(result):
return emoji_codemap.get(result.group(1), result.group(0))

Expand Down Expand Up @@ -72,3 +73,6 @@ def render_emoji(result):

self._result.append(('message', self.decode_buffer()))
return self._result

def __str__(self):
return urwid.Text(self.markup).text
Loading