aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md15
-rw-r--r--wee_slack.py699
2 files changed, 454 insertions, 260 deletions
diff --git a/README.md b/README.md
index 0296092..a69df48 100644
--- a/README.md
+++ b/README.md
@@ -3,15 +3,19 @@
wee-slack
=========
+**News:**
+ The 0.98.3+ releases have some big backend changes that should make startup and multi-group much faster. Please report any bugs to the Freenode IRC channel #wee-slack.
+
A WeeChat native client for Slack.com. Provides supplemental features only available in the web/mobile clients such as: synchronizing read markers, typing notification, search, (and more)! Connects via the Slack API, and maintains a persistent websocket for notification of events.
![animated screenshot](https://dl.dropboxusercontent.com/u/566560/slack.gif)
Features
--------
- * **New** edited messages work just like the official clients, where the original message changes and has (edited) appended.
- * **New** unfurled urls dont generate a new message, but replace the original with more info as it is received.
- * **New** regex style message editing (s/oldtext/newtext/)
+ * **New** Emoji reactions!
+ * Edited messages work just like the official clients, where the original message changes and has (edited) appended.
+ * Unfurled urls dont generate a new message, but replace the original with more info as it is received.
+ * Regex style message editing (s/oldtext/newtext/)
* Caches message history, making startup MUCH faster
* Smarter redraw of dynamic buffer info (much lower CPU %)
* beta UTF-8 support
@@ -71,7 +75,10 @@ pip install websocket-client
sudo apt-get install curl
pip install websocket-client
```
-
+##### FreeBSD
+```
+pkg install curl py27-websocket-client py27-six
+```
####2. copy wee_slack.py to ~/.weechat/python/autoload
```
diff --git a/wee_slack.py b/wee_slack.py
index f1b9646..1a497bc 100644
--- a/wee_slack.py
+++ b/wee_slack.py
@@ -8,9 +8,9 @@ import pickle
import sha
import re
import urllib
-import urlparse
import HTMLParser
import sys
+import traceback
from websocket import create_connection
# hack to make tests possible.. better way?
@@ -21,11 +21,12 @@ except:
SCRIPT_NAME = "slack_extension"
SCRIPT_AUTHOR = "Ryan Huber <rhuber@gmail.com>"
-SCRIPT_VERSION = "0.97.26"
+SCRIPT_VERSION = "0.98.4"
SCRIPT_LICENSE = "MIT"
SCRIPT_DESC = "Extends weechat for typing notification/search/etc on slack.com"
BACKLOG_SIZE = 200
+SCROLLBACK_SIZE = 2000
SLACK_API_TRANSLATOR = {
"channel": {
@@ -53,6 +54,9 @@ NICK_GROUP_HERE = "0|Here"
NICK_GROUP_AWAY = "1|Away"
def dbg(message, fout=False, main_buffer=False):
+ """
+ send debug output to the slack-debug buffer and optionally write to a file.
+ """
message = "DEBUG: {}".format(message)
#message = message.encode('utf-8', 'replace')
if fout:
@@ -63,97 +67,33 @@ def dbg(message, fout=False, main_buffer=False):
if slack_debug is not None:
w.prnt(slack_debug, message)
-# hilarious, i know
-
-
-class Meta(list):
-
- def __init__(self, attribute, search_list):
- self.attribute = attribute
- self.search_list = search_list
-
- def __str__(self):
- string = ''
- for each in self.search_list.get_all(self.attribute):
- string += "{} ".format(each)
- return string
-
- def __repr__(self):
- self.search_list.get_all(self.attribute)
-
- def __getitem__(self, index):
- things = self.get_all()
- return things[index]
-
- def __iter__(self):
- things = self.get_all()
- for channel in things:
- yield channel
-
- def get_all(self):
- items = []
- items += self.search_list.get_all(self.attribute)
- return items
-
- def find(self, name):
- items = self.search_list.find_deep(name, self.attribute)
- items = [x for x in items if x is not None]
- if len(items) == 1:
- return items[0]
- elif len(items) == 0:
- pass
- else:
- dbg("probably something bad happened with meta items: {}".format(items))
- return items
- #raise AmbiguousProblemError
-
- def find_first(self, name):
- items = self.find(name)
- if items.__class__ == list:
- return items[0]
- else:
- return False
-
- def find_by_class(self, class_name):
- items = self.search_list.find_by_class_deep(class_name, self.attribute)
- return items
-
class SearchList(list):
+ """
+ A normal python list with some syntactic sugar for searchability
+ """
+ def __init__(self):
+ self.hashtable = {}
+ super(SearchList, self).__init__(self)
def find(self, name):
- items = []
- for child in self:
- if child.__class__ == self.__class__:
- items += child.find(name)
- else:
- if child == name:
- items.append(child)
- if len(items) == 1:
- return items[0]
- elif items != []:
- return items
-
- def find_deep(self, name, attribute):
- items = []
- for child in self:
- if child.__class__ == self.__class__:
- if items is not None:
- items += child.find_deep(name, attribute)
- elif dir(child).count('find') == 1:
- if items is not None:
- items.append(child.find(name, attribute))
- if items != []:
- return items
-
- def get_all(self, attribute):
- items = []
+ if name in self.hashtable.keys():
+ return self.hashtable[name]
+ #this is a fallback to __eq__ if the item isn't in the hashtable already
+ if self.count(name) > 0:
+ self.update_hashtable()
+ return self[self.index(name)]
+
+ def append(self, item, aliases=[]):
+ super(SearchList, self).append(item)
+ self.update_hashtable()
+
+ def update_hashtable(self):
for child in self:
- if child.__class__ == self.__class__:
- items += child.get_all(attribute)
- else:
- items += (eval("child." + attribute))
- return items
+ if hasattr(child, "get_aliases"):
+ for alias in child.get_aliases():
+ if alias is not None:
+ self.hashtable[alias] = child
def find_by_class(self, class_name):
items = []
@@ -173,7 +113,9 @@ class SearchList(list):
class SlackServer(object):
-
+ """
+ Root object used to represent connection and state of the connection to a slack group.
+ """
def __init__(self, token):
self.nick = None
self.name = None
@@ -206,6 +148,18 @@ class SlackServer(object):
def __repr__(self):
return "{}".format(self.identifier)
+ def add_user(self, user):
+ self.users.append(user, user.get_aliases())
+ users.append(user, user.get_aliases())
+
+ def add_channel(self, channel):
+ self.channels.append(channel, channel.get_aliases())
+ channels.append(channel, channel.get_aliases())
+
+ def get_aliases(self):
+ aliases = [self.identifier, self.token, self.buffer]
+ return aliases
+
def find(self, name, attribute):
attribute = eval("self." + attribute)
return attribute.find(name)
@@ -297,13 +251,14 @@ class SlackServer(object):
self.ws_hook = w.hook_fd(self.ws.sock._sock.fileno(), 1, 0, 0, "slack_websocket_cb", self.identifier)
self.ws.sock.setblocking(0)
return True
- except:
+ except Exception as e:
+ print("websocket connection error: {}".format(e))
return False
def create_slack_mappings(self, data):
for item in data["users"]:
- self.users.append(User(self, item["name"], item["id"], item["presence"], item["deleted"]))
+ self.add_user(User(self, item["name"], item["id"], item["presence"], item["deleted"]))
for item in data["channels"]:
if "last_read" not in item:
@@ -314,17 +269,17 @@ class SlackServer(object):
item["topic"] = {}
item["topic"]["value"] = ""
if not item["is_archived"]:
- self.channels.append(Channel(self, item["name"], item["id"], item["is_member"], item["last_read"], "#", item["members"], item["topic"]["value"]))
+ self.add_channel(Channel(self, item["name"], item["id"], item["is_member"], item["last_read"], "#", item["members"], item["topic"]["value"]))
for item in data["groups"]:
if "last_read" not in item:
item["last_read"] = 0
if not item["is_archived"]:
- self.channels.append(GroupChannel(self, item["name"], item["id"], item["is_open"], item["last_read"], "#", item["members"], item["topic"]["value"]))
+ self.add_channel(GroupChannel(self, item["name"], item["id"], item["is_open"], item["last_read"], "#", item["members"], item["topic"]["value"]))
for item in data["ims"]:
if "last_read" not in item:
item["last_read"] = 0
name = self.users.find(item["user"]).name
- self.channels.append(DmChannel(self, name, item["id"], item["is_open"], item["last_read"]))
+ self.add_channel(DmChannel(self, name, item["id"], item["is_open"], item["last_read"]))
for item in self.channels:
item.get_history()
@@ -341,26 +296,11 @@ class SlackServer(object):
pass
#w.prnt("", "%s\t%s" % (user, message))
-
-class SlackThing(object):
-
- def __init__(self, name, identifier):
- self.name = name
- self.identifier = identifier
- self.channel_buffer = None
-
- def __str__(self):
- return self.name
-
- def __repr__(self):
- return self.name
-
-
def buffer_input_cb(b, buffer, data):
- if not data.startswith('s/'):
+ if not data.startswith('s/') or data.startswith('+'):
channel = channels.find(buffer)
channel.send_message(data)
- channel.buffer_prnt(channel.server.nick, data)
+ #channel.buffer_prnt(channel.server.nick, data)
elif data.count('/') == 3:
old, new = data.split('/')[1:3]
channel = channels.find(buffer)
@@ -369,33 +309,58 @@ def buffer_input_cb(b, buffer, data):
return w.WEECHAT_RC_ERROR
-class Channel(SlackThing):
-
+class Channel(object):
+ """
+ Represents a single channel and is the source of truth
+ for channel <> weechat buffer
+ """
def __init__(self, server, name, identifier, active, last_read=0, prepend_name="", members=[], topic=""):
- super(Channel, self).__init__(name, identifier)
+ self.name = prepend_name + name
+ self.identifier = identifier
+ self.active = active
+ self.last_read = float(last_read)
+ self.members = set(members)
+ self.topic = topic
+
+ self.members_table = {}
+ self.channel_buffer = None
self.type = "channel"
self.server = server
- self.name = prepend_name + self.name
self.typing = {}
- self.active = active
self.opening = False
- self.members = set(members)
- self.topic = topic
- self.last_read = float(last_read)
self.last_received = None
+ self.messages = []
+ self.scrolling = False
if active:
self.create_buffer()
self.attach_buffer()
+ self.create_members_table()
self.update_nicklist()
self.set_topic(self.topic)
buffer_list_update_next()
+ def __str__(self):
+ return self.name
+
+ def __repr__(self):
+ return self.name
+
def __eq__(self, compare_str):
if compare_str == self.fullname() or compare_str == self.name or compare_str == self.identifier or compare_str == self.name[1:] or (compare_str == self.channel_buffer and self.channel_buffer is not None):
return True
else:
return False
+ def get_aliases(self):
+ aliases = [self.fullname(), self.name, self.identifier, self.name[1:], ]
+ if self.channel_buffer is not None:
+ aliases.append(self.channel_buffer)
+ return aliases
+
+ def create_members_table(self):
+ for user in self.members:
+ self.members_table[user] = self.server.users.find(user)
+
def create_buffer(self):
channel_buffer = w.buffer_search("", "{}.{}".format(self.server.domain, self.name))
if channel_buffer:
@@ -434,7 +399,7 @@ class Channel(SlackThing):
'', NICK_GROUP_AWAY, "weechat.color.nicklist_group", 1)
try:
for user in self.members:
- user = self.server.users.find(user)
+ user = self.members_table[user]
if user.deleted:
continue
if user.presence == 'away':
@@ -442,7 +407,7 @@ class Channel(SlackThing):
else:
w.nicklist_add_nick(self.channel_buffer, here, user.name, user.color_name, "", "", 1)
except Exception as e:
- print "DEBUG: {} {} {}".format(self.identifier, self.name, e)
+ dbg("DEBUG: {} {} {}".format(self.identifier, self.name, e))
def fullname(self):
return "{}.{}".format(self.server.domain, self.name)
@@ -452,11 +417,13 @@ class Channel(SlackThing):
def user_join(self, name):
self.members.add(name)
+ self.create_members_table()
self.update_nicklist()
def user_leave(self, name):
if name in self.members:
self.members.remove(name)
+ self.create_members_table()
self.update_nicklist()
def set_active(self):
@@ -487,14 +454,14 @@ class Channel(SlackThing):
for item in enumerate(message):
if item[1].startswith('@') and len(item[1]) > 1:
named = re.match('.*[@#](\w+)(\W*)', item[1]).groups()
- if named[0] in ["group", "channel"]:
+ if named[0] in ["group", "channel", "here"]:
message[item[0]] = "<!{}>".format(named[0])
if self.server.users.find(named[0]):
- message[item[0]] = "<@{}>{}".format(self.server.users.find(named[0]).identifier, named[1])
+ message[item[0]] = "<@{}|{}>{}".format(self.server.users.find(named[0]).identifier, named[0], named[1])
if item[1].startswith('#') and self.server.channels.find(item[1]):
named = re.match('.*[@#](\w+)(\W*)', item[1]).groups()
if self.server.channels.find(named[0]):
- message[item[0]] = "<#{}>{}".format(self.server.channels.find(named[0]).identifier, named[1])
+ message[item[0]] = "<#{}|{}>{}".format(self.server.channels.find(named[0]).identifier, named[0], named[1])
dbg(message)
return " ".join(message)
@@ -524,10 +491,6 @@ class Channel(SlackThing):
async_slack_api_request(self.server.domain, self.server.token, SLACK_API_TRANSLATOR[self.type]["leave"], {"channel": self.identifier})
def closed(self):
- try:
- message_cache.pop(self.identifier)
- except KeyError:
- pass
self.channel_buffer = None
self.last_received = None
self.close()
@@ -573,19 +536,23 @@ class Channel(SlackThing):
if w.buffer_get_string(self.channel_buffer, "short_name") != (color + new_name):
w.buffer_set(self.channel_buffer, "short_name", color + new_name)
- def buffer_prnt_changed(self, user, text, time, append=""):
- if user:
- if self.server.users.find(user):
- name = self.server.users.find(user).formatted_name()
- else:
- name = user
- name = name.decode('utf-8')
- modify_buffer_line(self.channel_buffer, name, text, time, append)
- else:
- modify_buffer_line(self.channel_buffer, None, text, time, append)
- return False
-
- def buffer_prnt(self, user='unknown user', message='no message', time=0):
+# deprecated in favor of redrawing the entire buffer
+# def buffer_prnt_changed(self, user, text, time, append=""):
+# if self.channel_buffer:
+# if user:
+# if self.server.users.find(user):
+# name = self.server.users.find(user).formatted_name()
+# else:
+# name = user
+# name = name.decode('utf-8')
+# modify_buffer_line(self.channel_buffer, name, text, time, append)
+# else:
+# modify_buffer_line(self.channel_buffer, None, text, time, append)
+
+ def buffer_prnt(self, user='unknown_user', message='no message', time=0):
+ """
+ writes output (message) to a buffer (channel)
+ """
set_read_marker = False
time_float = float(time)
if time_float != 0 and self.last_read >= time_float:
@@ -599,6 +566,8 @@ class Channel(SlackThing):
tags = "irc_smart_filter"
else:
tags = "notify_message"
+ #don't write these to local log files
+ #tags += ",no_log"
time_int = int(time_float)
if self.channel_buffer:
if self.server.users.find(user):
@@ -625,6 +594,44 @@ class Channel(SlackThing):
self.last_received = time
self.unset_typing(user)
+ def buffer_redraw(self):
+ if self.channel_buffer and not self.scrolling:
+ w.buffer_clear(self.channel_buffer)
+ self.messages.sort()
+ for message in self.messages:
+ process_message(message.message_json, False)
+
+ def set_scrolling(self):
+ self.scrolling = True
+
+ def unset_scrolling(self):
+ self.scrolling = False
+ self.buffer_redraw()
+
+ def has_message(self, ts):
+ return self.messages.count(ts) > 0
+
+ def change_message(self, ts, text):
+ if self.has_message(ts):
+ message_index = self.messages.index(ts)
+ self.messages[message_index].change_text(text)
+ self.buffer_redraw()
+ return True
+
+ def add_reaction(self, ts, reaction):
+ if self.has_message(ts):
+ message_index = self.messages.index(ts)
+ self.messages[message_index].add_reaction(reaction)
+ self.buffer_redraw()
+ return True
+
+ def remove_reaction(self, ts, reaction):
+ if self.has_message(ts):
+ message_index = self.messages.index(ts)
+ self.messages[message_index].remove_reaction(reaction)
+ self.buffer_redraw()
+ return True
+
def change_previous_message(self, old, new):
message = self.my_last_message()
if new == "" and old == "":
@@ -634,15 +641,21 @@ class Channel(SlackThing):
async_slack_api_request(self.server.domain, self.server.token, 'chat.update', {"channel": self.identifier, "ts": message['ts'], "text": new_message})
def my_last_message(self):
- for message in reversed(message_cache[self.identifier]):
- if "user" in message and "text" in message and message["user"] == self.server.users.find(self.server.nick).identifier:
- return message
+ for message in reversed(self.messages):
+ if "user" in message.message_json and "text" in message.message_json and message.message_json["user"] == self.server.users.find(self.server.nick).identifier:
+ return message.message_json
+
+ def cache_message(self, message_json, from_me=False):
+ if from_me:
+ message_json["user"] = self.server.users.find(self.server.nick).identifier
+ self.messages.append(Message(message_json))
+ if len(self.messages) > SCROLLBACK_SIZE:
+ self.messages = self.messages[-SCROLLBACK_SIZE:]
def get_history(self):
if self.active:
- if self.identifier in message_cache.keys():
- for message in message_cache[self.identifier]:
- process_message(message)
+ for message in cache_get(self.identifier):
+ process_message(json.loads(message), True)
if self.last_received != None:
async_slack_api_request(self.server.domain, self.server.token, SLACK_API_TRANSLATOR[self.type]["history"], {"channel": self.identifier, "oldest": self.last_received, "count": BACKLOG_SIZE})
else:
@@ -677,14 +690,16 @@ class DmChannel(Channel):
w.buffer_set(self.channel_buffer, "short_name", new_name)
-class User(SlackThing):
+class User(object):
def __init__(self, server, name, identifier, presence="away", deleted=False):
- super(User, self).__init__(name, identifier)
- self.channel_buffer = w.info_get("irc_buffer", "{}.{}".format(domain, self.name))
- self.deleted = deleted
- self.presence = presence
self.server = server
+ self.name = name
+ self.identifier = identifier
+ self.presence = presence
+ self.deleted = deleted
+
+ self.channel_buffer = w.info_get("irc_buffer", "{}.{}".format(domain, self.name))
self.update_color()
self.name_regex = re.compile(r"([\W]|\A)(@{0,1})" + self.name + "('s|[^'\w]|\Z)")
@@ -697,12 +712,21 @@ class User(SlackThing):
self.nicklist_pointer = w.nicklist_add_nick(server.buffer, ngroup, self.name, self.color_name, "", "", 1)
# w.nicklist_add_nick(server.buffer, "", self.formatted_name(), "", "", "", 1)
+ def __str__(self):
+ return self.name
+
+ def __repr__(self):
+ return self.name
+
def __eq__(self, compare_str):
if compare_str == self.name or compare_str == "@" + self.name or compare_str == self.identifier:
return True
else:
return False
+ def get_aliases(self):
+ return [self.name, "@" + self.name, self.identifier]
+
def set_active(self):
self.presence = "active"
for channel in self.server.channels:
@@ -744,6 +768,46 @@ class User(SlackThing):
#reply = async_slack_api_request("im.open", {"channel":self.identifier,"ts":t})
async_slack_api_request(self.server.domain, self.server.token, "im.open", {"user": self.identifier, "ts": t})
+class Message(object):
+
+ def __init__(self, message_json):
+ self.message_json = message_json
+ self.ts = message_json['ts']
+ #split timestamp into time and counter
+ self.ts_time, self.ts_counter = message_json['ts'].split('.')
+
+ def change_text(self, new_text):
+ self.message_json["text"] = new_text
+
+ def add_reaction(self, reaction):
+ if "reactions" in self.message_json:
+ found = False
+ for r in self.message_json["reactions"]:
+ if r["name"] == reaction:
+ r["count"] += 1
+ found = True
+ if not found:
+ self.message_json["reactions"].append({u"count": 1, u"name": reaction})
+ else:
+ self.message_json["reactions"] = [{u"count": 1, u"name": reaction}]
+
+ def remove_reaction(self, reaction):
+ if "reactions" in self.message_json:
+ for r in self.message_json["reactions"]:
+ if r["name"] == reaction:
+ r["count"] -= 1
+ else:
+ pass
+
+ def __eq__(self, other):
+ return self.ts_time == other or self.ts == other
+
+ def __repr__(self):
+ return "{} {} {} {}\n".format(self.ts_time, self.ts_counter, self.ts, self.message_json)
+
+ def __lt__(self, other):
+ return self.ts < other.ts
+
def slack_command_cb(data, current_buffer, args):
a = args.split(' ', 1)
@@ -756,7 +820,6 @@ def slack_command_cb(data, current_buffer, args):
command = cmds[function_name](current_buffer, args)
except KeyError:
w.prnt("", "Command not found: " + function_name)
-
return w.WEECHAT_RC_OK
@@ -847,14 +910,28 @@ def command_channels(current_buffer, args):
def command_nodistractions(current_buffer, args):
global hide_distractions
hide_distractions = not hide_distractions
- if distracting_channels[0] != "":
+ if distracting_channels != ['']:
for channel in distracting_channels:
try:
- w.buffer_set(channels.find(channel).channel_buffer, "hidden", str(int(hide_distractions)))
+ channel_buffer = channels.find(channel).channel_buffer
+ if channel_buffer:
+ w.buffer_set(channels.find(channel).channel_buffer, "hidden", str(int(hide_distractions)))
except:
dbg("Can't hide channel {}".format(channel), main_buffer=True)
+def command_distracting(current_buffer, args):
+ global distracting_channels
+ distracting_channels = [x.strip() for x in w.config_get_plugin("distracting_channels").split(',')]
+ fullname = channels.find(current_buffer).fullname()
+ if distracting_channels.count(fullname) == 0:
+ distracting_channels.append(fullname)
+ else:
+ distracting_channels.pop(distracting_channels.index(fullname))
+ new = ','.join(distracting_channels)
+ w.config_set_plugin('distracting_channels', new)
+
+
@slack_buffer_required
def command_users(current_buffer, args):
"""
@@ -875,7 +952,6 @@ def command_setallreadmarkers(current_buffer, args):
for channel in channels:
channel.mark_read()
-
def command_changetoken(current_buffer, args):
w.config_set_plugin('slack_api_token', args)
@@ -916,20 +992,9 @@ def command_markread(current_buffer, args):
if servers.find(domain).channels.find(channel):
servers.find(domain).channels.find(channel).mark_read()
-def command_cacheinfo(current_buffer, args):
- for channel in message_cache.keys():
- c = channels.find(channel)
- w.prnt("", "{} {}".format(channels.find(channel), len(message_cache[channel])))
-# server.buffer_prnt("{} {}".format(channels.find(channel), len(message_cache[channel])))
-
def command_flushcache(current_buffer, args):
global message_cache
- message_cache = {}
- cache_write_cb("","")
-
-def command_uncache(current_buffer, args):
- identifier = channels.find(current_buffer).identifier
- message_cache.pop(identifier)
+ message_cache = []
cache_write_cb("","")
def command_cachenow(current_buffer, args):
@@ -1053,9 +1118,10 @@ def process_reply(message_json):
identifier = message_json["reply_to"]
item = server.message_buffer.pop(identifier)
if "type" in item:
- if item["type"] == "message":
+ if item["type"] == "message" and "channel" in item.keys():
item["ts"] = message_json["ts"]
- cache_message(item, from_me=True)
+ channels.find(item["channel"]).cache_message(item, from_me=True)
+ channels.find(item["channel"]).buffer_prnt(item["user"], item["text"], item["ts"])
dbg("REPLY {}".format(item))
def process_pong(message_json):
@@ -1065,7 +1131,7 @@ def process_pong(message_json):
def process_team_join(message_json):
server = servers.find(message_json["myserver"])
item = message_json["user"]
- server.users.append(User(server, item["name"], item["id"], item["presence"]))
+ server.add_user(User(server, item["name"], item["id"], item["presence"]))
server.buffer_prnt(server.buffer, "New user joined: {}".format(item["name"]))
def process_manual_presence_change(message_json):
@@ -1073,13 +1139,11 @@ def process_manual_presence_change(message_json):
def process_presence_change(message_json):
server = servers.find(message_json["myserver"])
- nick = message_json.get("user", server.nick)
- buffer_name = "{}.{}".format(domain, nick)
- buf_ptr = w.buffer_search("", buffer_name)
+ identifier = message_json.get("user", server.nick)
if message_json["presence"] == 'active':
- users.find(nick).set_active()
+ server.users.find(identifier).set_active()
else:
- users.find(nick).set_inactive()
+ server.users.find(identifier).set_inactive()
def process_channel_marked(message_json):
@@ -1103,7 +1167,7 @@ def process_channel_created(message_json):
server.channels.find(message_json["channel"]["name"]).open(False)
else:
item = message_json["channel"]
- server.channels.append(Channel(server, item["name"], item["id"], False))
+ server.add_channel(Channel(server, item["name"], item["id"], False))
server.buffer_prnt("New channel created: {}".format(item["name"]))
@@ -1130,7 +1194,7 @@ def process_channel_joined(message_json):
server.channels.find(message_json["channel"]["name"]).open(False)
else:
item = message_json["channel"]
- server.channels.append(Channel(server, item["name"], item["id"], item["is_open"], item["last_read"], "#", item["members"], item["topic"]["value"]))
+ server.add_channel(Channel(server, item["name"], item["id"], item["is_open"], item["last_read"], "#", item["members"], item["topic"]["value"]))
def process_channel_leave(message_json):
@@ -1139,7 +1203,7 @@ def process_channel_leave(message_json):
channel.user_leave(message_json["user"])
-def process_channel_archive(message_json):
+def process_channel_archive(message_json):
server = servers.find(message_json["myserver"])
channel = server.channels.find(message_json["channel"])
channel.detach_buffer()
@@ -1156,7 +1220,7 @@ def process_group_joined(message_json):
server.channels.find(message_json["channel"]["name"]).open(False)
else:
item = message_json["channel"]
- server.channels.append(GroupChannel(server, item["name"], item["id"], item["is_open"], item["last_read"], "#", item["members"], item["topic"]["value"]))
+ server.add_channel(GroupChannel(server, item["name"], item["id"], item["is_open"], item["last_read"], "#", item["members"], item["topic"]["value"]))
def process_group_archive(message_json):
@@ -1189,7 +1253,7 @@ def process_im_created(message_json):
server.channels.find(channel_name).open(False)
else:
item = message_json["channel"]
- server.channels.append(DmChannel(server, channel_name, item["id"], item["is_open"], item["last_read"]))
+ server.add_channel(DmChannel(server, channel_name, item["id"], item["is_open"], item["last_read"]))
server.buffer_prnt("New channel created: {}".format(item["name"]))
@@ -1202,54 +1266,62 @@ def process_user_typing(message_json):
# todo: does this work?
-
def process_error(message_json):
pass
#connected = False
-# def process_message_changed(message_json):
-# process_message(message_json)
+def process_reaction_added(message_json):
+ channel = channels.find(message_json["item"]["channel"])
+ channel.add_reaction(message_json["item"]["ts"], message_json["reaction"])
-def cache_message(message_json, from_me=False):
- global message_cache
- if from_me:
- server = channels.find(message_json["channel"]).server
- message_json["user"] = server.users.find(server.nick).identifier
- channel = message_json["channel"]
- if channel not in message_cache:
- message_cache[channel] = []
- if message_json not in message_cache[channel]:
- message_cache[channel].append(message_json)
- if len(message_cache[channel]) > BACKLOG_SIZE:
- message_cache[channel] = message_cache[channel][-BACKLOG_SIZE:]
-
-
-def modify_buffer_line(buffer, user, new_message, time, append):
- time = int(float(time))
- own_lines = w.hdata_pointer(w.hdata_get('buffer'), buffer, 'own_lines')
- if own_lines:
- line = w.hdata_pointer(w.hdata_get('lines'), own_lines, 'last_line')
- hdata_line = w.hdata_get('line')
- hdata_line_data = w.hdata_get('line_data')
-
- while line:
- data = w.hdata_pointer(hdata_line, line, 'data')
- if data:
- date = w.hdata_time(hdata_line_data, data, 'date')
- prefix = w.hdata_string(hdata_line_data, data, 'prefix')
- if user and (int(date) == int(time) and user == prefix):
+def process_reaction_removed(message_json):
+ channel = channels.find(message_json["item"]["channel"])
+ channel.remove_reaction(message_json["item"]["ts"], message_json["reaction"])
+
+def create_reaction_string(reactions):
+ if not isinstance(reactions, list):
+ reaction_string = " [{}]".format(reactions)
+ else:
+ reaction_string = ' ['
+ count = 0
+ for r in reactions:
+ if r["count"] > 0:
+ count += 1
+ reaction_string += ":{}:{} ".format(r["name"], r["count"])
+ reaction_string = reaction_string[:-1] + ']'
+ if count == 0:
+ reaction_string = ''
+ return reaction_string
+
+# deprecated in favor of redrawing the entire buffer
+#def modify_buffer_line(buffer, user, new_message, time, append):
+# time = int(float(time))
+# own_lines = w.hdata_pointer(w.hdata_get('buffer'), buffer, 'own_lines')
+# if own_lines:
+# line = w.hdata_pointer(w.hdata_get('lines'), own_lines, 'last_line')
+# hdata_line = w.hdata_get('line')
+# hdata_line_data = w.hdata_get('line_data')
+#
+# while line:
+# data = w.hdata_pointer(hdata_line, line, 'data')
+# if data:
+# date = w.hdata_time(hdata_line_data, data, 'date')
+# prefix = w.hdata_string(hdata_line_data, data, 'prefix')
+# if new_message == "":
+# new_message = w.hdata_string(hdata_line_data, data, 'message')
+# if user and (int(date) == int(time) and user == prefix):
# w.prnt("", "found matching time date is {}, time is {} ".format(date, time))
- w.hdata_update(hdata_line_data, data, {"message": "{}{}".format(new_message, append)})
- break
- elif not user and (int(date) == int(time)):
- w.hdata_update(hdata_line_data, data, {"message": "{}{}".format(new_message, append)})
- else:
- pass
- line = w.hdata_move(hdata_line, line, -1)
- return w.WEECHAT_RC_OK
+# w.hdata_update(hdata_line_data, data, {"message": "{}{}".format(new_message, append)})
+# break
+# elif not user and (int(date) == int(time)):
+# w.hdata_update(hdata_line_data, data, {"message": "{}{}".format(new_message, append)})
+# else:
+# pass
+# line = w.hdata_move(hdata_line, line, -1)
+# return w.WEECHAT_RC_OK
-def process_message(message_json):
+def process_message(message_json, cache=True):
try:
# send these messages elsewhere
known_subtypes = ['channel_join', 'channel_leave', 'channel_topic']
@@ -1268,8 +1340,6 @@ def process_message(message_json):
dbg("message came for closed channel {}".format(channel.name))
return
- cache_message(message_json)
-
time = message_json['ts']
if "fallback" in message_json:
text = message_json["fallback"]
@@ -1278,13 +1348,22 @@ def process_message(message_json):
else:
text = ""
- text = unfurl_refs(text)
+ text = text.decode('utf-8')
+
+ ignore_alt_text = False
+ if w.config_get_plugin('unfurl_ignore_alt_text') != "0":
+ ignore_alt_text = True
+ text = unfurl_refs(text, ignore_alt_text=ignore_alt_text)
+
if "attachments" in message_json:
text += u" --- {}".format(unwrap_attachments(message_json))
text = text.lstrip()
text = text.replace("\t", " ")
name = get_user(message_json, server)
+ if "reactions" in message_json:
+ text += create_reaction_string(message_json["reactions"])
+
text = text.encode('utf-8')
name = name.encode('utf-8')
@@ -1293,11 +1372,13 @@ def process_message(message_json):
append = " (edited)"
else:
append = ''
- channel.buffer_prnt_changed(message_json["message"]["user"], text, message_json["message"]["ts"], append)
+ channel.change_message(message_json["message"]["ts"], text + append)
+ cache=False
elif "subtype" in message_json and message_json["subtype"] == "message_deleted":
append = "(deleted)"
text = ""
- channel.buffer_prnt_changed(None, text, message_json["deleted_ts"], append)
+ channel.change_message(message_json["deleted_ts"], text + append)
+ cache = False
elif message_json.get("subtype", "") == "channel_leave":
channel.buffer_prnt(w.prefix("quit").rstrip(), text, time)
elif message_json.get("subtype", "") == "channel_join":
@@ -1306,9 +1387,12 @@ def process_message(message_json):
channel.buffer_prnt(w.prefix("network").rstrip(), text, time)
else:
channel.buffer_prnt(name, text, time)
- except:
- dbg("cannot process message {}".format(message_json))
+ if cache:
+ channel.cache_message(message_json)
+
+ except Exception:
+ dbg("cannot process message {}\n{}".format(message_json, traceback.format_exc()))
def unwrap_message(message_json):
if "message" in message_json:
@@ -1337,18 +1421,28 @@ def unwrap_attachments(message_json):
return attachment_text
-def unfurl_refs(text):
+def unfurl_refs(text, ignore_alt_text=False):
+ """
+ Worst code ever written. this needs work
+ """
if text.find('<') > -1:
newtext = []
text = text.split(" ")
for item in text:
# dbg(item)
+ prefix = ""
+ suffix = ""
start = item.find('<')
end = item.find('>')
if start > -1 and end > -1:
+ prefix = item[:start]
+ suffix = item[end+1:]
item = item[start + 1:end]
if item.find('|') > -1:
- item = item.split('|')[0]
+ if ignore_alt_text:
+ item = item.split('|')[1]
+ else:
+ item = item.split('|')[0]
if item.startswith('@U'):
if users.find(item[1:]):
try:
@@ -1358,7 +1452,7 @@ def unfurl_refs(text):
if item.startswith('#C'):
if channels.find(item[1:]):
item = "{}".format(channels.find(item[1:]).name)
- newtext.append(item)
+ newtext.append(prefix + item + suffix)
text = " ".join(newtext)
return text
else:
@@ -1382,7 +1476,7 @@ def get_user(message_json, server):
def typing_bar_item_cb(data, buffer, args):
- typers = [x for x in channels.get_all() if x.is_someone_typing()]
+ typers = [x for x in channels if x.is_someone_typing()]
if len(typers) > 0:
direct_typers = []
channel_typers = []
@@ -1416,7 +1510,6 @@ def buffer_list_update_cb(data, remaining_calls):
gray_check = False
if len(servers) > 1:
gray_check = True
- # for channel in channels.find_by_class(Channel) + channels.find_by_class(GroupChannel):
for channel in channels:
channel.rename()
buffer_list_update = False
@@ -1466,15 +1559,18 @@ def typing_notification_cb(signal, sig_type, data):
typing_timer = now
return w.WEECHAT_RC_OK
-# NOTE: figured i'd do this because they do
-
-
def slack_ping_cb(data, remaining):
+ """
+ Periodic websocket ping to detect broken connection.
+ """
servers.find(data).ping()
return w.WEECHAT_RC_OK
def slack_connection_persistence_cb(data, remaining_calls):
+ """
+ Reconnect if a connection is detected down
+ """
for server in servers:
if not server.connected:
server.buffer_prnt("Disconnected from slack, trying to reconnect..")
@@ -1494,11 +1590,62 @@ def slack_never_away_cb(data, remaining):
server.send_to_websocket(request, expect_reply=False)
return w.WEECHAT_RC_OK
-# Slack specific requests
-# NOTE: switched to async/curl because sync slowed down the UI
+def nick_completion_cb(data, completion_item, buffer, completion):
+ """
+ Adds all @-prefixed nicks to completion list
+ """
+
+ channel = channels.find(buffer)
+ if channel is None or channel.members is None:
+ return w.WEECHAT_RC_OK
+ for m in channel.members:
+ user = channel.server.users.find(m)
+ w.hook_completion_list_add(completion, "@" + user.name, 1, w.WEECHAT_LIST_POS_SORT)
+ return w.WEECHAT_RC_OK
+
+
+def complete_next_cb(data, buffer, command):
+ """Extract current word, if it is equal to a nick, prefix it with @ and
+ rely on nick_completion_cb adding the @-prefixed versions to the
+ completion lists, then let Weechat's internal completion do its
+ thing
+
+ """
+
+ channel = channels.find(buffer)
+ if channel is None or channel.members is None:
+ return w.WEECHAT_RC_OK
+ input = w.buffer_get_string(buffer, "input")
+ current_pos = w.buffer_get_integer(buffer, "input_pos") - 1
+ input_length = w.buffer_get_integer(buffer, "input_length")
+ word_start = 0
+ word_end = input_length
+ # If we're on a non-word, look left for something to complete
+ while current_pos >= 0 and input[current_pos] != '@' and not input[current_pos].isalnum():
+ current_pos = current_pos - 1
+ for l in range(current_pos, 0, -1):
+ if input[l] != '@' and not input[l].isalnum():
+ word_start = l + 1
+ break
+ for l in range(current_pos, input_length):
+ if not input[l].isalnum():
+ word_end = l
+ break
+ word = input[word_start:word_end]
+ for m in channel.members:
+ user = channel.server.users.find(m)
+ if user.name == word:
+ # Here, we cheat. Insert a @ in front and rely in the @
+ # nicks being in the completion list
+ w.buffer_set(buffer, "input", input[:word_start] + "@" + input[word_start:])
+ w.buffer_set(buffer, "input_pos", str(w.buffer_get_integer(buffer, "input_pos") + 1))
+ return w.WEECHAT_RC_OK_EAT
+ return w.WEECHAT_RC_OK
+# Slack specific requests
+# NOTE: switched to async/curl because sync slowed down the UI
def async_slack_api_request(domain, token, request, post_data, priority=False):
if not STOP_TALKING_TO_SLACK:
post_data["token"] = token
@@ -1511,7 +1658,7 @@ def async_slack_api_request(domain, token, request, post_data, priority=False):
big_data = {}
def url_processor_cb(data, command, return_code, out, err):
- global big_data, message_cache
+ global big_data
data = pickle.loads(data)
identifier = sha.sha("{}{}".format(data, command)).hexdigest()
if identifier not in big_data:
@@ -1530,6 +1677,7 @@ def url_processor_cb(data, command, return_code, out, err):
if my_json:
if data["request"] == 'rtm.start':
servers.find(data["token"]).connected_to_slack(my_json)
+ servers.update_hashtable()
else:
if "channel" in data["post_data"]:
@@ -1551,10 +1699,32 @@ def url_processor_cb(data, command, return_code, out, err):
return w.WEECHAT_RC_OK
def cache_write_cb(data, remaining):
- open("{}/{}".format(WEECHAT_HOME, CACHE_NAME), 'w').write(json.dumps(message_cache))
+ cache_file = open("{}/{}".format(WEECHAT_HOME, CACHE_NAME), 'w')
+ for channel in channels:
+ if channel.active:
+ for message in channel.messages:
+ cache_file.write("{}\n".format(json.dumps(message.message_json)))
return w.WEECHAT_RC_OK
-
+def cache_load():
+ global message_cache
+ try:
+ file_name = "{}/{}".format(WEECHAT_HOME, CACHE_NAME)
+ if sum(1 for line in open('myfile.txt')) > 2:
+ cache_file = open(file_name, 'r')
+ for line in cache_file:
+ message_cache.append(line)
+ except IOError:
+ #cache file didn't exist
+ pass
+
+def cache_get(channel):
+ lines = []
+ for line in message_cache:
+ j = json.loads(line)
+ if j["channel"] == channel:
+ lines.append(line)
+ return lines
# END Slack specific requests
@@ -1632,17 +1802,33 @@ def config_changed_cb(data, option, value):
return w.WEECHAT_RC_OK
def quit_notification_cb(signal, sig_type, data):
- global STOP_TALKING_TO_SLACK
- STOP_TALKING_TO_SLACK = True
- cache_write_cb("", "")
- return w.WEECHAT_RC_OK
+ stop_talking_to_slack()
def script_unloaded():
+ stop_talking_to_slack()
+ return w.WEECHAT_RC_OK
+
+def stop_talking_to_slack():
+ """
+ Prevents a race condition where quitting closes buffers
+ which triggers leaving the channel because of how close
+ buffer is handled
+ """
global STOP_TALKING_TO_SLACK
STOP_TALKING_TO_SLACK = True
cache_write_cb("", "")
return w.WEECHAT_RC_OK
+def scrolled_cb(signal, sig_type, data):
+ try:
+ if w.window_get_integer(data, "scrolling") == 1:
+ channels.find(w.current_buffer()).set_scrolling()
+ else:
+ channels.find(w.current_buffer()).unset_scrolling()
+ except:
+ pass
+ return w.WEECHAT_RC_OK
+
# END Utility Methods
# Main
@@ -1666,6 +1852,8 @@ if __name__ == "__main__":
w.config_set_plugin('colorize_nicks', "1")
if not w.config_get_plugin('trigger_value'):
w.config_set_plugin('trigger_value', "0")
+ if not w.config_get_plugin('unfurl_ignore_alt_text'):
+ w.config_set_plugin('unfurl_ignore_alt_text', "0")
version = w.info_get("version_number", "") or 0
if int(version) >= 0x00040400:
@@ -1688,25 +1876,20 @@ if __name__ == "__main__":
buffer_list_update = False
previous_buffer_list_update = 0
- #name = None
never_away = False
hide_distractions = False
hotlist = w.infolist_get("hotlist", "", "")
main_weechat_buffer = w.info_get("irc_buffer", "{}.{}".format(domain, "DOESNOTEXIST!@#$"))
- try:
- cache_file = open("{}/{}".format(WEECHAT_HOME, CACHE_NAME), 'r')
- message_cache = json.loads(cache_file.read())
- except (IOError, ValueError):
- message_cache = {}
- # End global var section
+ message_cache = []
+ cache_load()
- #channels = SearchList()
servers = SearchList()
for token in slack_api_token.split(','):
- servers.append(SlackServer(token))
- channels = Meta('channels', servers)
- users = Meta('users', servers)
+ server = SlackServer(token)
+ servers.append(server)
+ channels = SearchList()
+ users = SearchList()
w.hook_config("plugins.var.python." + SCRIPT_NAME + ".*", "config_changed_cb", "")
w.hook_timer(3000, 0, 0, "slack_connection_persistence_cb", "")
@@ -1722,6 +1905,7 @@ if __name__ == "__main__":
w.hook_signal('window_switch', "buffer_switch_cb", "")
w.hook_signal('input_text_changed', "typing_notification_cb", "")
w.hook_signal('quit', "quit_notification_cb", "")
+ w.hook_signal('window_scrolled', "scrolled_cb", "")
w.hook_command(
# Command name and description
'slack', 'Plugin to allow typing notification and sync of read markers for slack.com',
@@ -1741,5 +1925,8 @@ if __name__ == "__main__":
w.hook_command_run('/join', 'join_command_cb', '')
w.hook_command_run('/part', 'part_command_cb', '')
w.hook_command_run('/leave', 'part_command_cb', '')
+ w.hook_command_run("/input complete_next", "complete_next_cb", "")
+ w.hook_completion("nicks", "complete @-nicks for slack",
+ "nick_completion_cb", "")
w.bar_item_new('slack_typing_notice', 'typing_bar_item_cb', '')
# END attach to the weechat hooks we need